Question

J'essaie de garder à l'esprit le motif MVP utilisé dans une application C # / Winforms. J'ai donc créé un "bloc-notes" simple. comme application pour essayer de régler tous les détails. Mon objectif est de créer quelque chose qui respecte les comportements classiques de Windows en matière d'ouverture, de sauvegarde, de création et de modification du nom du fichier enregistré dans la barre de titre. De plus, lorsqu'il y a des modifications non enregistrées, la barre de titre doit inclure un *.

J'ai donc créé une vue & amp; un présentateur qui gère l'état de persistance de l'application. Une des améliorations que j'ai envisagée consiste à modifier le code de traitement du texte afin que le présentateur / présentateur soit véritablement une entité à usage unique.

Voici une capture d'écran pour référence ...

texte alt

J'inclus tous les fichiers pertinents ci-dessous. J'aimerais savoir si je l'ai fait correctement ou s'il existe des moyens de s'améliorer.

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();
        }
    }
}
Était-ce utile?

La solution

La seule façon de se rapprocher d'un modèle de vue passive MVP parfait serait d'écrire vos propres triades MVP pour les boîtes de dialogue au lieu d'utiliser les boîtes de dialogue WinForms. Ensuite, vous pouvez déplacer la logique de création de dialogue de la vue vers le présentateur.

Cela nous amène au sujet de la communication entre les triades mvp, sujet qui est généralement passé sous silence lorsque nous examinons ce modèle. Ce que j’ai trouvé fonctionne pour moi, c’est de connecter des triades à leurs présentateurs.

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;

        ...

La méthode Show () est bien sûr chargée d'afficher un OpenDialogView non mentionné, qui accepterait la saisie de l'utilisateur et la transmettrait au OpenDialogPresenter. . Dans tous les cas, il devrait commencer à devenir clair qu'un présentateur est un intermédiaire élaboré. Dans des circonstances différentes, vous pourriez être tenté de repenser un intermédiaire, mais il est intentionnel de:

  • Conservez la logique hors de la vue, car il est plus difficile de la tester
  • Évitez les dépendances directes entre la vue et le modèle

J'ai parfois vu le modèle utilisé pour la communication triade MVP. L'avantage de ceci est que le présentateur n'a pas besoin de se connaître. Cela est généralement accompli en définissant un état dans le modèle, ce qui déclenche un événement qu'un autre présentateur écoute ensuite. Une idée intéressante. Celui que je n'ai pas utilisé personnellement.

Voici quelques liens avec certaines des techniques utilisées par d'autres pour traiter la communication en triade:

Autres conseils

Tout semble aller bien, le seul niveau possible pour aller plus loin consiste à faire abstraction de la logique de sauvegarde du fichier et de le faire gérer par les fournisseurs afin que vous puissiez facilement intégrer d'autres méthodes de sauvegarde telles que la base de données, la messagerie électronique, le stockage en nuage.

IMO, chaque fois que vous manipulez le système de fichiers, il est toujours préférable de l'abstraire, il est également plus facile de se moquer et de tester.

Une des choses que j'aime faire est de me débarrasser de la communication directe de View to Presenter. La raison en est que la vue se situe au niveau de l'interface utilisateur et que le présentateur se situe dans la couche de gestion. Je n'aime pas que mes couches aient une connaissance inhérente les unes des autres et je cherche à limiter autant que possible la communication directe. Mon modèle est généralement la seule chose qui transcende les calques. Ainsi, le présentateur manipule la vue via l'interface, mais la vue ne prend pas beaucoup d'action directe contre le présentateur. J'aime que le présentateur puisse écouter et manipuler mon point de vue en fonction de la réaction, mais j'aime aussi limiter la connaissance que mon point de vue a de son présentateur.

J'ajouterais quelques événements à mon IPersistenceStateView:

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

Ensuite, demandez à mon animateur d'écouter ces événements:

public PersistenceStatePresenter(IPersistenceStateView view)
{
    _view = view;

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

   InitializeModel();
   InitializeView();
}

Modifiez ensuite l'implémentation de la vue pour que les clics du bouton déclenchent les événements.

Cela fait en sorte que le présentateur se comporte davantage comme un maître de marionnettes, réagissant à la vue et tirant les ficelles; en supprimant les appels directs aux méthodes du présentateur. Vous devrez toujours instancier le présentateur dans la vue, mais c'est à peu près le seul travail direct que vous ferez dessus.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top