Pregunta

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

Así que creé una vista & amp; Un presentador que gestiona el estado de persistencia de la aplicación. Una mejora que he considerado es romper el código de manejo de texto para que la vista / presentador sea realmente una entidad de un solo propósito.

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

texto alternativo ??

Incluyo todos los archivos relevantes a continuación. Me interesan los comentarios sobre si lo he hecho 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();
        }
    }
}
¿Fue útil?

Solución

La única forma de acercarse a un patrón de vista pasiva MVP perfecto sería escribir sus propias tríadas MVP para los cuadros de diálogo en lugar de utilizar los cuadros de diálogo 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 mvp, un tema que generalmente se pasa por alto al examinar este patrón. Lo que he encontrado que funciona para mí es conectar triadas en 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 mostrar un OpenDialogView no mencionado, que aceptaría la entrada de los usuarios y la pasaría al OpenDialogPresenter . En cualquier caso, debería comenzar a ser claro que un presentador es un intermediario elaborado. En diferentes circunstancias, puede verse tentado a refactorizar a un intermediario, pero aquí es intencional:

  • Mantenga la lógica fuera de la vista, donde es más difícil de probar
  • Evite dependencias directas entre la vista y el modelo

A veces también he visto el modelo utilizado para la comunicación de tríadas MVP. El beneficio de esto es que los presentadores no necesitan conocerse entre sí. Por lo general, se logra al establecer un estado en el modelo, que desencadena un evento, que luego otro presentador escucha. Una idea interesante Uno que no he usado personalmente.

Aquí hay algunos enlaces con algunas de las técnicas que otros han usado para lidiar con la comunicación triada:

Otros consejos

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

OMI cada vez que tratas de tocar el sistema de archivos, siempre es mejor abstraerlo un nivel, también hace que burlarse y probar sean mucho más fáciles.

Una cosa que me gusta hacer es deshacerme de la comunicación directa de View to Presenter. La razón de esto es que la vista está en el nivel de la interfaz de usuario y el presentador está en la capa empresarial. 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 toma mucha acción directa contra el presentador. Me gusta que el presentador pueda escuchar y manipular mi punto de 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.

Agregaría algunos eventos a mi IPersistenceStateView:

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

Luego haga que mi presentador escuche esos eventos:

public PersistenceStatePresenter(IPersistenceStateView view)
{
    _view = view;

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

   InitializeModel();
   InitializeView();
}

Luego cambie la implementación de la vista para que los clics en los botones activen los eventos.

Esto hace que el presentador actúe más como un titiritero, reaccionando a la vista y tirando de sus cuerdas; 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.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top