You are on page 1of 7

ЛАБОРАТОРНАЯ РАБОТА № 25

Тема: Создание программной системы, использующей шаблон MVP.

Цель: научиться использовать шаблон проектирования MVP для построения


пользовательского интерфейса.

Краткий теоретический блок

Шаблон MVP (Model-View-Presenter) – это архитектурный шаблон проектирования,


производный от MVC, который используется в основном для проектирования пользовательского
интерфейса. Его назначение – отделение пользовательского интерфейса от данных приложения и
методов их обработки (бизнес-логики). Это достигается путем введения дополнительного объекта
- презентора.
Взаимосвязь составных частей системы представлена на схеме:

Элемент Presenter в данном шаблоне берёт на себя функциональность посредника


(аналогично контроллеру в MVC) и отвечает за управление событиями пользовательского
интерфейса (например, использование мыши) так же, как в других шаблонах обычно отвечает
представление.
MVP — шаблон проектирования пользовательского интерфейса, который был разработан
для облегчения автоматического модульного тестирования и улучшения разделения
ответственности в презентационной логике (отделения логики от отображения):
Model (модель) — это набор данных и методов их обработки, т.е. это бизнес-логика
нашего приложения. Модель ничего не знает о существовании презентора и, тем более,
представления. Она полностью независима;
View (вид, представление) — отвечает за визуализацию данных, которые получены от
модели; обращается к Presenter за обновлениями, перенаправляет события от пользователя в
Presenter. Как, правило, модель только отображает данные, но в некоторых случаях возможно
включение и простых алгоритмов расчета. Например, представление самостоятельно может
подсчитать общее количество записей в таблице или итоговую сумму;
Presenter (представитель) — связующее звено между моделью и представлением. Он
ответственен за обработку событий, возникающих в представлении, которые обычно
инициированы пользователем, и в зависимости от типа события изменять состояние модели путем
вызова ее публичных методов, после чего обновлять представление в соответствии с состоянием
модели. Один Presenter отвечает за взаимодействие с одним представлением.
Обычно экземпляр Вида (Представление) создаёт экземпляр Представителя, передавая
ему ссылку на себя. При этом Представитель работает с Видом в абстрактном виде, через
его интерфейс. Когда вызывается событие Представления, оно вызывает конкретный метод
Представителя, не имеющего ни параметров, ни возвращаемого значения. Представитель
получает необходимые для работы метода данные о состоянии пользовательского интерфейса

1
через интерфейс Вида и через него же передаёт в Вид данные из Модели и другие результаты
своей работы.
Основное отличие MVP и MVC в том, что в MVC обновлённая модель сама говорит
виду, что нужно показать другие данные. Если же этого не происходит и приложению нужен
посредник в виде представителя, то паттерн стоит называть MVP.
Это достаточно отстраненные определения, и чтобы лучше в них разобраться рассмотрим
простое приложение DemoMVP, которое будет производить эмуляцию перевода денежных
средств с одного счета на другой. Здесь используется технология WPF, которая сейчас стала более
популярна, чем WinForms, однако, благодаря четкому разделению обязанностей приложение
очень легко портируется и под WinForms.
На рисунке ниже представлена диаграмма основных классов:

Рассмотрим интерфейс модели IBankModel. Выделим специальный интерфейс для


