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

时间:2009-09-14 15:37:50

标签: c# winforms model-view-controller design-patterns mvp

我正在尝试围绕C#/ Winforms应用程序中使用的MVP模式。所以我创建了一个简单的“记事本”,就像应用程序一样,试图找出所有细节。我的目标是创建一些东西来执行打开,保存,新的经典窗口行为,以及在标题栏中反映已保存文件的名称。此外,当有未保存的更改时,标题栏应包含*。

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

这是一个参考屏幕截图...

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

3 个答案:

答案 0 :(得分: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三合会通信的模型。这样做的好处是主持人不需要知道彼此存在。它通常通过在模型中设置状态来实现,该状态触发事件,然后另一个演示者监听。一个有趣的想法。一个我没有亲自使用过。

以下是其他人用来处理黑社会通信的一些技巧的链接:

答案 1 :(得分:2)

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

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

答案 2 :(得分:1)

我喜欢做的一件事是摆脱直接的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();
}

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

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