Domanda

Sto cercando di avvolgere la mia mente attorno al modello MVP utilizzato in un'app C # / Winforms. Quindi ho creato un semplice "blocco note" come l'applicazione per provare a elaborare tutti i dettagli. Il mio obiettivo è quello di creare qualcosa che faccia i classici comportamenti di windows di open, save, new oltre a riflettere il nome del file salvato nella barra del titolo. Inoltre, in caso di modifiche non salvate, la barra del titolo dovrebbe includere un *.

Quindi ho creato una vista & amp; un relatore che gestisce lo stato di persistenza dell'applicazione. Un miglioramento che ho preso in considerazione è la scomposizione del codice di gestione del testo, pertanto view / presenter è veramente un'entità a scopo unico.

Ecco una schermata per riferimento ...

alt text

Includo tutti i file rilevanti di seguito. Sono interessato al feedback sul fatto se 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();
        }
    }
}
È stato utile?

Soluzione

L'unico modo per avvicinarsi ad un perfetto modello di vista passiva MVP sarebbe quello di scrivere le proprie triadi MVP per le finestre di dialogo invece di utilizzare le finestre di dialogo WinForms. Quindi è possibile spostare la logica di creazione della finestra di dialogo dalla vista al presentatore.

Questo entra nell'argomento della comunicazione tra triadi mvp, un argomento che di solito viene sorpreso quando si esamina questo schema. Quello che ho trovato funziona per me è collegare 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;

        ...

Il metodo Show () , ovviamente, è responsabile di mostrare un OpenDialogView non menzionato, che accetterebbe l'input dell'utente e lo passerebbe al OpenDialogPresenter . In ogni caso, dovrebbe iniziare a diventare chiaro che un presentatore è un intermediario elaborato. In circostanze diverse, potresti essere tentato di rimodellare un intermediario ma qui è intenzionale:

  • Tieni la logica fuori dalla vista, dove è più difficile testarla
  • Evita dipendenze dirette tra la vista e il modello

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

Ecco alcuni link con alcune delle tecniche che altri hanno usato per gestire la comunicazione della triade:

Altri suggerimenti

Tutto sembra a posto, l'unico livello possibile che vorrei andare oltre è quello di sottrarre la logica per il salvataggio del file e farla gestire dai provider in modo da poter facilmente flettere in metodi di salvataggio alternativi come database, e-mail, cloud storage.

IMO ogni volta che hai a che fare con il contatto con il file system è sempre meglio sottrarlo a un livello, anche rendere più facile deridere e testare.

Una cosa che mi piace fare è sbarazzarmi della comunicazione View to Presenter diretta. Il motivo è che la vista è a livello di interfaccia utente e il presentatore è a livello aziendale. Non mi piace che i miei livelli abbiano una conoscenza intrinseca 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 agisce in modo molto diretto contro il presentatore. Mi piace che il Presentatore sia in grado di ascoltare e manipolare la mia visione in base alla reazione, ma mi piace anche limitare la conoscenza che la mia visione ha del suo presentatore.

Aggiungerei alcuni eventi al mio IPersistenceStateView:

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

Quindi chiedi al mio relatore di 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 modifica l'implementazione della vista in modo che il clic sui pulsanti attivi gli eventi.

Questo rende il presentatore più simile a un burattinaio, reagendo alla vista e tirando le corde; ivi, rimuovendo le chiamate dirette sui metodi del presentatore. Dovrai comunque creare un'istanza del presentatore nella vista, ma si tratta dell'unico lavoro diretto che eseguirai su di esso.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top