批评我的简单 MVP Winforms 应用程序 [关闭]
-
07-07-2019 - |
题
我正在尝试围绕 C#/Winforms 应用程序中使用的 MVP 模式进行思考。因此,我创建了一个简单的“记事本”之类的应用程序来尝试计算出所有细节。我的目标是创建一些能够执行打开、保存、新建等经典 Windows 行为的东西,并在标题栏中反映保存文件的名称。此外,当有未保存的更改时,标题栏应包含 *。
因此,我创建了一个视图和一个演示器来管理应用程序的持久状态。我考虑过的一项改进是打破文本处理代码,使视图/演示者真正成为单一用途的实体。
这是供参考的屏幕截图...
我在下面包含了所有相关文件。我对有关我是否以正确的方式完成工作或是否有改进方法的反馈感兴趣。
NoteModel.cs:
public class NoteModel : INotifyPropertyChanged
{
public string Filename { get; set; }
public bool IsDirty { get; set; }
string _sText;
public readonly string DefaultName = "Untitled.txt";
public string TheText
{
get { return _sText; }
set
{
_sText = value;
PropertyHasChanged("TheText");
}
}
public NoteModel()
{
Filename = DefaultName;
}
public void Save(string sFilename)
{
FileInfo fi = new FileInfo(sFilename);
TextWriter tw = new StreamWriter(fi.FullName);
tw.Write(TheText);
tw.Close();
Filename = fi.FullName;
IsDirty = false;
}
public void Open(string sFilename)
{
FileInfo fi = new FileInfo(sFilename);
TextReader tr = new StreamReader(fi.FullName);
TheText = tr.ReadToEnd();
tr.Close();
Filename = fi.FullName;
IsDirty = false;
}
private void PropertyHasChanged(string sPropName)
{
IsDirty = true;
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(sPropName));
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
Form2.cs:
public partial class Form2 : Form, IPersistenceStateView
{
PersistenceStatePresenter _peristencePresenter;
public Form2()
{
InitializeComponent();
}
#region IPersistenceStateView Members
public string TheText
{
get { return this.textBox1.Text; }
set { textBox1.Text = value; }
}
public void UpdateFormTitle(string sTitle)
{
this.Text = sTitle;
}
public string AskUserForSaveFilename()
{
SaveFileDialog dlg = new SaveFileDialog();
DialogResult result = dlg.ShowDialog();
if (result == DialogResult.Cancel)
return null;
else
return dlg.FileName;
}
public string AskUserForOpenFilename()
{
OpenFileDialog dlg = new OpenFileDialog();
DialogResult result = dlg.ShowDialog();
if (result == DialogResult.Cancel)
return null;
else
return dlg.FileName;
}
public bool AskUserOkDiscardChanges()
{
DialogResult result = MessageBox.Show("You have unsaved changes. Do you want to continue without saving your changes?", "Disregard changes?", MessageBoxButtons.YesNo);
if (result == DialogResult.Yes)
return true;
else
return false;
}
public void NotifyUser(string sMessage)
{
MessageBox.Show(sMessage);
}
public void CloseView()
{
this.Dispose();
}
public void ClearView()
{
this.textBox1.Text = String.Empty;
}
#endregion
private void btnSave_Click(object sender, EventArgs e)
{
_peristencePresenter.Save();
}
private void btnOpen_Click(object sender, EventArgs e)
{
_peristencePresenter.Open();
}
private void btnNew_Click(object sender, EventArgs e)
{
_peristencePresenter.CleanSlate();
}
private void Form2_Load(object sender, EventArgs e)
{
_peristencePresenter = new PersistenceStatePresenter(this);
}
private void Form2_FormClosing(object sender, FormClosingEventArgs e)
{
_peristencePresenter.Close();
e.Cancel = true; // let the presenter handle the decision
}
private void textBox1_TextChanged(object sender, EventArgs e)
{
_peristencePresenter.TextModified();
}
}
IPersistenceStateView.cs
public interface IPersistenceStateView
{
string TheText { get; set; }
void UpdateFormTitle(string sTitle);
string AskUserForSaveFilename();
string AskUserForOpenFilename();
bool AskUserOkDiscardChanges();
void NotifyUser(string sMessage);
void CloseView();
void ClearView();
}
PersistenceStatePresenter.cs
public class PersistenceStatePresenter
{
IPersistenceStateView _view;
NoteModel _model;
public PersistenceStatePresenter(IPersistenceStateView view)
{
_view = view;
InitializeModel();
InitializeView();
}
private void InitializeModel()
{
_model = new NoteModel(); // could also be passed in as an argument.
_model.PropertyChanged += new PropertyChangedEventHandler(_model_PropertyChanged);
}
private void InitializeView()
{
UpdateFormTitle();
}
private void _model_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "TheText")
_view.TheText = _model.TheText;
UpdateFormTitle();
}
private void UpdateFormTitle()
{
string sTitle = _model.Filename;
if (_model.IsDirty)
sTitle += "*";
_view.UpdateFormTitle(sTitle);
}
public void Save()
{
string sFilename;
if (_model.Filename == _model.DefaultName || _model.Filename == null)
{
sFilename = _view.AskUserForSaveFilename();
if (sFilename == null)
return; // user canceled the save request.
}
else
sFilename = _model.Filename;
try
{
_model.Save(sFilename);
}
catch (Exception ex)
{
_view.NotifyUser("Could not save your file.");
}
UpdateFormTitle();
}
public void TextModified()
{
_model.TheText = _view.TheText;
}
public void Open()
{
CleanSlate();
string sFilename = _view.AskUserForOpenFilename();
if (sFilename == null)
return;
_model.Open(sFilename);
_model.IsDirty = false;
UpdateFormTitle();
}
public void Close()
{
bool bCanClose = true;
if (_model.IsDirty)
bCanClose = _view.AskUserOkDiscardChanges();
if (bCanClose)
{
_view.CloseView();
}
}
public void CleanSlate()
{
bool bCanClear = true;
if (_model.IsDirty)
bCanClear = _view.AskUserOkDiscardChanges();
if (bCanClear)
{
_view.ClearView();
InitializeModel();
InitializeView();
}
}
}
解决方案
更接近完美 MVP 被动视图模式的唯一方法是为对话框编写您自己的 MVP 三元组,而不是使用 WinForms 对话框。然后,您可以将对话框创建逻辑从视图移动到演示者。
这涉及到 mvp 三合会之间的沟通主题,在检查此模式时通常会忽略这个主题。我发现对我有用的是在他们的主持人中连接三合会。
public class PersistenceStatePresenter
{
...
public Save
{
string sFilename;
if (_model.Filename == _model.DefaultName || _model.Filename == null)
{
var openDialogPresenter = new OpenDialogPresenter();
openDialogPresenter.Show();
if(!openDialogPresenter.Cancel)
{
return; // user canceled the save request.
}
else
sFilename = openDialogPresenter.FileName;
...
这 Show()
当然,方法负责显示未提及的 OpenDialogView
, ,它将接受用户输入并将其传递给 OpenDialogPresenter
. 。无论如何,人们应该开始清楚演示者是一个精心设计的中间人。在不同的情况下,您可能会想重构中间人,但这里的目的是:
- 将逻辑置于视图之外,因为这样更难测试
- 避免视图和模型之间的直接依赖
有时我还看到用于 MVP 三元组通信的模型。这样做的好处是演示者不需要知道对方的存在。它通常通过在模型中设置一个状态来完成,该状态会触发一个事件,然后另一个演示者会侦听该事件。一个有趣的想法。我个人没用过的一款。
以下是其他人用来处理三合会通信的一些技术的一些链接:
其他提示
一切看起来都很好唯一可能的水平我会更进一步的是抽象出保存文件的逻辑并由提供商处理,以便以后你可以轻松地使用其他保存方法,如数据库,电子邮件,云存储。 / p>
IMO随时处理触摸文件系统时,最好将其抽象出一个级别,这也使得模拟和测试变得更加容易。
我喜欢做的一件事是摆脱直接的View to Presenter沟通。原因是视图位于UI级别,演示者位于业务层。我不喜欢我的图层彼此有固有的知识,我试图尽可能地限制直接沟通。通常,我的模型是唯一超越图层的东西。因此,演示者通过界面操纵视图,但视图不会对演示者采取太多直接操作。我喜欢Presenter能够根据反应倾听和操纵我的观点,但我也想限制我对观众的看法。
我会在IPersistenceStateView中添加一些事件:
event EventHandler Save; event EventHandler Open; // etc.
然后让我的演讲者听取这些事件:
public PersistenceStatePresenter(IPersistenceStateView view) { _view = view; _view.Save += (sender, e) => this.Save(); _view.Open += (sender, e) => this.Open(); // etc. InitializeModel(); InitializeView(); }
然后更改视图实现以使按钮单击以触发事件。
这使得主持人的行为更像是一个傀儡主义者,对观点作出反应并拉动它的弦;其中,删除对演示者方法的直接调用。您仍然需要在视图中实例化演示者,但这是您将要对其进行的唯一直接工作。