воскресенье, 29 марта 2009 г.

Behaviors: Как это работает?

В прошлом посте я рассказал о том, что такое поведения, и показал простенький пример, реализующий поведение "перетягивабельности" UIElement'a. Здесь же мы посмотрим, что заставляет поведения работать. Для того, чтобы вы комфортно себя чувствовали при чтении поста, от вас, дорогой читатель, ожидается понимание модели свойств, представленной в WPF/SL: Attached Properties и Dependency Properties. Если же для вас эти названия в новинку - не отчаивайтесь, в скором времени мы пройдемся и по этим товарищам, разметая туман загадочности метлой желания и разума :).

Поехали!


Давайте посмотрим, на пример поведения, из предыдущего сообщения:

<Rectangle Fill="Green" Width="40" Height="40" x:Name="rct">
 <i:Interaction.Behaviors>
  <local:DragBehavior/>
 </i:Interaction.Behaviors>
</Rectangle>
Класс Interaction определяет присоединенное свойство (Attached Property) типа BehaviorCollection:

BehaviorsProperty = DependencyProperty.RegisterAttached(
 "Behaviors",
 typeof (BehaviorCollection),
 typeof (Interaction),
 new PropertyMetadata(new PropertyChangedCallback(Interaction.OnBehaviorsChanged))
 );    
Когда мы прикрепляем к некоторому объекту это свойство, в рамках нашего класса-сервиса Interaction, вызывается метод OnBehaviorsChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args). Первым параметром этого метода будет объект, к которому мы присоединили attached property, а во втором мы найдем список определенных поведений. (В нашем примере: obj == прямоугольник rct, а args.NewValue == коллекция, с одним элементом DragBehavior). Имея эти два козыря на руках, класс Interaction, в методе OnBehaviorsChanged() проходится по коллекции поведений, и у каждого из поведений вызывает метод Attach(), передавая объект, к которому был присоединен attached property:

//... Где-то в OnBehaviorsChanged()    
foreach (Behavior behavior in behaviorsCollection)
{
    behavior.Attach(obj); 
}
Дальше остается дело за малым: базовый класс Behavior устанавливает свое свойство AssociatedObject в переданный ему obj, и вызывает виртуальный метод OnAttached(), чтобы известить своих наследников о произошедшем соединении :). Именно этот метод мы использовали в примере из прошлого поста, чтобы подписаться на события от мышки.

public class DragBehavior : Behavior<UIElement>
{
 protected override void OnAttached()
 {
  AssociatedObject.MouseLeftButtonDown += AssociatedObject_MouseLeftButtonDown;
  AssociatedObject.MouseMove += AssociatedObject_MouseMove;
  AssociatedObject.MouseLeftButtonUp += AssociatedObject_MouseLeftButtonUp;
 }

 // ...
}

Последний штрих: мы не можем напрямую унаследоваться от  класса Behavior (у него internal конструктор). Вместо этого нам предлагают воспользоваться обобщенным классом Behavior<T>, где T - DependencyObject.

Почему на T накладывается ограничение - быть DependencyObject'ом понятно: attached property можно прикрепить только к DependencyObject'у. Но почему же мы не можем напрямую унаследоваться от Behavior? Зачем делать конструктор внутренним? - Для нашего же блага :).

Например, мы захотим написать поведение применимое только к панелям. Вместо того, чтобы ходить каждый раз после крэша в Graphic Design Department, и объяснять дизайнерам: "Пожалуйста, не перетаскивайте поведение с названием OnlyForPanelsBehavior, на кнопки - это сломает систему", мы просто наследуемся от Behavior<Panel>, и Blend не даст перетянуть поведение на кого-то, отличного от Panel'a :). Удобно, не так ли?

Что дальше?


Айда писать классные, потрясающие и просто захватывающие поведения :)! Поделиться своим произведением всегда можно на сайте Microsoft Expression Gallery.

К сожалению, мой обзор поведений остается неполным без Trigger'ов и Action'ов. В ближайшем будущем (на следующей неделе) я собираюсь избавиться от этого пробела :).

Отличного программирования, друзья!

среда, 25 марта 2009 г.

Behaviors + Blend = WOW!

Что такое Behavior?


На Mix 09 парни из Microsfot показывали 3-й Expression Blend, который уже можно скачать и установить (ставится на ура рядом со вторым блендом). В новом бленде появилась возможность создавать Behaviors - поведения. Сложно сказать конкретно, что такое "поведение", т.к. оно может быть чем угодно :). Поведения наделяют объект каким-либо свойством. Например, на презентации длиною в 18 минут, Pete Blois дает возможность кнопкам:
  • Соответствовать законам физики: при запуске приложения кнопки начинают падать.
  • Обладать свойством перетаскивабельности: можно потянуть кнопку мышкой. А т.к. кнопка еще и действует по законам физики, вы получите некоторое последействие, когда отпустите мышку. Кнопка поедет чуточку дальше :).
  • Обладать магнетизмом: кнопка начинает притягивать другие объекты :).
