내 간단한 MVP Winforms 앱에 대한 비판 [닫기]
-
07-07-2019 - |
문제
C#/Winforms 앱에 사용되는 MVP 패턴에 대해 생각해 보려고 합니다.그래서 모든 세부 사항을 처리하기 위해 응용 프로그램과 같은 간단한 "메모장"을 만들었습니다.내 목표는 열기, 저장, 새로 만들기 등의 고전적인 창 동작을 수행하고 제목 표시줄에 저장된 파일의 이름을 반영하는 무언가를 만드는 것입니다.또한 저장되지 않은 변경 사항이 있는 경우 제목 표시줄에 *가 포함되어야 합니다.
그래서 저는 애플리케이션의 지속성 상태를 관리하는 뷰와 프리젠터를 만들었습니다.제가 고려한 한 가지 개선 사항은 텍스트 처리 코드를 분리하여 보기/발표자가 진정한 단일 목적 엔터티가 되도록 하는 것입니다.
참고용 스크린샷입니다...
아래에 관련 파일을 모두 포함하고 있습니다.내가 올바른 방법으로 했는지, 개선할 수 있는 방법이 있는지에 대한 피드백에 관심이 있습니다.
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(); }
그런 다음 버튼 클릭이 이벤트를 실행하도록 뷰 구현을 변경합니다.
이는 발표자가 뷰에 반응하고 그 줄을 당기는 꼭두각시 조련사처럼 행동하게 만듭니다.거기에서 발표자의 메서드에 대한 직접 호출을 제거합니다.여전히 뷰에서 프리젠터를 인스턴스화해야 하지만 이것이 직접 수행할 유일한 작업입니다.