Professional Documents
Culture Documents
Лабораторная работа №25 Шаблон MVP
Лабораторная работа №25 Шаблон MVP
1
через интерфейс Вида и через него же передаёт в Вид данные из Модели и другие результаты
своей работы.
Основное отличие MVP и MVC в том, что в MVC обновлённая модель сама говорит
виду, что нужно показать другие данные. Если же этого не происходит и приложению нужен
посредник в виде представителя, то паттерн стоит называть MVP.
Это достаточно отстраненные определения, и чтобы лучше в них разобраться рассмотрим
простое приложение DemoMVP, которое будет производить эмуляцию перевода денежных
средств с одного счета на другой. Здесь используется технология WPF, которая сейчас стала более
популярна, чем WinForms, однако, благодаря четкому разделению обязанностей приложение
очень легко портируется и под WinForms.
На рисунке ниже представлена диаграмма основных классов:
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.
Внешний вид окна показан на рисунке:
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, в который передается созданный ранее
экземпляр окна (нашего представления).
Теперь наше приложение готово к компиляции и запуску.
Ход работы
Контрольные вопросы