Поведения работают как в Silverlight 3.0, так и в WPF. В Preview версии Blend'a нет стандартных поведений:
но, на сайте Microsoft Expression Community Gallery, уже можно скачать те поведения, что использовал Pete в своей презентации :).

Как создать свое поведение?

Здесь приводится алгоритм создания своего поведения на примере DragBehavior'a, наделяющего объект свойством перетягивабельности :).
1. Для начала стоит поставить третий бленд :).
2. Создаем новый Silverlight Project (с равным успехом, можно создавать WPF проект, но пример мы сделаем на Silverlight'e).
3. В References нашего Silverlight приложения добавляем сборку "Microsoft.Expression.Interactivity.dll" (по умолчанию, она находится в папке "C:\Program Files\Microsoft Expression\Blend 3 Preview\Libraries") :
4. Теперь нам нужно определить новый класс поведения. Поскольку я ленивый, сделаю это прямо в файле с Code behind'ом для моего Entry Point'a в приложение. После редактирования, он выглядит так:
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Microsoft.Expression.Interactivity;

namespace SilverlightApplication1
{
 public partial class MainControl : UserControl
 {
  public MainControl()
  {
   // Required to initialize variables
   InitializeComponent();
  }
 }

 public class DragBehavior : Behavior<UIElement>
 {
  private bool _isDragging = false;
  private Point _prevPoint;

  protected override void OnAttached()
  {
   AssociatedObject.MouseLeftButtonDown += AssociatedObject_MouseLeftButtonDown;
   AssociatedObject.MouseMove += AssociatedObject_MouseMove;
   AssociatedObject.MouseLeftButtonUp += AssociatedObject_MouseLeftButtonUp;
  }

  void AssociatedObject_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
  {
   _isDragging = false;
   AssociatedObject.ReleaseMouseCapture();
  }

  void AssociatedObject_MouseMove(object sender, MouseEventArgs e)
  {
   if (!_isDragging)
   {
    return;
   }

   var currentPoint = e.GetPosition(null);
   var left = Canvas.GetLeft(AssociatedObject) +
              currentPoint.X - _prevPoint.X;
   var top = Canvas.GetTop(AssociatedObject) +
             currentPoint.Y - _prevPoint.Y;

   Canvas.SetLeft(AssociatedObject, left);
   Canvas.SetTop(AssociatedObject, top);

   _prevPoint = currentPoint;
  }

  void AssociatedObject_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
  {
   _isDragging = true;
   _prevPoint = e.GetPosition(null);

   AssociatedObject.CaptureMouse();
  }
 }
}
Как видите, новое поведение наследуется от Generic-класса "Behavior". В качестве параметра шаблона мы передаем UIElement, именно к такого типа элементам применимо наше поведение. Единственное ограничение на параметр шаблона: он обязательно должен быть DependencyObject'ом (Почему? - Читайте в следующем посте)
Мы также перекрыли вызов метода "OnAttached()", что произойдет, когда наше поведение будет присоединено к UIElement'у. Сам UIElement доступен через свойство "AssociatedObject". Имея в распоряжении полный доступ к элементу, мы подписываемся на его события мышки, и при зажатой левой кнопке меняем координаты Canvas.Left, Canvas.Top. Конечно, это накладывает ограничения: чтобы пример заработал, нужно положить UIElement в Canvas.
5. Скомпилируем проект, и пойдем в Asset Library. На вкладке Behaviors мы найдем наше поведение:
6. Теперь, имея следующую разметку нашего Root Visual'a:
<UserControl
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 x:Class="SilverlightApplication1.MainControl"
 Width="640" Height="480">

 <Grid x:Name="LayoutRoot" Background="White">
  <Canvas>
   <Rectangle Fill="Green" Width="40" Height="40"/>
  </Canvas>
 </Grid>
</UserControl>
Мы можем перетащить DragBehavior на Rectangle. После этого, наш Rectangle получает... присоединенное DependencyProperty: Interaction.Behaviors:
<Rectangle Fill="Green" Width="40" Height="40">
 <i:Interaction.Behaviors>
  <local:DragBehavior/>
 </i:Interaction.Behaviors>
</Rectangle>
7. Запустив приложение, мы сможем возькать мышкой прямоугольник.

Как работают поведения?

Об этом читайте в следующем посте :).

вторник, 24 марта 2009 г.

Хорошего работника узнают по инструменту

Пословица честно стибрена позаимствована из книги Брукса "МЧМ" :). Однако, чем больше работаешь с кодом, тем лучше понимаешь важность выбора хороших инструментов. Хочу поделиться инструментарием, который может пригодится в повседневной жизни WPF-разработчика, и которым активно пользуюсь сам. Если какой-либо инструмент был незаслуженно пропущен, пожалуйста, дайте знать. Я непременно обновлю пост и буду бесконечно благодарен :).

Visual Studio 2008


Не шучу. Многие ли из нас могут похвастаться доскональным знанием всех возможностей этого шедевра? Этот инструмент не раз удивлял меня. Предлагаю несколько находок, полезных в мире WPF/Silverlight:

1. Debug Designer. Как часто доводилось вам видеть следующую картину

