2009-09-14 22 views
14

Estoy tratando de concentrarme en el patrón MVP utilizado en una aplicación C#/Winforms. Así que creé una aplicación tipo "bloc de notas" simple para tratar de resolver todos los detalles. Mi objetivo es crear algo que tenga los comportamientos clásicos de Windows de abrir, guardar, nuevo y reflejar el nombre del archivo guardado en la barra de título. Además, cuando hay cambios no guardados, la barra de título debe incluir un *.Critique mi aplicación MVP Winforms simple

Así que creé una vista & un presentador que administra el estado de persistencia de la aplicación. Una mejora que he considerado es partir el código de manejo de texto para que la vista/presentador sea verdaderamente una entidad de propósito único.

Aquí es una captura de pantalla para referencia ...

alt text

Estoy incluyendo todos los archivos pertinentes de abajo. Estoy interesado en comentarios sobre si lo hice de la manera correcta o si hay formas de mejorar.

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

Esta pregunta ya no está en el tema, aunque hubiera estado bien cuando se publicó. En la actualidad, las preguntas de este tipo serían mejores en _Code Review_. – halfer

Respuesta

5

La única manera de acercarse más a un patrón perfecto de visión pasiva MVP sería escribir sus propias tríadas MVP para los cuadros de diálogo en lugar de usar los cuadros de diálogo de WinForms. Luego, podría mover la lógica de creación de diálogo de la vista al presentador.

Esto entra en el tema de la comunicación entre las tríadas de mvp, un tema que generalmente se pasa por alto al examinar este patrón. Lo que he encontrado que funciona para mí es conectar tríadas a sus presentadores.

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; 

     ... 

El método Show(), por supuesto, es responsable de una muestra de mencionar OpenDialogView, que aceptar la entrada de los usuarios y pasar a lo largo de la OpenDialogPresenter. En cualquier caso, debería comenzar a quedar claro que un presentador es un intermediario elaborado. En circunstancias diferentes, podría verse tentado a refactorizar un intermediario fuera, pero aquí su es intencional para:

  • lógica Mantener fuera de la vista, donde es más difícil de probar
  • evitar la dependencia directa entre la vista y el modelo

En ocasiones también he visto el modelo utilizado para la comunicación de la tríada MVP.El beneficio de esto es que los presentadores no necesitan conocerse. Generalmente se logra estableciendo un estado en el modelo, que desencadena un evento, que luego otro presentador escucha. Una idea interesante Uno que no he usado personalmente.

He aquí algunos enlaces con algunas de las técnicas que otros han utilizado para hacer frente a la comunicación tríada:

+0

Gracias por los comentarios. ¿Por qué usaste var con openDialogPresenter? ¿Tiene algún enlace relacionado con la comunicación tríada? Creo que mi enfoque actual se inclina hacia el estado en el modelo con eventos para provocar acciones en los presentadores apropiados. ¿Es una mala idea? –

+0

Tiendo a usar var por defecto a menos que haya una razón válida para no hacerlo, solo una preferencia personal. Actualicé mi respuesta con un par de enlaces relacionados con la comunicación de la tríada de MVP. –

2

Todo parece bueno, el único nivel posible que iría más allá es abstraer la lógica para guardar el archivo y que los proveedores lo manejen para poder flexibilizar fácilmente en métodos alternativos de guardado como base de datos, correo electrónico, almacenamiento en la nube.

IMO cada vez que se ocupa de tocar el sistema de archivos, siempre es mejor abstraerlo un nivel, también hace que la burla y las pruebas sean mucho más fáciles.

+0

Sí, por supuesto. Tratando de mantenerlo simple en esta etapa. –

1

Una cosa que me gusta hacer es deshacerse de la directa Ver a la comunicación del presentador. La razón para esto es que la vista está en el nivel de UI y el presentador está en la capa de negocios. No me gusta que mis capas tengan un conocimiento inherente el uno del otro, y trato de limitar la comunicación directa tanto como sea posible. Por lo general, mi modelo es lo único que trasciende las capas. Por lo tanto, el presentador manipula la vista a través de la interfaz, pero la vista no requiere mucha acción directa contra el presentador. Me gusta que el presentador pueda escuchar y manipular mi vista en función de la reacción, pero también me gusta limitar el conocimiento que mi punto de vista tiene de su presentador.

me gustaría añadir algunos eventos a mi IPersistenceStateView:

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

Luego tienen mi Presentador escuchar a esos eventos:

 
public PersistenceStatePresenter(IPersistenceStateView view) 
{ 
    _view = view; 

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

    InitializeModel(); 
    InitializeView(); 
} 

A continuación, cambiar la aplicación a fin de tener los clics de botón de fuego los eventos .

Esto hace que el presentador actúe más como un titiritero, reacciona a la vista y tira de sus hilos; allí, eliminando las llamadas directas en los métodos del presentador. Todavía tendrá que crear una instancia del presentador en la vista, pero ese es el único trabajo directo que hará en él.

+0

Me gusta esta sugerencia también. –

+0

@Travis: El problema con este enfoque, si es que lo tiene, es que el control de la vista ya no se garantiza que solo sea ejecutado por el presentador, ya que debe hacer que los eventos sean públicos. –

+0

@Johann: No creo que esto sea un problema en absoluto. Hace que la vista sea completamente independiente, autónoma e ignorante de lo que la controla. Encuentro que eso agrega flexibilidad, permitiéndole usar la vista en diferentes contextos, al mismo tiempo que aprovecha el patrón de MVP. –

Cuestiones relacionadas