2009-09-14 11 views
14

Sto cercando di farmi un'idea del pattern MVP utilizzato in un'app C#/Winforms. Così ho creato una semplice applicazione simile a "notepad" per provare a elaborare tutti i dettagli. Il mio obiettivo è creare qualcosa che mostri i classici comportamenti di Windows aperti, salvati, nuovi e che rispecchino il nome del file salvato nella barra del titolo. Inoltre, quando ci sono modifiche non salvate, la barra del titolo dovrebbe includere un *.Critica la mia semplice app Winform MVP

Quindi ho creato una vista & un presentatore che gestisce lo stato di persistenza dell'applicazione. Un miglioramento che ho considerato è scomporre il codice di gestione del testo in modo che la vista/presentatore sia veramente un'entità a scopo singolo.

Ecco una schermata di riferimento ...

alt text

sto inclusi tutti i file rilevanti sotto. Sono interessato al feedback sul fatto che l'ho fatto nel modo giusto o se ci sono modi per migliorare.

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

Questa domanda non è più in argomento, anche se sarebbe stato corretto quando è stato pubblicato. In questi giorni le domande di questo tipo sarebbero migliori su _Code Review_. – halfer

risposta

5

L'unico modo per avvicinarsi a un pattern di visualizzazione passiva MVP perfetto sarebbe quello di scrivere le proprie triade MVP per le finestre di dialogo anziché utilizzare le finestre di dialogo di WinForms. Quindi è possibile spostare la logica di creazione della finestra di dialogo dalla vista al presentatore.

Questo entra nel tema della comunicazione tra le triadi di MVP, un argomento che viene solitamente nascosto quando si esamina questo modello. Quello che ho trovato funziona per me è connettere le triadi ai loro presentatori.

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; 

     ... 

Procedimento Show(), naturalmente, è responsabile per mostrare un unmentioned OpenDialogView, che accetta l'ingresso degli utenti e passarlo insieme al OpenDialogPresenter. In ogni caso, dovrebbe essere chiaro che un presentatore è un intermediario elaborato. In circostanze diverse, si potrebbe essere tentati di refactoring un intermediario fuori, ma qui la sua è voluto:

  • logica tenere fuori dalla vista, in cui è più difficile per testare
  • evitare le dipendenze dirette tra la vista e la modello

A volte ho anche visto il modello utilizzato per la comunicazione della triade MVP.Il vantaggio di questo è che il presentatore non ha bisogno di conoscersi l'un l'altro. Solitamente si ottiene impostando uno stato nel modello, che attiva un evento, che viene poi ascoltato da un altro presentatore. Un'idea interessante Uno che non ho usato personalmente.

Ecco alcuni link con alcune delle tecniche che altri hanno usato a che fare con la comunicazione triade:

+0

Grazie per il feedback. Perché hai usato var con openDialogPresenter? Avete dei collegamenti relativi alla comunicazione della triade. Immagino che il mio approccio attuale sia inclinato verso lo stato nel modello con eventi che causino azioni in relatori appropriati. È una cattiva idea? –

+0

Tendo ad usare var di default a meno che non ci sia un valido motivo per non farlo, solo una preferenza personale. Ho aggiornato la mia risposta con un paio di link relativi alla comunicazione della triade MVP. –

2

Tutto sembra a posto, l'unico livello possibile che andrei oltre è quello di astrarre la logica per salvare il file e farlo gestire dai fornitori in modo da poter facilmente flettere in metodi di salvataggio alternativi come database, e-mail, cloud storage.

IMO ogni volta che si tocca toccare il file system è sempre meglio estrapolarlo di un livello, inoltre rende più semplice il mocking e il test.

+0

Sì, certo. Cercando di renderlo semplice in questa fase. –

1

Una cosa che mi piace fare è sbarazzarsi di diretta Visualizza la comunicazione di Presenter. La ragione di ciò è la vista è al livello di interfaccia utente e il presentatore è a livello aziendale. Non mi piace che i miei strati abbiano una conoscenza inerente l'uno dell'altro e cerco di limitare il più possibile la comunicazione diretta. In genere, il mio modello è l'unica cosa che trascende i livelli. Quindi il presentatore manipola la vista attraverso l'interfaccia, ma la vista non richiede molta azione diretta contro il presentatore. Mi piace che il presentatore sia in grado di ascoltare e manipolare la mia opinione sulla base della reazione, ma mi piace anche limitare la conoscenza che il mio punto di vista ha del suo presentatore.

mi piacerebbe aggiungere alcuni eventi al mio IPersistenceStateView:

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

Poi ho il mio presentatore ascoltare quegli eventi:

 
public PersistenceStatePresenter(IPersistenceStateView view) 
{ 
    _view = view; 

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

    InitializeModel(); 
    InitializeView(); 
} 

Quindi modificare l'implementazione al fine di avere i clic pulsante di fuoco gli eventi .

Questo fa sì che il presentatore agisca più come un burattinaio, reagendo alla vista e tirando le corde; in esso, rimuovendo le chiamate dirette sui metodi del presentatore. Dovrai comunque istanziare il presentatore nella vista, ma si tratta dell'unico lavoro diretto che dovrai svolgere su di esso.

+0

Anche a me piace questo suggerimento. –

+0

@Travis: il problema con questo approccio, se presente, è che il controllo della vista non è più garantito che venga eseguito solo dal presentatore, poiché è necessario rendere pubblici gli eventi. –

+0

@Johann: Non penso che questo sia un problema. Rende la visione completamente indipendente, autonoma e inconsapevole di ciò che la controlla. Trovo che aggiunga flessibilità, consentendo di utilizzare la vista in diversi contesti, pur sfruttando ancora il pattern MVP. –