Вопрос

Я пытаюсь осмыслить шаблон MVP, используемый в приложении C#/Winforms.Поэтому я создал простое приложение, похожее на «блокнот», чтобы попытаться проработать все детали.Моя цель — создать что-то, что выполняло бы классическое поведение Windows: открытие, сохранение, создание нового, а также отражало бы имя сохраненного файла в строке заголовка.Кроме того, если есть несохраненные изменения, строка заголовка должна содержать *.

Поэтому я создал представление и презентатор, которые управляют состоянием персистентности приложения.Одним из улучшений, которые я рассмотрел, является изменение кода обработки текста, чтобы представление/представитель действительно было единым целевым объектом.

Вот скриншот для справки...

alt text

Я включаю все соответствующие файлы ниже.Меня интересуют отзывы о том, правильно ли я это сделал или есть способы улучшить.

ПримечаниеМодель.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
}

Форма2.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();
}

Персистенсстатепресентер.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 с 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();
}

Затем измените реализацию представления, чтобы нажатие кнопки запускало события.

Это заставляет ведущего действовать скорее как кукловод, реагируя на вид и дергая его за ниточки;при этом удаляются прямые вызовы методов презентатора.Вам все равно придется создать экземпляр презентатора в представлении, но это, пожалуй, единственная непосредственная работа, которую вы с ним проделаете.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top