2009-09-14 4 views
14

Ich versuche, mich um das MVP-Muster in einer C#/Winforms App verwendet. Also habe ich eine einfache "Notizblock" -ähnliche Anwendung erstellt, um alle Details auszuarbeiten. Mein Ziel ist es, etwas zu erstellen, das die klassischen Windows-Verhaltensweisen "Öffnen", "Speichern" und "Neu" ausführt und den Namen der gespeicherten Datei in der Titelleiste widerspiegelt. Wenn ungespeicherte Änderungen vorhanden sind, sollte die Titelleiste ein * enthalten.Kritik meine einfache MVP Winforms App

So erstellte ich eine Ansicht & ein Presenter, die den Persistenzstatus der Anwendung verwalten. Eine Verbesserung, die ich in Betracht gezogen habe, besteht darin, den Textverarbeitungscode auszubrechen, so dass die Ansicht/der Präsentator wirklich eine Einzweckeinheit ist.

ist hier ein Screenshot Referenz ...

alt text

ich alle relevanten Dateien unten darunter. Ich bin an Rückmeldungen darüber interessiert, ob ich es richtig gemacht habe oder ob es Verbesserungsmöglichkeiten gibt.

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

Diese Frage steht nicht mehr zum Thema, obwohl es bei der Veröffentlichung gut gewesen wäre. Heutzutage sind Fragen dieser Art in _Code Review_ besser. – halfer

Antwort

5

Die einzige Möglichkeit, einem perfekten passiven MVP-Ansichtsmuster näher zu kommen, wäre, eigene MVP-Triaden für die Dialoge zu schreiben, anstatt die WinForms-Dialoge zu verwenden. Dann könnten Sie die Dialogerstellungslogik von der Ansicht zum Präsentator verschieben.

Dies wird in das Thema der Kommunikation zwischen mvp Triaden, ein Thema, das bei der Untersuchung dieses Musters in der Regel beschönigt ist. Was ich gefunden habe, funktioniert für mich, Triaden an ihre Moderatoren zu verbinden.

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() Die Methode ist natürlich verantwortlich für eine unmentioned OpenDialogView zeigt, die den Benutzer Eingabe akzeptieren würde, und es entlang der OpenDialogPresenter passieren. Auf jeden Fall sollte klar werden, dass ein Moderator ein ausgeklügelter Vermittler ist. Unter anderen Umständen könnten Sie einen Zwischenhändler zu Refactoring versucht sein, aber hier sind sein als beabsichtigt:

  • Halt Logik aus der Sicht, wo es schwieriger ist,
  • Vermeiden Sie direkte Abhängigkeiten zwischen der Ansicht zu testen und der Modell

Manchmal habe ich auch das Modell für MVP Triade Kommunikation gesehen.Der Vorteil davon ist, dass der Präsentator sich nicht kennen muss. Dies wird normalerweise erreicht, indem ein Zustand im Modell festgelegt wird, der ein Ereignis auslöst, auf das ein anderer Moderator dann wartet. Eine interessante Idee. Eine, die ich nicht persönlich benutzt habe.

Hier ein paar Links mit einigen der Techniken haben andere Wissenschaftler mit Triade Kommunikation beschäftigen:

+0

Danke für die Rückmeldung. Warum hast du var mit openDialogPresenter benutzt? Haben Sie irgendwelche Links zur Triadenkommunikation? Ich denke, mein gegenwärtiger Ansatz neigt dazu, sich im Modell mit Ereignissen anzugleichen, die in geeigneten Präsentatoren Aktionen auslösen. Ist das eine schlechte Idee? –

+0

Ich neige dazu, var standardmäßig zu verwenden, es sei denn, es gibt einen gültigen Grund, nicht, nur eine persönliche Vorliebe. Ich habe meine Antwort mit ein paar Links aktualisiert, die sich mit der MVP-Triaden-Kommunikation befassen. –

2

Alles sieht gut aus die einzige mögliche Ebene, die ich weiter gehen würde, ist die Logik für das Speichern der Datei zu abstrahieren und diese von Providern zu behandeln, so dass Sie später alternative Speichermethoden wie Datenbank, E-Mail, Cloud-Speicher einfach verwenden können.

IMO Immer, wenn Sie mit dem Berühren des Dateisystems umgehen, ist es immer besser, es weg zu abstrahieren, macht auch Mocking und Tests viel einfacher.

+0

Ja, natürlich. Versuchen, es in diesem Stadium einfach zu halten. –

1

Eine Sache, die ich tun möchte, ist der direkte loswerden Zeigen Sie auf Presenter-Kommunikation. Der Grund hierfür ist, dass die Ansicht auf der Ebene der Benutzeroberfläche erfolgt und der Präsentator sich auf der Business-Ebene befindet. Ich mag es nicht, dass meine Schichten inhärentes Wissen voneinander haben, und ich versuche, die direkte Kommunikation so weit wie möglich zu begrenzen. Normalerweise ist mein Modell das Einzige, das über Ebenen hinausgeht. So manipuliert der Präsentator die Ansicht durch die Schnittstelle, aber die Ansicht nimmt keine große direkte Wirkung gegen den Präsentator auf. Ich mag es, wenn der Moderator meine Sicht aufgrund von Reaktionen hören und manipulieren kann, aber ich möchte auch gerne das Wissen einschränken, das mein Sprecher von seinem Moderator hat.

würde ich einige Ereignisse zu meinem IPersistenceStateView hinzufügen:

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

Dann meine Presenter auf diese Ereignisse hören haben:

 
public PersistenceStatePresenter(IPersistenceStateView view) 
{ 
    _view = view; 

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

    InitializeModel(); 
    InitializeView(); 
} 

dann die Ansicht Implementierung ändern auf die Schaltfläche klickt, die Ereignisse ausgelöst haben .

Dadurch wirkt der Moderator eher wie ein Puppenspieler, der auf die Ansicht reagiert und die Fäden zieht; darin, die direkten Anrufe auf den Methoden des Vorführers zu entfernen. Sie müssen den Präsentator in der Ansicht immer noch instanziieren, aber das ist die einzige direkte Arbeit, die Sie daran ausführen werden.

+0

Ich mag diesen Vorschlag auch. –

+0

@Travis: Das Problem bei diesem Ansatz besteht darin, dass die Kontrolle der Ansicht nicht mehr nur vom Moderator garantiert wird, da Sie die Ereignisse öffentlich machen müssen. –

+0

@Johann: Ich denke nicht, dass das Problem überhaupt ist. Es macht den Blick völlig unabhängig, unabhängig und nicht bewusst, was ihn kontrolliert. Ich finde, das fügt Flexibilität hinzu, so dass Sie die Ansicht in verschiedenen Kontexten verwenden können, während Sie immer noch das MVP-Muster nutzen. –