2009-09-14 79 views
14

我试图围绕在C#/ Winforms应用程序中使用的MVP模式。所以我创建了一个简单的“记事本”应用程序来试图解决所有的细节。我的目标是创建一些可以打开,保存,新建的经典窗口行为,并在标题栏中反映已保存文件的名称。另外,当有未保存的更改时,标题栏应包含*。评论我的简单MVP Winforms应用程序

因此,我创建了一个视图&管理应用程序持久状态的演示者。我考虑过的一个改进是分解文本处理代码,以便视图/演示者是真正的单一目的实体。

这里是一个屏幕截图以供参考......

alt text

我在内的所有下面的相关文件。我对我是否以正确的方式完成或者是否有改进的方法感兴趣。

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(); 
     } 
    } 
} 
+6

这个问题已经不在话题上,即使它在发布时也没有问题。现在这些问题在_Code Review_上会更好。 – halfer 2014-02-06 22:07:54

回答

5

获得更接近完美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三元组通信的模型。这样做的好处是主持人不需要知道彼此存在。它通常通过在模型中设置一个状态来完成,该状态触发一个事件,然后另一个演示者再听。一个有趣的想法。一个我没有亲自使用。

这里有一些其他人用来对付黑社会通信技术的几个环节:

+0

感谢您的反馈。你为什么在openDialogPresenter中使用var?你有任何与黑社会沟通有关的链接吗?我想我目前的方法倾向于模型中的状态,并在事件中引发适当的演示者的操作。这是一个坏主意吗? – 2009-09-16 13:13:41

+0

我倾向于默认使用var,除非有一个有效的理由不仅仅是个人喜好。我用一些处理MVP黑社会沟通的链接更新了我的答案。 – 2009-09-17 19:53:38

2

一切都看起来不错,我会走得更远的唯一可能的级别是抽象出保存文件的逻辑,并让提供程序处理这些逻辑,以便稍后可以轻松地在备用保存方法(如数据库,电子邮件和云存储)中进行灵活操作。

海事组织任何时候处理触摸文件系统总是更好地将其抽离一个级别,也使嘲笑和测试更容易。

+0

当然可以。在这个阶段试图保持简单。 – 2009-09-14 15:47:13

1

有一件事我喜欢做的是摆脱直接查看演示者通信。原因在于该视图位于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(); 
} 

然后更改视图实现有按钮点击触发事件。

这使得演示者更像一个傀儡大师,对视图做出反应并拉动它的字符串;从而消除了演示者方法的直接调用。您仍然必须在视图中实例化演示者,但这是您要做的唯一直接工作。

+0

我也喜欢这个建议。 – 2009-09-30 14:28:25

+0

@ Travis:这种方法的问题在于,对视图的控制不再保证只由演示者执行,因为你需要公开事件。 – 2010-04-14 10:16:48

+0

@Johann:我认为这不是问题。它使得视图完全独立,自成一体并且不知道控制它的是什么。我发现这增加了灵活性,允许您在不同的上下文中使用视图,同时仍然利用MVP模式。 – 2010-04-21 01:06:47