суббота, 21 марта 2009 г.

Silverlight: MVVM

Привет, друзья!

Вчера, в пятницу 20-го марта, нам вместе с Сергеем Лутаем и Сашей Кондуфоровым посчастливилось выступать перед харьковским сообществом .NET программистов. Саша повел доклад за ASP.NET MVC, мы же с Сергеем, пролили немножко света в мир сильверлайта :). Спустя пару дней, выкладываем в онлайн то, что было в оффлайне :). В этой статье, я пишу про MVC в мире WPF/Silverlight (ака MVVM), а у Сережи вы найдете рассказ об архитектуре самой технологии.

MVVM


Evolution
Откуда берутся паттерны? Что делает одни шаблоны проектирования популярными а другие канут в Лету? Почему Лета пишется с большой буквы? Ой. Куда-то занесло не туда :).

Не берусь полностью ответить на все вопросы, но, есть подозрение, что популярным паттерн делает та же сила, которая дала мне возможность писать этот текст, а вам читать его - эволюция.

Чарльз Дарвин сказал:
"Из всех видов выживают не самые сильные, и даже не самые умные, а лишь те, кто лучше других приспосабливаются к изменениям".
Когда думаешь о его словах в контексте проектирования, первым приходит в голову MVC. Подумать только, этот паттерн шагает в ногу с разработчиками вот уже 31-й год! Сколько же технологий повидал этот долгожитель?

MVC/MVP


Нет-нет, я не планирую пускаться в пространные рассуждения обо всем и сразу. Дайте руку, дорогой читатель, и мы лишь помчимся по самым верхам со скоростью звука (т.е. со скоростью проговаривания текста про себя :)).

MVC: простейший способ понять MVC: Model == Данные, View == Показанные данные, Controller == Связь между Данными и Показанными данными. Если придумаете проще - дайте знать :).

MVP:
1. Most Valuable Professional. Не наш случай :).
2. Model View Presenter. Model и View остаются без изменений, Controller превращается в Presenter'a. Паттерн неплохо приспосабливается в мире WinForms и ASP.NET'a.


Здесь View изображен в виде интерфейса. Слой представления в приложении (будь то ASP.NET, WinForms или еще что-то), обязуется реализовать этот интерфейс. Presenter же общается с конкретной имплементацией View через интерфейс, ничего не подозревая о самой имплементации.

Прекрасный пример реализации MVP есть на codeproject.com. Слямзил оттуда лишь кусочек кода, для пущей наглядности. Легче всего разбираться с новым кодом при помощи... юнит тестов:
[TestFixture]
public class CurrentTimePresenterTests
{
 [Test]
 public void TestInitView()
 {
  MockCurrentTimeView view = new MockCurrentTimeView();
  CurrentTimePresenter presenter = new CurrentTimePresenter(view);
  presenter.InitView();

  Assert.IsTrue(view.CurrentTime > DateTime.MinValue);
 }

 private class MockCurrentTimeView : ICurrentTimeView
 {
  public DateTime CurrentTime
  {
   set { currentTime = value; }

   // This getter won't be required by ICurrentTimeView,
   // but it allows us to unit test its value.
   get { return currentTime; }
  }

  private DateTime currentTime = DateTime.MinValue;
 }
}

Как видно, конструктор Presenter'a получает экземпляр мока в качестве View, и после вызова метода Init() ожидает от мока определенного состояния. Интерфейс ICurrentTimeView и класс Presenter'a выглядят следующим образом:
public interface ICurrentTimeView
{
 DateTime CurrentTime { set; }
}

public class CurrentTimePresenter
{
 public CurrentTimePresenter(ICurrentTimeView view)
 {
  if (view == null)
   throw new ArgumentNullException("view may not be null");

  this.view = view;
 }

 public void InitView()
 {
  view.CurrentTime = DateTime.Now;
 }

 private ICurrentTimeView view;
}
Теперь мы можем легко сменить View, например на ASP.NET старницу. На страницу поместим Label с именем lblCurrentTime, и:
public partial class ShowMeTheTime : Page, ICurrentTimeView
{
 protected void Page_Load(object sender, EventArgs e)
 {
  CurrentTimePresenter presenter = new CurrentTimePresenter(this);
  presenter.InitView();
 }

 public DateTime CurrentTime
 {
  set { lblCurrentTime.Text = value.ToString(); }
 }
}
Что же мы получили? - Отличное покрытие тестами, четкое разграничение обязанностей, моральное удовлетворение :).

Model-View-ViewModel (aka MVVM)


MVP - может прекрасно жить и в мире WPF/Silverlight, но благодаря отличительным особенностям технологий, жизнь программиста может быть еще проще. Большую часть забот по связыванию данных с их отображением берут на себя Binding-средства. Обо всей мощи Binding'a в WPF/Silverlight'e я собираюсь написать в ближайшем будущем, пока же достаточно будет показать маленький пример.

Как вы знаете, структура приложения, написанного на WPF/SL определяется иерархической хмл-разметкой:
<UserControl.Resources>
    <vm:User x:Key="UserInfo"/>
</UserControl.Resources>
<Grid DataContext="{StaticResource UserInfo}">
    <Border>
        <TextBox Text="{Binding Name}"/>
    </Border>
