문제

C#/Winforms 앱에 사용되는 MVP 패턴에 대해 생각해 보려고 합니다.그래서 모든 세부 사항을 처리하기 위해 응용 프로그램과 같은 간단한 "메모장"을 만들었습니다.내 목표는 열기, 저장, 새로 만들기 등의 고전적인 창 동작을 수행하고 제목 표시줄에 저장된 파일의 이름을 반영하는 무언가를 만드는 것입니다.또한 저장되지 않은 변경 사항이 있는 경우 제목 표시줄에 *가 포함되어야 합니다.

그래서 저는 애플리케이션의 지속성 상태를 관리하는 뷰와 프리젠터를 만들었습니다.제가 고려한 한 가지 개선 사항은 텍스트 처리 코드를 분리하여 보기/발표자가 진정한 단일 목적 엔터티가 되도록 하는 것입니다.

참고용 스크린샷입니다...

alt text

아래에 관련 파일을 모두 포함하고 있습니다.내가 올바른 방법으로 했는지, 개선할 수 있는 방법이 있는지에 대한 피드백에 관심이 있습니다.

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();
        }
    }
}
도움이 되었습니까?

해결책

완벽한 MVP 수동 뷰 패턴에 더 가까이 다가 갈 수있는 유일한 방법은 Winforms 대화 상자를 사용하는 대신 대화 상자에 대해 자신의 MVP 트라이어드를 작성하는 것입니다. 그런 다음 대화 상자 생성 로직을보기에서 발표자로 이동할 수 있습니다.

이것은 MVP 트라이어드 간의 의사 소통 주제에 들어갑니다. 내가 찾은 것은 나를 위해 일하는 것이 발표자에게 트라이어드를 연결하는 것입니다.

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;

        ...

그만큼 Show() 물론 방법은 언급되지 않은 것을 보여줄 책임이 있습니다. OpenDialogView, 사용자가 입력하고 그것을 따라 전달할 것입니다. OpenDialogPresenter. 어쨌든 발표자가 정교한 중개인이라는 것이 분명해지기 시작해야합니다. 다른 상황에서, 당신은 중개인을 재현하려는 유혹을받을 수 있지만 여기서는 다음과 같습니다.

  • 테스트하기가 더 어려운 시야에서 논리를 유지하십시오.
  • 보기와 모델 사이의 직접적인 종속성을 피하십시오

때로는 MVP 트라이어드 커뮤니케이션에 사용되는 모델도 보았습니다. 이것의 이점은 발표자가 서로 존재한다는 것을 알 필요가 없다는 것입니다. 일반적으로 모델에 상태를 설정하여 이벤트를 트리거하고 다른 발표자가 듣는 이벤트를 트리거합니다. 흥미로운 아이디어. 나는 개인적으로 사용하지 않았다.

다음은 다른 사람들이 트라이어드 커뮤니케이션을 다루는 데 사용한 기술 중 일부와 몇 가지 링크입니다.

다른 팁

모든 것이 좋아 보인다. 더 나아갈 수있는 유일한 수준은 파일을 저장하기위한 논리를 추상화하고 제공 업체가 처리하는 것이 나중에 데이터베이스, 이메일, 클라우드 스토리지와 같은 대체 저장 방법을 쉽게 구부릴 수 있도록하는 것입니다.

IMO는 파일 시스템을 만질 때마다 파일 시스템을 다루는 것이 항상 레벨을 추상화하는 것이 좋습니다. 또한 조롱과 테스트를 쉽게 할 수 있습니다.

제가 좋아하는 것 중 하나는 View에서 Presenter로의 직접적인 통신을 제거하는 것입니다.그 이유는 뷰가 UI 수준에 있고 프리젠터가 비즈니스 계층에 있기 때문입니다.나는 내 계층이 서로에 대해 고유한 지식을 갖는 것을 좋아하지 않으며 직접적인 의사소통을 최대한 제한하려고 노력합니다.일반적으로 내 모델은 레이어를 초월하는 유일한 모델입니다.따라서 프리젠터는 인터페이스를 통해 뷰를 조작하지만 뷰는 프리젠터에 대해 직접적인 조치를 취하지 않습니다.나는 발표자가 반응을 기반으로 내 견해를 듣고 조작할 수 있다는 점을 좋아하지만, 발표자에 대해 내 견해가 갖는 지식을 제한하는 것도 좋아합니다.

IPersistenceStateView에 몇 가지 이벤트를 추가하겠습니다.

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

그런 다음 발표자가 해당 이벤트를 듣도록 합니다.

public PersistenceStatePresenter(IPersistenceStateView view)
{
    _view = view;

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

   InitializeModel();
   InitializeView();
}

그런 다음 버튼 클릭이 이벤트를 실행하도록 뷰 구현을 변경합니다.

이는 발표자가 뷰에 반응하고 그 줄을 당기는 꼭두각시 조련사처럼 행동하게 만듭니다.거기에서 발표자의 메서드에 대한 직접 호출을 제거합니다.여전히 뷰에서 프리젠터를 인스턴스화해야 하지만 이것이 직접 수행할 유일한 작업입니다.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top