модели, чтобы не привязывать ссылающиеся на нее классы (наш презентор) к конкретной
реализации. В данном примере модель –это класс SimpleBankModel, который реализует
интерфейс IBankModel. Создание прослойки в виде интерфейса позволит в дальнейшем эту
простую реализацию заменить на модель, которая работает с БД и сохраняет свое состояние, без
изменения использующих ее классов. Реализация SimpleBankModel выглядит след. образом:
namespace DemoMVP.DomainModel
{ public class SimpleBankModel : IBankModel
{ private IList<Account> accounts;
public SimpleBankModel(IList<Account> accounts)

2
{ this.accounts = accounts; }
/// <summary> Снять деньги со счета </summary>
/// <param name="accountId">Идентификатор счета</param>
/// <param name="sum">сумма</param>
/// <param name="transaction">объект транзакции</param>
/// <returns>True, если успешно. False - если нет такой суммы на
балансе</returns>
public bool Withdraw(string accountId, decimal sum, ITransaction
transaction)
{ EnsureSum(sum);
Account account = GetAccountById(accountId);
if (account.Balance < sum) { return false; }
(transaction as Transaction).SaveState(account);
account.Balance -= sum; return true; }
/// <summary> Внести деньги на счет </summary>
/// <param name="accountId">Идентификатор счета</param>
/// <param name="sum">сумма</param>
/// <param name="transaction">объект транзакции</param>
public void Deposit(string accountId, decimal sum, ITransaction
transaction)
{ EnsureSum(sum);
Account account = GetAccountById(accountId);
(transaction as Transaction).SaveState(account);
account.Balance += sum; }
/// <summary> Получить баланс счета </summary>
/// <param name="accountId">Идентификатор счета</param>
/// <returns>Сумма денег на счету</returns>
public decimal GetBalance(string accountId)
{ Account account = GetAccountById(accountId); return account.Balance; }
/// <summary> Создать объект транзакции </summary>
/// <returns></returns>
public ITransaction CreateTransaction()
{ return new Transaction(); }
private void EnsureSum(decimal sum)
{ if (sum <= 0)
{ throw new ApplicationException("Sum must be greater than 0"); }
}
private Account GetAccountById(string accountId)
{ Account account = accounts.Where(x => x.Id ==
accountId).FirstOrDefault();
if (account == null)
{ throw new ApplicationException("Account not found"); }
return account; }
}
}
Т.е. реализация достаточно тривиальна. Стоит обратить внимание на объект transaction. В
данном случае он ничего не делает, но призван показать, что при совершении операций система
сохраняет свое состояние, чтобы в случае каких-либо проблем смогла восстановить свое исходное
состояние. Методов Withdraw (снять) и Deposit (зачислить) и объекта транзакции достаточно для
того, чтобы обеспечить надежную систему по переводу денежных средств.
Представлением в нашем случае является класс MainWindow, который реализует
интерфейс ITransferView. Этот интерфейс скрывает подробности реализации представления и
обеспечивает только несколько методов и событий для взаимодействия с представлением. Код
данного интерфейса показан ниже:
namespace DemoMVP.Abstract
{ public interface ITransferView
{ /// <summary> Обновить баланс счета-источника </summary>
/// <param name="balance">Сумма на балансе</param>
void UpdateSrcBalance(decimal balance);
/// <summary> Обновить баланс счетаназначения </summary>
/// <param name="balance">Сумма на балансе</param>

3
void UpdateDestBalance(decimal balance);
/// <summary> Отобразить предупреждение </summary>
/// <param name="text">Текст сообщения</param>
void ShowWarning(string text);
/// <summary> Отобразить ошибку </summary>
/// <param name="text">Текст ошибки</param>
void ShowError(string text);
/// <summary> Событие, срабатывающее при изменении идентификатора счета-источника
/// </summary>
event EventHandler<AccountChangedEventArgs> SrcAccountChanged;
/// <summary> /// Событие, срабатывающее при изменении идентификатора счета-
назначения
/// </summary>
event EventHandler<AccountChangedEventArgs> DestAccountChanged;
/// <summary> Событие, срабатывающее при запросе перевода денег </summary>
event EventHandler<TransferMoneyEventArgs> TransferMoney;
}
}
Обратите внимание, что ITransferView не содержит никаких ссылок на элементы
управления, характерные для WPF. Это важный момент! Поэтому класс, который будет содержать
ссылку на данный интерфейс, не сможет получить доступ к элементам управления. Он сможет
обращаться к представлению только через четко обозначенный нами интерфейс. Таким образом и
достигается независимость представления от других частей программы. Поэтому, чтобы сменить
интерфейс, нам достаточно будет переписать только класс MainWindow.
Внешний вид окна показан на рисунке:

Теперь давайте взглянем, на код MainWindow и посмотрим, каким образом достигается