Мне, увы, чаще, чем хотелось бы. Быстро найти проблему помогает... второй экземпляр студии. Запускаем еще одну студию. Присоединяемся к процессу первой студии (Tools -> Attach to Process...):


И жмем в первой студии (с обломавшимся дизайнером) кнопочку Reload Designer. Второй экземпляр студии быстро (ну или не очень - зависит от вашего бортового CPU :)) останавливается на ошибке:


Ага! В моем случае, причиной ошибки был {StaticResource ...}: он был определен в App.xaml, и во время выполнения приложение не падало . Изменив определение на {DynamicResource ...}, я получил работающий дизайнер и приложение (как и прежде):


Спасибо, студия. Кстати, этот же трюк сработает и с дизайнером Expression Blend: просто присоединитесь к процессу Blend'a.

2. Debug .NET Code. Иногда просто лень читать документацию, и хочется посмотреть как это сделано внутри. Reflector - первый инструмент который приходит на ум, но он, к сожалению, не показывает комментарии к исходникам (во всяком случае, пока :) ). Студия и здесь может оказаться полезной. Идем в настройки, и включаем Enable .NET Framework Source Stepping:


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

Например, мы хотим посмотреть, почему наш TextBox не получает фокус ввода при вызове txtName.Focus(). Устанавливаем бряк поинт на вызов, и жмем F11:


Пройдя последовательно по вызовам, мы можем прочитать все необходимые условия для передачи фокуса:


3. Visual Guidelines. Недавно выкачал исохдники хромого, и привлекло описание Visual Studio tricks (особенно Column Limit). Достаточно задать "скрытый" строковый параметр в реестре по пути
[HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\9.0\Text Editor]
"Guides"="RGB(128,0,0) 80"



И мы получаем от редактора полоску на 80-м символе (после рестарта студии):


Какой от этого может быть прок? Давайте отойдем на секунду от кода и подумаем вот о чем. Текст в книгах и журналах набирается по колонкам, ширина которых обычно меньше ширины самой страницы. Зачем это делается? Да затем, что узкая колонка сокращает охватываемое взглядом пространство при движении глаз вперед-назад - чтение становится проще, когда глаза меньше работают. Чтение упрощается, когда то, что мы читаем, и то, что собираемся прочесть, находится в поле нашего зрения. Именно эту мысль высказывают авторы 32-й главы "Код в развитии" книги Идеальный код, Лаура Уингерд и Кристофер Сейвалд.

Но у этого подхода есть еще один плюс: стараясь придерживаться правила 80-столбцов, мы можем просматривать два файла в студии бок-о-бок, не испытывая дискомфорта. А это бывает очень полезно, когда разрабатываешь приложения в стиле MVVM, или хотя бы ASP.NET + Code Behind. Только взгляните, насколько упрощается написание кода, без необходимости постоянного преключения контекстов (для включения этого режима, тыцьните правой кнопкой мышки по имени файла в редакторе и выберите New Vertical Tab Group, в контекстном меню):

Кстати, эта подсказка от Хромого привела к супер-пупер блогу Сары Форд: MS VS Tips. Сара работала SDET, в VS Core Team, на протяжении пяти лет. И, у нее также описывался механизм гайдлайнов.

Покамест по студии все. Двинем дальше :).

Snoop + Silverlight Spy


Ну почему? Почему здесь показывается этот серый квадратик? А кто это у меня обработал событие?

Знакомо? В таких ситуациях очень полезными оказываются наши очередные резиденты: Snoop, для мира WPF, и Silverlight Spy, для... Silverlight'a конечно :).

Snoop. Не очень представляю, как жил без этой утилиты. Может найти WPF процесс, внедрится в него и показать все внутренности. Просто просмотрите скриншоты на сайте Snoop'a, если мои доводы не убедительны :). Вот как Снуп разложил Blend по полочкам, т.е. по z-index'у:


Silverligth Spy. Может практически все, что может Snoop, только для Сильверлайта :). Если скажете, где лежит рефлектор, покажет и исохдники приложения. Если вы - сильверлайт разработчик, просто пройдитесь по списку возможностей.

KaXaml


KaXaml - это очень легкий редактор замла. Удобен для тестирования разметки, да и вообще - удобен :) :


Baml Viewer


Baml Viewer - это add-in для Reflector'a. Позволяет декомпилировать .baml и смотреть чистый .xaml :).


Так, имея на руках Snoop, Reflector и Baml Viewer можно посмотреть исходники Blend'a (для изучения, конечно же :)).

Выводы?


Позвольте не делать никаких выводов :). Я не включал в обзор сам Expression Blend, т.к. он заслуживает отдельного поста. Некоторые находят удобными так же Xaml Power Toys, но мне как-то они не приглянулись. Хотя возможность редактировать Grid, изменяя порядок столбцов, и генерировать ViewModel по модели - это, конечно же, достойные плюсы.
А чем вы пользуетесь? Было ли что-то полезно/интересно из предложенного списка? Не молчите, пожалуйста, дайте знать :). Буду очень признателен!
Отличного настроения и программирования!

суббота, 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();
}

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


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

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