2009-09-14 11 views
14

Próbuję zawinąć mój umysł wokół wzorca MVP używanego w aplikacji C#/WinForm. Stworzyłem więc prostą aplikację typu "notatnik", aby wypróbować wszystkie szczegóły. Moim celem jest stworzenie czegoś, co zachowuje klasyczne zachowanie otwarte, zapisywanie, nowe, a także odzwierciedla nazwę zapisanego pliku na pasku tytułu. Ponadto w przypadku niezapisanych zmian pasek tytułu powinien zawierać znak *.Krytyczna moja prosta aplikacja WinForce MVP

Stworzyłem więc przeglądarkę, która zarządza stanem trwałości aplikacji. Jedną z popraw, którą rozważałem, jest przełamanie kodu obsługi tekstu, więc widok/prezenter jest naprawdę jednostką jednofunkcyjną.

Oto zrzut ekranu dla odniesienia ...

alt text

jestem w tym wszystkich istotnych plików poniżej. Interesuje mnie informacja o tym, czy zrobiłem to we właściwy sposób, czy też istnieją sposoby poprawy.

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

To pytanie nie jest już na temat, chociaż byłoby dobrze, gdy został wysłany. W dzisiejszych czasach tego typu pytania byłyby lepsze na _Code Review_. – halfer

Odpowiedz

5

Jedynym sposobem na zbliżenie się do idealnego pasywnego widoku MVP byłoby napisanie własnych triad MVP dla okien dialogowych zamiast korzystania z okien dialogowych WinForm. Następnie możesz przenieść logikę tworzenia dialogów z widoku do prezentera.

To wchodzi w temat komunikacji pomiędzy triadami mvp, tematu, który zwykle jest pomijany przy badaniu tego wzorca. To, co dla mnie znalazłem, to połączenie triad na ich prezenterach.

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; 

     ... 

Sposób Show(), oczywiście, jest odpowiedzialny za wykazujące niewymienione OpenDialogView, który zaakceptuje wejście użytkowników i przekazać go wraz z OpenDialogPresenter. W każdym razie powinno stać się jasne, że prezenter jest wyrafinowanym pośrednikiem. W innych okolicznościach, może być skłonny byłaby pośrednikiem się ale tutaj jego jest zamierzona do:

  • Zachowaj logiki z widoku, gdzie trudniej jest przetestowanie
  • uniknąć bezpośredniej zależności między View i model

Czasami widziałem również model używany do komunikacji triad MVP.Zaletą tego jest to, że prezenter nie musi znać się nawzajem. Zwykle odbywa się to poprzez ustawienie stanu w modelu, który wyzwala zdarzenie, którego słucha inny prezenter. Ciekawy pomysł. Tego, którego nie używałem osobiście.

Oto kilka linków z niektórych technik inni wykorzystywanych do czynienia z triady komunikacji:

+0

Dzięki za opinię. Dlaczego używasz var z openDialogPresenter? Czy masz jakieś linki związane z komunikacją triady? Sądzę, że moje obecne podejście skłania się ku stanowi w modelu z wydarzeniami, które powodują działania u odpowiednich prezenterów. Czy to zły pomysł? –

+0

Używam domyślnie var, chyba że istnieje uzasadniony powód, aby nie, tylko osobiste preferencje. Zaktualizowałem swoją odpowiedź kilkoma linkami dotyczącymi komunikacji triadowej MVP. –

2

Wszystko wygląda dobrze Jedynym możliwym poziomem, który posunęłbym dalej, jest usunięcie logiki zapisywania pliku i sprawdzenie go przez dostawców, aby później można było łatwo zgiąć w alternatywnych metodach oszczędzania, takich jak baza danych, poczta e-mail, przechowywanie w chmurze.

IMO za każdym razem, gdy mamy do czynienia z dotykaniem systemu plików, zawsze lepiej jest go wykopać na niższym poziomie, a także znacznie ułatwiać kpiny i testy.

+0

Tak, oczywiście. Na tym etapie staram się zachować prostotę. –

1

Jedno chciałbym zrobić, to pozbyć bezpośredni Zobacz do komunikacji z prezenterami. Powodem tego jest widok na poziomie interfejsu użytkownika, a prezenter znajduje się w warstwie biznesowej. Nie lubię, gdy moje warstwy mają wrodzoną wiedzę o sobie nawzajem i staram się ograniczać bezpośrednią komunikację tak bardzo, jak to możliwe. Zazwyczaj mój model jest jedyną rzeczą, która wykracza poza warstwy. W ten sposób prezenter manipuluje widokiem za pośrednictwem interfejsu, ale widok nie podejmuje wiele bezpośrednich działań przeciwko prezenterowi. Lubię prezentera, aby móc słuchać i manipulować moim poglądem w oparciu o reakcję, ale lubię też ograniczać wiedzę, jaką mój pogląd ma na temat swojego prezentera.

Chciałbym dodać niektóre zdarzenia do mojego IPersistenceStateView:

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

wtedy moja Presenter słuchać tych wydarzeń:

 
public PersistenceStatePresenter(IPersistenceStateView view) 
{ 
    _view = view; 

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

    InitializeModel(); 
    InitializeView(); 
} 

Następnie zmień widok na realizację mają przycisk kliknie ogień zdarzeń .

To sprawia, że ​​prezenter działa bardziej jak marionetka, reagując na widok i pociągając za sznurki; w tym celu, usuwając bezpośrednie wywołania metod prezentera. Nadal będziesz musiał utworzyć prezentację w widoku, ale to jedyna bezpośrednia praca, jaką możesz w tej sprawie wykonać.

+0

Podoba mi się ta sugestia. –

+0

@Travis: Problem z tym podejściem, jeśli w ogóle, polega na tym, że kontrola widoku nie jest już gwarantowana tylko przez prezentera, ponieważ trzeba upublicznić te wydarzenia. –

+0

@Johann: Nie sądzę, żeby to był problem. Sprawia, że ​​widok jest całkowicie niezależny, samowystarczalny i nieświadomy tego, co go kontroluje. Uważam, że zwiększa to elastyczność, umożliwiając korzystanie z widoku w różnych kontekstach, przy jednoczesnym wykorzystaniu wzorca MVP. –

Powiązane problemy