2009-09-14 1 views
14

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

Итак, я создал представление & ведущего, который управляет состоянием сохранения приложения. Одним из улучшений, которые я рассмотрел, является разрывание кода обработки текста, поэтому view/presenter - это действительно одноцелевая сущность.

Вот снимок экрана, для справки ...

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

ответ

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. В любом случае, должно стать ясно, что ведущий является продуманным посредником. При различных обстоятельствах, вы могли бы попытаться реорганизовать посредник из, но здесь его является намеренным:

  • Keep логики вне поля зрения, где это труднее проверить
  • Избегайте прямые зависимости между представлением и модель

Временами я также видел модель, используемую для связи триады MVP.Преимущество этого заключается в том, что ведущий не должен знать друг друга. Обычно это достигается путем установки состояния в модели, которое запускает событие, которое затем прослушивает другой ведущий. Интересная идея. Один из них я не использовал лично.

Вот несколько ссылок с некоторыми из методов, другие использовали для борьбы с триады связи:

+0

Спасибо за отзыв. Почему вы использовали var с openDialogPresenter? У вас есть какие-либо ссылки, связанные с коммуникацией триады. Думаю, мой нынешний подход склоняется к состоянию в модели с событиями, чтобы вызвать действия у соответствующих докладчиков. Это плохая идея? –

+0

Я обычно использую var по умолчанию, если нет веской причины не только для личных предпочтений. Я обновил свой ответ с помощью нескольких ссылок, касающихся связи триады MVP. –

2

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

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

+0

Да, конечно. Попытка сделать это простым на этом этапе. –

1

Одна вещь, которую я хотел бы сделать, это избавиться от прямой Просмотр сообщения ведущего. Причиной этого является представление на уровне пользовательского интерфейса, а ведущий - на бизнес-уровне. Мне не нравятся мои слои, чтобы иметь неотъемлемые знания друг о друге, и я стараюсь максимально ограничить прямую связь. Как правило, моя модель - это единственное, что выходит за пределы слоев. Таким образом, ведущий манипулирует представлением через интерфейс, но представление не требует особого прямого действия против ведущего. Мне нравится, что Ведущий сможет слушать и манипулировать моим взглядом, основанным на реакции, но я также хотел бы ограничить знание, которое имеет мое мнение у его ведущего.

Я хотел бы добавить некоторые события в мою IPersistenceStateView:

 
event EventHandler Save; 
event EventHandler Open; 
// etc. 

Тогда есть мой Presenter слушать те события:

 
public PersistenceStatePresenter(IPersistenceStateView view) 
{ 
    _view = view; 

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

    InitializeModel(); 
    InitializeView(); 
} 

Затем изменить реализацию вида, чтобы щелчки кнопки сгореть события ,

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

+0

Мне тоже нравится это предложение. –

+0

@Travis: Проблема с этим подходом, если таковая имеется, заключается в том, что контроль над представлением больше не гарантируется только исполнителем, поскольку вы должны сделать события общедоступными. –

+0

@Johann: Я не думаю, что это проблема вообще. Это делает взгляд полностью независимым, самодостаточным и не знает, что контролирует его. Я нахожу, что добавляет гибкость, позволяя использовать представление в разных контекстах, сохраняя при этом шаблон MVP. –

 Смежные вопросы

  • Нет связанных вопросов^_^