Pergunta

Eu estou tentando envolver minha mente em torno do padrão MVP usado em um aplicativo C # / WinForms. Então, eu criei um simples "bloco de notas" como aplicativo para tentar trabalhar para fora todos os detalhes. Meu objetivo é criar algo que faz os comportamentos janelas clássicas de abrir, salvar, novo, bem como reflete o nome do arquivo salvo na barra de título. Além disso, quando há alterações não salvas, a barra de título deve incluir um *.

Então, eu criei uma visão e um apresentador que gerenciar o estado da persistência do aplicativo. Uma melhoria Eu considerei está quebrando o código de manipulação de texto para que a vista / apresentador é realmente uma entidade de finalidade única.

Aqui está uma captura de tela para referência ...

text alt

Eu estou incluindo todos os arquivos relevantes abaixo. Estou interessado em feedback sobre se eu fiz isso da maneira certa ou se existem maneiras de melhorar.

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();
        }
    }
}
Foi útil?

Solução

A única maneira de obter qualquer mais perto de um padrão perfeito vista MVP passivo seria escrever seus próprios tríades MVP para os diálogos em vez de usar os diálogos WinForms. Então você pode mover a lógica de criação de diálogo a partir da vista para o apresentador.

Este entra no tema de comunicação entre tríades mvp, um tópico que normalmente é encoberto ao examinar esse padrão. O que eu encontrei funciona para mim é conectar tríades em seus apresentadores.

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;

        ...

O método Show(), é claro, é responsável por mostrar um OpenDialogView unmentioned, que aceitaria a entrada de usuários e passá-lo para o OpenDialogPresenter. Em qualquer caso, deve-se começar a ficar claro que um apresentador é um intermediário elaborado. Em circunstâncias diferentes, você pode ser tentado a refatorar um intermediário fora, mas aqui a sua é intencional para:

  • Mantenha a lógica fora da vista, onde é mais difícil de teste
  • Evite dependências diretas entre a visão eo modelo

Às vezes eu também vi o modelo utilizado para MVP tríade comunicação. O benefício disso é do apresentador não precisa conhecer uns aos outros existir. Geralmente é feito definindo um estado no modelo, o que desencadeia um evento, que outro apresentador, em seguida, escuta. Uma idéia interessante. Um que eu não usei pessoalmente.

Eis algumas ligações com algumas das técnicas outros têm usado para lidar com a comunicação tríade:

Outras dicas

Tudo parece ser bom o único nível possível eu iria mais longe é abstrair a lógica para salvar o arquivo e ter esse tratado por provedores assim mais tarde você poderia facilmente dobrar em métodos de poupança alternativos, tais como banco de dados, e-mail, armazenamento em nuvem.

IMO sempre que lidar com tocar o sistema de arquivos é sempre melhor para abstrair-lo afastado um nível, também faz zombando e testar muito mais fácil.

Uma coisa que eu gosto de fazer é se livrar de vista directa para a comunicação Presenter. A razão para isso é a vista é ao nível da interface do usuário eo apresentador está na camada de negócios. Eu não gosto de minhas camadas ter conhecimento inerente do outro, e eu tentar limitar a comunicação direta, tanto quanto possível. Normalmente, o meu modelo é a única coisa que transcende camadas. se, assim, as manipula apresentador a visualização através da interface, mas a vista não ter muita ação direta contra o apresentador. I como o apresentador de ser capaz de ouvir e manipular meu ponto de vista baseado na reação, mas eu também gosto de limitar o conhecimento meu ponto de vista tem de seu apresentador.

Eu gostaria de acrescentar alguns eventos ao meu IPersistenceStateView:

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

Então, o meu Apresentador ouvir esses eventos:

public PersistenceStatePresenter(IPersistenceStateView view)
{
    _view = view;

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

   InitializeModel();
   InitializeView();
}

Em seguida, altere a implementação vista a ter o botão cliques disparar os eventos.

Isso faz com que o acto apresentador mais como um puppetmaster, reagindo à vista e puxar as cordas; nela, removendo as chamadas diretas sobre os métodos do apresentador. Você ainda vai ter que instanciar o apresentador na vista, mas que é sobre a obra única direta você vai fazer sobre ele.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top