реализация интерфейса ITransferView.
namespace DemoMVP
{ /// <summary> Логика взаимодействия для MainWindow.xaml </summary>
public partial class MainWindow : Window, ITransferView
{ public MainWindow()
{ InitializeComponent(); }
private string srcAccountId = string.Empty;
private string destAccountId = string.Empty;
public void UpdateSrcBalance(decimal balance)
{ lblSrcBalance.Content = balance.ToString(); }
public void UpdateDestBalance(decimal balance)
{ lblDestBalance.Content = balance.ToString(); }
public void ShowWarning(string text)
{ MessageBox.Show(text, "Предупреждение", MessageBoxButton.OK,
MessageBoxImage.Warning); }
public void ShowError(string text)
{ MessageBox.Show(text, "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error);}
public event EventHandler<TransferMoneyEventArgs> TransferMoney;
protected virtual void OnTransferMoney(TransferMoneyEventArgs e)

4
{ if (TransferMoney != null)
{ TransferMoney(this, e); } }
public event EventHandler<AccountChangedEventArgs> SrcAccountChanged;
protected virtual void OnSrcAccountChanged(AccountChangedEventArgs e)
{ if (SrcAccountChanged != null)
{ SrcAccountChanged(this, e); }
}
public event EventHandler<AccountChangedEventArgs>DestAccountChanged;
protected virtual void OnDestAccountChanged(AccountChangedEventArgs e)
{ if (DestAccountChanged != null)
{ DestAccountChanged(this, e); }
}
private void txtSrcAccount_LostFocus(object sender, RoutedEventArgs e)
{ if (!string.IsNullOrEmpty(txtSrcAccount.Text) && txtSrcAccount.Text !=
srcAccountId)
{ OnSrcAccountChanged(new AccountChangedEventArgs(txtSrcAccount.Text));
srcAccountId = txtSrcAccount.Text; }
}
private void txtDestAccount_LostFocus(object sender, RoutedEventArgs e)
{ if (!string.IsNullOrEmpty(txtDestAccount.Text) && txtDestAccount.Text !=
destAccountId)
{ OnDestAccountChanged(new AccountChangedEventArgs(txtDestAccount.Text));
destAccountId = txtDestAccount.Text; }
}
private void btnTransfer_Click(object sender, RoutedEventArgs e)
{ decimal sum;
if (decimal.TryParse(txtSum.Text, out sum))
{ OnTransferMoney(new TransferMoneyEventArgs(txtSrcAccount.Text,
txtDestAccount.Text, sum)); }
else { ShowWarning("Ошибка ввода суммы"); }
}
}
}
Здесь мы видим, каким образом реализация методов по обновлению баланса
взаимодействует с существующими элементами управления WPF, а также, каким образом
достигается отображение предупреждений и ошибок. Благодаря тому, что данный класс реализует
интерфейс ITransferView, мы можем всего лишь в одном месте изменить реализацию
методов ShowWarning и ShowError таким образом, чтобы отображать сообщения не окошками, а
выводить их, например, в Label где-то внизу окна. Что касается срабатывания событий, то здесь
мы, по сути, просто пробрасываем события от элементов управления WPF в подходящие
события ITransferView, предварительно поместив всю необходимую информацию из элементов
управления формы в экземпляр соответствующего класса. В обработчиках событий
txtSrcAccount_LostFocus и txtDestAccount_LostFocus мы реализовали логику проверки изменения
номера счета. И события срабатывают только в том случае, если пользователь указал новый счет.
Обработчик btnTransfer_Click дополнительно выполняет преобразование введенную строку в
число и в случае, если обнаружена ошибка, то информирует об этом.
Осталось рассмотреть последний элемент – класс презентора, который служит
связующим звеном между представлением и моделью. Презентор подписан на события
интерфейса представления ITransferView, в соответствии с полученными данными изменяет
состояние модели через вызовы метода интерфейса IBankModel, а затем обновляет представление.
Ниже показан код презентора.
namespace DemoMVP.Presenter
{ public class TransferPresenter
{ private IBankModel model;
private ITransferView view;
public TransferPresenter(IBankModel model, ITransferView view)
{ this.model = model;
this.view = view;
this.view.SrcAccountChanged += view_SrcAccountChanged;
this.view.DestAccountChanged += view_DestAccountChanged;

5
this.view.TransferMoney += view_TransferMoney; }
private void view_SrcAccountChanged(object sender, AccountChangedEventArgs e)
{ try
{ decimal balance = model.GetBalance(e.AccountId);
view.UpdateSrcBalance(balance); }
catch(ApplicationException ex)
{ view.ShowError(ex.Message); }
}
private void view_DestAccountChanged(object sender, AccountChangedEventArgs e)
{ try
{ decimal balance = model.GetBalance(e.AccountId);
view.UpdateDestBalance(balance); }
catch (ApplicationException ex)
{ view.ShowError(ex.Message); }
}
private void view_TransferMoney(object sender, TransferMoneyEventArgs e)
{ ITransaction tran = model.CreateTransaction();
try
{ tran.Begin();
bool success = model.Withdraw(e.SrcAccountId, e.Sum, tran);
if (success)
{ model.Deposit(e.DestAccountId, e.Sum, tran);
tran.Commit();
view.UpdateSrcBalance(model.GetBalance(e.SrcAccountId));
view.UpdateDestBalance(model.GetBalance(e.DestAccountId));
}
else
{ tran.Rollback();
view.ShowWarning("Недостаточно денег для перевода");
}
}
catch(ApplicationException ex)
{ tran.Rollback(); view.ShowError(ex.Message); }
}
}
}
Презентору в конструкторе передаются ссылки на интерфейс представления и интерфейс
модели, после чего происходит подключение обработчиков событий представления. Презентор
отвечает также за обработку ошибок, которые могут возникнуть при вызове методов модели.
Чтобы не загромождать приложение, я просто сообщение об исключении передаю
представлению, однако, в реальности, возможно, это не самая лучшая идея раскрывать
подробности исключения пользователю, хотя иногда оправдан и такой подход. В обработчике
события view_TransferMoney показан пример работы с транзакцией-заглушкой при вызове
методов в модели. В случае успеха перевода денег происходит подтверждение транзакции, а в
случае ошибок - ее откат.
Мы рассмотрели все составляющие паттерна MVP. Теперь осталось нам создать
экземпляры всех наших классов и запустить приложение. По умолчанию приложение WPF
создает частичный класс приложения App и помещает точку входа - метод Main в автоматически
генерируемый файл App.g.cs. Чтобы самому реализовать метод Main, необходимо открыть
свойства файла App.xaml и параметр "Действие при построении" изменить с
"ApplicationDefinition" на "Page". Далее, нужно открыть содержимое файла App.xaml и удалить
атрибут StartupUri="MainWindow.xaml". Теперь добавим в содержимое файла App.xaml.cs
следующие строки:
namespace DemoMVP
{ /// <summary> Логика взаимодействия для App.xaml </summary>
public partial class App : Application
{ App()
{ InitializeComponent(); }
[STAThread]
static void Main()

6
{ IBankModel model = new SimpleBankModel(new List<Account>()
{ new Account() { Id = "1", Balance = 5000 },
new Account() { Id = "2", Balance = 2000 },
new Account() { Id = "3", Balance = 500 },
new Account() { Id = "4", Balance = 5500 }
});
MainWindow view = new MainWindow();
TransferPresenter presenter = new TransferPresenter(model, view);
App app = new App();
app.Run(view);
}
}
}
Здесь мы создаем уже конкретный экземпляр модели и наполняем ее некоторыми
объектами счетов. Затем создаем экземпляр представления и презентора, которому в конструктор
и передаем нашу модель и представление. Далее идет стандартный код WPF по созданию
экземпляра класса приложения и вызов метода Run, в который передается созданный ранее
экземпляр окна (нашего представления).
Теперь наше приложение готово к компиляции и запуску.

Ход работы

Задание 1: Продемонстрируйте технологию разделения сложной системы на модули


согласно архитектурной идеи MVP на примере, рассмотренном выше.

Задание 2: Продемонстрируйте технологию использования шаблона MVP на примере


своего индивидуального задания.

Задание 3: Продолжите разработку действующего прототипа программного средства,


дополняя его функционалом разработанных модулей. В отчете представьте скриншоты
обновлённых форм интерфейса, продемонстрируйте реализованный функционал с результатами
функционального (модульного) тестирования, а также поместите программный код
разработанных модулей с подробными описаниями.

Контрольные вопросы

1. Поясните понятие и назначение разделения сложной системы на модули согласно


архитектурной идеи MVP, опишите этапы проектирования ПС в соответствии с данной идеей.
2. Опишите преимущества использования принципов MVC при разработке ПС.
3. Опишите отличия шаблонов MVP и MVС.

You might also like