</Grid>
Здесь мы создали один Grid, положили внутрь него Border а внутрь последнего положили TextBox. Наибольший интерес для нас представляет свойство DataContext (которое Grid получил в наследство от FrameworkElement'a), и выражение Binding. Как только мы установили DataContext, все потомки Grid'a имеют к нему доступ, вне зависимости от уровня вложенности. Запись Text="{Binding Name}" означает: возьми то, что находится в DataContext'e, найди у него свойство Name, и сохрани значение этого свойства в Text. Более того, механизм Binding'a будет наблюдать за изменениями как источника данных так и потребителя. Как только изменится первый - второй будет обновлен. И наоборот: если пользователь введет данные в TextBox это вызовет обновления свойства Name, у объекта лежащего в DataContext'e (а в нашем случае это объект класса User).

Теперь, имея представление о связывании данных и представления в мире WPF/SL, давайте посмотрим на MVC.

Все, что нужно View, для отображения данных - это правильный "DataContext" и правильные свойства в этом DataContext'e. Т.е. нужен посредник, который возьмет только необходимые для View данные из модели, и сделает их доступными через открытые свойства (ака properties). Этот посредник и получил название ViewModel. ViewModel - это адаптер модели для View. Кто не верит, что это адаптер, взгляните на UML определение адаптера :) :
Ну хорошо, а как быть, если нам нужно отреагировать на нажатие кнопки? В мире WPF'a широко используется интерфейс ICommand, для обработки подобных действий. Достаточно выставить у ViewModel'a наружу свойство типа ICommand, и привязать его к свойству Command у Button'a. Когда будет нажата кнопка произойдет вызов метода Execute(), где вы можете обновить состояние модели или ViewModel'a. Кстати, реализовывать свой ICommand, обычно не приходится. В стандартной поставке доступны несколько готовых реализаций, но в самом ViewModel'e оказывается очень удобным использовать DelegateCommand.

Сильверлайт так же имеет интерфейс ICommand, но на этом его поддержа коммандной модели заканчивается ("Silverlight 2 does not implement a commanding system. This interface is in place only for interoperability and compatibility reasons." (c) MSDN). Что же, не беда. Всегда можно воспользоваться обработчиком события Click, и вызвать соответствующий метод у ViewModel'a.

Silver Dictionary


Пока мы готовились к презентации, мы написали небольшое Silverlight-приложение в MVVM стиле:

Суть приложения: переводчик. Выбираем с какого языка будем переводить на какой, и, через Google, осуществляем перевод. По исходному и по переведенному словам осуществляем поиск картинок (пользуясь сервисами Google).

Исходные коды к проекту всегда доступны на Google Code. Загружайте/модифицируйте/изучайте код в свое удовольствие. А здесь же, мы пока пройдемся по самым основам приложения:



В папке Model мы определяем интерфейс взаимодействия с сервисами Google (переводчик и поисковик картинок). Интерфейсы модели имеют простой вид: есть методы которые стартуют поиск картинок/перевод, и есть события которые вызываются по завершению поиска/перевода.

В папке ViewModel собраны классы отвечающие за преобразование модели к виду, удобному для View. Например, SilverDictionaryViewModel содержит открытое свойство Languages, к которому привязываются списки с исходным и целевым языками перевода.

Ну и наконец, xaml-файлы содержат разметку приложения и Binding выражения. DataContext мы устанавливаем при событии Page.Loaded:
void Page_Loaded(object sender, RoutedEventArgs e)
{
 _silverDictionaryViewModel.Init();
 DataContext = _silverDictionaryViewModel;
}
А вот как выглядит обработчик нажатия кнопки Translate:
Xaml:
<Button Content="Translate"
 Margin="5, 0, 5, 0"
 Grid.Column="0"
 Click="TranslateButtonClick"
 IsEnabled="{Binding IsValid}"/>
Code Behind:
private void TranslateButtonClick(object sender, RoutedEventArgs e)
{
 DoTranslate();
}
private void DoTranslate()
{
 _silverDictionaryViewModel.Translate();
}

Конец... Или только начало?


Ну что же, дорогой читатель, на этом, наше путешествие по паттернам-приспособленцам подходит к концу. Спасибо, что вы дочитали до самого конца. Надеюсь, вам было интересно :). До скорых встреч и отличного программирования!

ЗЫЖ Лета - это река забвения, в древнегреческой мифологии. Потому и пишут с большой буквы.

2 комментария:

  1. Nu 4to skazati - interesno, no etot golovokrujiteliniy effect ya uje davno ne ispolizuiu potomu4to on o4eni stariy - v Linuxe etot effect bil dobavlen davneniko.. no sylverlight podu4iti dumaiu stoit hotea interesno - po skorosti - on bistree flash`a ?

    ОтветитьУдалить
  2. Привет, Rodislav!

    Вопрос Flash vs Silverlight - относится к разряду холиварных, и ответ на него будет зависеть от того, к какому "лагерю" вы принадлежите :). Чтобы решить для себя, что лучше, можно посмотреть сайт http://www.shinedraw.com/ - парень делает просто потрясающие сравнения технологий, а пользователи говорят, что получилось лучше.

    ОтветитьУдалить