Критика моего простого приложения MVP Winforms [закрыто]
-
07-07-2019 - |
Вопрос
Я пытаюсь осмыслить шаблон MVP, используемый в приложении C#/Winforms.Поэтому я создал простое приложение, похожее на «блокнот», чтобы попытаться проработать все детали.Моя цель — создать что-то, что выполняло бы классическое поведение Windows: открытие, сохранение, создание нового, а также отражало бы имя сохраненного файла в строке заголовка.Кроме того, если есть несохраненные изменения, строка заголовка должна содержать *.
Поэтому я создал представление и презентатор, которые управляют состоянием персистентности приложения.Одним из улучшений, которые я рассмотрел, является изменение кода обработки текста, чтобы представление/представитель действительно было единым целевым объектом.
Вот скриншот для справки...
Я включаю все соответствующие файлы ниже.Меня интересуют отзывы о том, правильно ли я это сделал или есть способы улучшить.
ПримечаниеМодель.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
}
Форма2.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();
}
Персистенсстатепресентер.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 — это написать свои собственные триады MVP для диалогов вместо использования диалогов WinForms.Затем вы можете переместить логику создания диалога из представления в презентатор.
Здесь мы переходим к теме взаимодействия между триадами 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.Причина этого в том, что представление находится на уровне пользовательского интерфейса, а ведущий — на бизнес-уровне.Мне не нравится, когда мои слои обладают врожденными знаниями друг о друге, и я стараюсь максимально ограничить прямое общение.Обычно моя модель — единственное, что выходит за рамки слоев.Таким образом, презентатор манипулирует представлением через интерфейс, но представление не предпринимает особых прямых действий против презентатора.Мне нравится, когда ведущий может слушать и манипулировать моим мнением на основе реакции, но мне также нравится ограничивать знания моего представления о его ведущем.
Я бы добавил несколько событий в свой 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(); }
Затем измените реализацию представления, чтобы нажатие кнопки запускало события.
Это заставляет ведущего действовать скорее как кукловод, реагируя на вид и дергая его за ниточки;при этом удаляются прямые вызовы методов презентатора.Вам все равно придется создать экземпляр презентатора в представлении, но это, пожалуй, единственная непосредственная работа, которую вы с ним проделаете.