批判我简单的MVP Winforms应用程序

我试图围绕C#/ Winforms应用程序中使用的MVP模式。 所以我创建了一个简单的“记事本”,就像应用程序一样,试图找出所有细节。 我的目标是创建一些能够执行open,save,new的经典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三合会通信的模型。 这样做的好处是主持人不需要知道彼此存在。 它通常通过在模型中设置状态来实现,该状态触发事件,然后另一个演示者监听。 一个有趣的想法。 一个我个人没用过的。

以下是其他人用于处理三合会传播的一些技巧的一些链接:

一切看起来不错,唯一可能的水平我会更进一步的是抽象出保存文件的逻辑并由提供商处理,以便稍后您可以轻松地在其他保存方法(如数据库,电子邮件,云存储)中进行灵活处理。

IMO随时处理触摸文件系统时,最好将其抽象出一个级别,这也使得模拟和测试变得更加容易。

我喜欢做的一件事就是摆脱直接的View to Presenter沟通。 原因是视图位于UI级别,演示者位于业务层。 我不喜欢我的图层彼此有固有的知识,我试图尽可能地限制直接沟通。 通常,我的模型是唯一超越图层的东西。 因此,演示者通过界面操纵视图,但视图不会对演示者采取太多直接操作。 我喜欢Presenter能够根据反应倾听和操纵我的观点,但我也想限制我对其主持人的看法。

我将一些事件添加到我的IPersistenceStateView:

 event EventHandler保存;
 event EventHandler Open;
 //等

然后让我的演示者听取这些事件:

 public PersistenceStatePresenter(IPersistenceStateView视图)
 {
     _view = view;

     _view.Save + =(sender,e)=> this.Save();
     _view.Open + =(sender,e)=> this.Open();
    //等

    InitializeModel();
    InitializeView();
 }

然后更改视图实现以使按钮单击以触发事件。

这使得主持人的行为更像是一个木偶大师,对视图做出反应并拉动它的弦; 其中,删除对演示者方法的直接调用。 您仍然需要在视图中实例化演示者,但这是您将要对其进行的唯一直接工作。