суббота, 19 декабря 2009 г.

SilverNotes: Drag'n'Drop + Print + RightClick

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

Сегодняшний пост посвящен сразу трем новинкам Silvelright 4.0: Printing, Drag'n'Drop и Right Click.

Надо отдать должное разработчикам Silverlight: они сделали настолько простой API для рассматриваемых новинок, что их описание влезло бы в два-три твита (интересно, твит - это уже стандартизированная единица размера информации :)? ).

В сумме эта тройка позволила мне за пару часов написать маленькое, но полезное приложение: SliverNotes (исходники, демо - для демо нужен 4-й сильверлайт). Эта программка позволяет сделать из обычного листка формата A4 карманную книжечку на 8 министраниц. Формат и содержание каждой странички определяется пользователем. Например, это может быть список покупок, подготовленный любимой женой забывчевому мужу, или же каждая страница может быть нотоносцем для талантливого музыканта, или... все что захотите. Вот пример распечатанной карманной книжки:



Итак к делу! (Иначе я рискую сделать введение самой длинной и сложной частью этого поста :)...)

Печать


Все, что нужно от разработчика для вывода содержимого на печать, это вызов функции Print() у объекта типа PrintDocument:
// PrintDocument - тот, кто нам нужен для печати.
var doc = new PrintDocument { DocumentName = "Notes" };

doc.PrintPage += (o, e) =>
                   {
                     e.PageVisual = uie;
                     e.HasMorePages = false;
                   };

doc.Print();
В обработчике события PrintPage  мы говорим Silverlight'у что именно нужно напечатать (e.PageVisual = uie). Здесь uie - это объект типа UIElement. Если бы мы хотели распечатать больше, чем одну страницу, то свойство e.HasMorePages нужно установить в true.

Чуть-чуть деталей

  • Если элемент, который вы выводите на печать "обрезается" родительским контейнером, он будет обрезанным и на бумаге. Чтобы это обойти, просто измените родительский контейнер, например, на Canvas.
  • Если вы применяете трансформации прямо к элементу, который служит PageVisual'ом, то они не будут применены на печати. Чтобы это обойти, устанавливайте трансформации ниже по дереву: вместо самого PageVisual'a, трансформируйте его первого ребенка (жуть-то какая :) ).
  • PageVisual не обязательно должен находиться в Visual Tree. Это может быть просто объект в памяти.
  • Инициировать печать можно только по запросу пользователя (например, в обработчике Click'a). Эй, а где еще его можно инициировать-то, умник? - Ну, у меня было желание стартовать печать в событии Loaded, но Silverlight пожелал мне удачи в виде приветливого Exception'a. Даже если обработчик Loaded - это лямбда в обработчике Click, вы получите исключение...

Drag'n'Drop


Эта новинка позволяет пользователям перетаскивать в плагин [пока только] файлы из операционной системы. Стоит ли говорить, на какой уровень это повышает UX (user experience)? . Фантазия - ваш предел. В SilverNotes вы можете перетащить на пустую страничку картинку, тем самым сделав свой шаблон для печати:


1. Слева - окошко Windows Explorer'a. Я схватил фотографию леса из моего родного города Лебедина, и тащу ее на SilverNotes. Превью фото делает Explorer.

2. Теперь указатель мышки находится над пустой страничкой SilverNote's. Я замечаю это в событии DragEnter, и начинаю анимировать цвет фона, как бы говоря: "Давай, бросай!"
 
3. Ну и, наконец, я отпускаю левую кнопку, и фото оказывается в Silverlight приложении...

Для того, чтобы разрешить бросать файл на UIElement, достаточно установить свойство AllowDrop="True". После этого у UIElement'a будут срабатывать обработчики событий DragEnter, DragLeave, DragOver и Drop. Вот пример кода, который считывает брошенную картинку на UIElement с установленным AllowDrop'ом:

private void DoDrop(object sender, DragEventArgs e)
{
  if (e.Data == null)
  {
    return; // И почему мы так не любим nulls?
  }
  
  // Куда именно это мы бросаем?
  Point dropPosition = e.GetPosition(_cnvDropTarget);

  // Ну-ка, что нам тут подбросили?
  var files = e.Data.GetData(DataFormats.FileDrop) as FileInfo[];

  foreach (var file in files)
  {
    using (var stream = file.OpenRead())
    {
      // Обрабатываем файл:
      ProcessFileContent(stream, dropPosition);
    }
  }
}

Чуть-чуть деталей

  • Не пытайтесь получить доступ к данным во время событий DragEnter/DragLeave. Только разочарование и SecurityException ждут вас. Правильное место для считывания данных - обработчик события Drop.
  • Не пытайтесь перетаскивать файл на Silverlight приложение, с установленным windowless режимом. Также проблематичным будет использование Drag'n'Drop под Маками (см. детали).

ПКМ ака RightClick


О поддержке правого клика в сильверлайт приложениях раньше приходилось только мечтать (или извращаться с html'ем поверх сильверлайта). Теперь же, все что нужно сделать - это подписаться на событие MouseRightButtonDown (или MouseRightButtonUp). Если вы хотите показать свое контекстное меню по правому клику, то нужно... написать свое контекстное меню и показать его. Что?! В сильверлайте нет стандартной реализации класса ContextMenu? - Да, покамест нет. Но не удивлюсь, если с выходом четверки (не беты) оно там появится. Пока же Microsoft предлагает пользоваться классом Popup, и в него встраивать контекстное содержимое.

Для демонстрации на IT Jam'e мы воспользовались кодом Jesse Bishop'a, и чуть-чуть изменили его, сделав более похожим на стандартное WPF меню:



В SilverNotes контекстное меню для Grid'a устанавливает следующий код:

<Grid x:Name="LayoutRoot" Background="Transparent">
  <menu:ContextMenu.ContextMenu>
 <menu:ContextMenu>
   <menu:MenuItem Click="OnDeleteClick">
  <menu:MenuItem.Content>
    <StackPanel Orientation="Horizontal">
   <Image Margin="0, 0, 8, 0" Source="/SilverNotes;Component/Resources/delete.png" Width="16" Height="16"/>
   <TextBlock Text="Delete"/>
    </StackPanel>
  </menu:MenuItem.Content>
   </menu:MenuItem>
 </menu:ContextMenu>
  </menu:ContextMenu.ContextMenu>
  <!-- Содержимое Grid. -->
</Grid>

Вы также можете пользоваться и улучшать код проекта, скачав исходники SilverNotes.

Чуть-чуть деталей

  • Чтобы полностью скрыть стандартное меню сильверлайта, не забудьте установить e.Handled = true, в обработчике MouseRightButtonDown.
  • Событие MouseRightButtonUp не прийдет к вам, до тех пор пока вы не установите e.Handled = true в MouseRightButtonDown.

Итак


Все три новинки наглядно показаны в SilverNotes. Я выложил этот проект на Google Code. Если у вас есть желание усовершенствовать проект, дописать шаблоны для печати - добро пожаловать :). Кстати, создавать свои собственные шаблоны в SilverNotes так же просто, как создавать обычный UserControl на Silverlight'e. Шаблоны загружаются при помощи MEF (Managed Extensibility Framework), который так же входит в поставку четвертого Silverlight. Но об этом чуть-чуть попозже :).

Отличного настроения и прекрасного программирования в предновогодние морозы :)!

понедельник, 7 декабря 2009 г.

Webcam Fun

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

На всеукраинской сходке айтишников IT Jam, нам с Сергеем Лутаем посчастливилось делать доклад по новинкам четвертого Сильверлайта. Пока обрабатывается видео с презентации, выкладываю код, который мы использовали для демонстрации работы с веб камерой. Кроме кода, мы посмотрим на основы работы с камерой  и на несколько веселых эффектов, которые можно сделать при помощи пиксельных шейдеров.

Исходники к этому посту можно скачать здесь. Живое демо находится здесь. Для запуска демо, вам, кроме камеры, понадобится установить Silverlight 4 Beta 1. Рантайм есть только для разработчиков, но ведь это про вас, верно :)?

Основы


Для старта нам понадобится... камера. Чтобы получить доступ к камере из Silverlight'a, достаточно спросить у пользователя разрешения, и получив оное, начать видеозахват:

private CaptureSource _captureSource;

private CaptureSource CaptureSource
{
  get { return _captureSource ?? (_captureSource = new CaptureSource()); }
}

private void StartCapture()
{
  // Можно мне попользоваться вашей камерой?
  if (CaptureDeviceConfiguration.AllowedDeviceAccess || CaptureDeviceConfiguration.RequestDeviceAccess())
  {
    // Урра! Начинаем захват: 
    CaptureSource.Start();
    
    var brush = new VideoBrush();
    brush.SetSource(CaptureSource);
    VideoBrush = brush; // К этому свойству можно привязать любой Brush: <Border Background="{Binding VideoBrush}" .../>
  }
}
Видео является обычной кисточкой (Brush), а значит, мы можем разукрасить выходом с камеры любой элемент, который можно разукрасить :). Пожалуй, это и все. Просто, не так ли?

Не Основы


К "продвинутым" темам можно отнести сохранение рисунка с камеры. Тут есть два варианта: воспользоваться функцией CaptureSource.AsyncCaptureImage() или сделать снимок UIElement'a, как обычно, при помощи WriteableBitmap'a: new WriteableBitmap(uie, null). Разница в том, что WriteableBitmap фотографирует содержимое со всеми эффектами, а CaptureSource выдаст именно то, что есть на выходе камеры, без трансформаций, эффектов и прочего.

Если есть несколько камер (скорее всего, у вас, как и у меня, их нет, но мало ли :)?), то можно всегда выбрать с какой камеры вести захват: CaptureDeviceConfiguration.GetAvailableVideoCaptureDevices();

Для того, чтобы сохранить WriteableBitmap в Jpeg формат нужно написать свой энкодер, который будет последоватльно, байт за байтом, запаковывать цвета в формат Jpeg. Для этого, конечно, нужно сначала достать спецификацию Jpeg'a, изучить ее, и заимплементить... Шутка :). Все уже написано до нас: библиотека FJCore позволяет легко сохранять jpeg'и. Для просмотра примера использования, загляните в исходники к этому посту.

Спецэффекты


Использование спецэффектов радует больше всего. В реальном времени увидеть себя с необычной стороны, и я говорю не о спине, - оказывается очень увлектально :)! О создании собственных эффектов на шейдерах, я уже писал. А на презентации мы показали четыре эффекта:

Эффект находит границы на рисунке и закрашивает их черным цветом. Все остальное выводится белым. На этой фотке знакомые могут узнать Мишу Чалого :).
С этой фотки из паралельной вселенной дружественный привет землянам шлет головастый и чертовски умный двойник  автора этого блога :).
Этот эффект-страшилка просто инвертирует цвета. Как-то так выглядел мир по ту сторону кольца, когда Фродо надевал его :).
И, наконец, последний эффект, "скрытая фича Sivlerlight 4.0", он же знакомый нам по передачам типа "Прогноз погоды": подмена вывода. Как только цвет точки становится ярче определенного порога, вместа оригинала выводится точка из другого сэмплера (читай - brush'a). Здесь я направил включенный фонарик в ладошку, открывая секретный кусочек лица Билла.

Конечно, можно написать шейдер, который будет подменять вывод в зависимости от разных условий, например, на синий цвет выводить Брина и Пейджа, на зеленый - Стива, а на очень белый (или черный, в зависимости от того, на чьей вы стороне) - Билла. 

Выводы


Пользоваться камерой в Silverlight  4.0 очень легко. Все операции с камерой не требуют  участия сервер-сайда. Можно легко сохранять изображения.

Для тех, кто любит просто читать с конца, еще раз привожу ссылку на исходники и демо. Не забудьте про рантайм: Silverlight 4 Beta 1. Отличного программирования и прекрасного настроения!

ЗЫЖ спасибо всем, кто пришел к нам на презентацию. И, конечно, всем тем, кто поставил за нас "наляпочки" на доске голосования :). Нам было очень приятно!

среда, 8 июля 2009 г.

July 10: See the Light

Привет :)!

Немножко пафосное название, но из домена букв не выбросишь. http://www.seethelight.com/ анонсирует выпуск третьего сильверлайта и бленда уже 10-го июля. Согласно MSDN Flash'у, виртуальный запуск состоится именно там.

Ждем-ждем тяпницы!

среда, 10 июня 2009 г.

Silverlight vs WPF

Привет!


Silverlight, обычно, называют подмножеством WPF и не зря. Стили программирвания на WPF и на Silverlight очень похожи друг на друга: (XAML + Code Behind) * .NET. Но за этим сходством кроется опасность. Например, программируя на WPF'e, можно ожидать, что Silverlight так же поддерживает создание своих собственных расширений разметки (markup extension). А он, козявка, и не думает. Таких тонких различий, компания Wintellect насобирала на 70 страниц в документе Guidance on Differences Between WPF and Silverlight. Помимо различий/сходств парни дают советы, как обойти некоторые ограничения "подмножества".

Надеюсь, в скором времени число страниц в документе уменьшится, как и различий между Silverlight и WPF :).

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

PS: XAML читается как "замл" или "зэмл", а не "икс-эй-эм-эл" и не "кзамл". Кстати, изначально аббревиатура означала Extensible Avalon Markup Language, а не Extensible Application Markup Language.

воскресенье, 17 мая 2009 г.

TechTalks: Sackoverflow

Привет!

К своему огромному стыду только что узнал, что stackoverflow.com был создан Джоэлом Спольки (киньте в меня чем-то, за темноту).

О том, почему сайт пользуется такой популярностью, Джоэл рассказывает на TeckTalk'e в Google. Очень рекомендую:

среда, 29 апреля 2009 г.

Magic Shaders: Genie Effect

Привет!

Мой коллега, счастливый обладатель MacBook'a, показал мне сногсшибательный эффект сворачивающегося окошка в Mac OS'e: Genie effect (эффект джина). Вот как он выглядит:



Тогда я загорелся идеей сделать аналогичный эффект в Silverlight приложении, но не знал как :). Последнюю неделю я провел за изучением пиксельных шейдеров и к огромной моей радости обнаружил ключ к разгадке. В скором времени родилась библиотека, реализующая Genie-эффект, как в WPF'e, так и в Silverlight'e.  Посмотреть живую демку эффекта можно здесь (нужен 3-й сильверлайт: link). В этом посте мы:
  • Посмотрим как использовать Genie-эффект у себя в коде.
  • Посмотрим как создаются собственные эффекты на базе пиксельных шейдеров.
  • Разберем алгоритм Genie-эффекта
Ссылка на исходники к статье находится в конце поста. Исходники распространяются под LGPL лицензией.

NB: Apple Inc. является обладателем патента на этот эффект в США. Надеюсь, я не нарушаю их прав, описывая в исключительно учебных целях один из возможных вариантов создания подобного эффекта на WPF/Silverlight технологиях.

GenieLib в действии


GenieLib - это моя библиотека, реализующая описанный эффект Джина (исходники - в конце поста). Для того, чтобы создать Genie-эффект в вашем приложении достаточно добавить ссылку на сборку GenieLib и определить два UIElement'a: один будет представлять Джина, а второй - Лампу (место, куда прячется Джин).

Вот, как это выглядит в коде:

XAML:
<Button Click="ToggleGenie"
 x:Name="btnLamp"
        Content="Lamp"/>
<Image x:Name="imgGenie" Source="...">
<Image.RenderTransform>
    <TranslateTransform />
</Image.RenderTransform>
</Image>

C#
private void ToggleGenie()
{
  GenieManager.IsGenieOut = !GenieManager.IsGenieOut;
}

private Magic GenieManager
{
  get
  {
    if (_genieManager == null)
    {
      _genieManager = new Magic(btnLamp, imgGenie);
      _genieManager.Expanding +=
        (o, e) =>
        {
          imgGenie.Visibility = Visibility.Visible;
        };

      _genieManager.Collapsed +=
        (o, e) =>
        {
          imgGenie.Visibility = Visibility.Collapsed;
        };
    }
    return _genieManager;
  }
}

Как видите, мы определили два UIElement'a в xaml-файле, и по нажатию на кнопку, которая по совместительству подрабатывает волшебной лампой, мы обращаемся к объекту типа Magic, инвертируя текущее состояние Джина. Не смотря на название, класс Магии не делает ничего таинственного. В его обязанности входит построение анимации перехода Джина в Лампу и из нее. Класс не накладывает никаких ограничений на представителей Лампы и Джина. Но для правильной работы он ожидает от Джина наличия одного RenderTransform'a: TranslateTransform. Именно этот трансформ позволяет Джину "летать" в пространстве по направлению к лампе и от нее. Если это требование не соблюдается, Джин будет уменьшаться в рамках собственной области, не приближаясь к лампе (картинка слева):



Размер и позиция Лампы относительно Джина играют важную роль. Позиция определяет в какую сторону будет лететь Джин при сворачивании, а размер Лампы (ширина или высота, в зависимости от направления сворачивания) указывает Джину насколько сильно нужно сузиться, чтобы "влезть" в горлышко Лампы :).

Помимо этого класс Magic уведомляет клиентов  при помощи событий о специфических состояниях Джина (собирается спрятаться в лампу, спрятался, собирается выйти из лампы и вышел из лампы). Клиенты могут реагировать на события должным образом. В нашем случае, мы убираем Джина, устанавливая его свойство Visibility в Visibility.Collapsed, когда Джин спрятался в Лампу, и показываем его, когда Джин собирается наружу. Забегая вперед, скажу, что эффект, который реализует уменьшение Джина ничего не знает о HitTesting'e, поэтому мы должны позаботиться об этом самостоятельно. Иными словами, если бы мы не меняли видимость Джина, пользователь все так же мог бы кликнуть мышью по пустой области и попасть на невидимую кнопку спрятавшегося Джина.

Библиотека также поддерживает несколько "продвинутых" сценариев, но я не хочу утомлять вас, дорогой читатель, этими подробностями. Любопытные всегда смогут найти детали в коде, который я старался тщательно документировать и писать предельно ясно :). Дальше мы переходим к промежуточному шагу: создание эффектов при помощи пиксельных шейдеров.


Создание эффектов при помощи шейдеров


Третий Silverlight ввел поддержку эффектов, которые сыздавна были доступны разработчикам WPF-приложений. Теперь мы можем добавить любому  UIElement'у эффект тени (DropShadowEffect), или сделать его размытым (BlurEffect). Но как быть, если мы хотим создать свой собственный эффект, например, полностью убирающий красную составляющую из UIElement'a к которому он прикреплен? На помощь приходит класс ShaderEffect, который также был доступен WPF разработчикам с выходом WPF 3.5 SP1.

Суть ShaderEffect'a проста: взять визуальное представление элемента и прогнать его через пиксельный шейдер. Мое наивное определение пиксельного шейдера: это функция, возвращающая цвет точки в заданной координате. Правда, функция эта описывается не на C#, а на языке шейдеров, и исполняется обычно не на CPU а на GPU. Мы пользуемся С-подобным языком от Microsfot'a: HLSL (High Level Shaders Language - высокоуровневым языком шейдеров) для описания шейдера. В Silvelright 3 Beta 1 сам шейдер выполняется центральным процессором, не смотря на введенную поддержку GPU. В релиз версию парни из Майкрософт обещают включить поддержку выполнения шейдеров видео картой.

Алгоритм создания ShaderEffect'a:

1. Создаем файл с названием RemoveRed.fx. В файле определяем функцию шейдера:
float r : register(C0);

sampler2D implicitInputSampler : register(S0);

float4 main(float2 uv : TEXCOORD) : COLOR
{
   float4 c = tex2D( implicitInputSampler, uv );
   c.r = r;
   return c;
} 
Здесь мы определили две глобальные переменные (r типа float; implicitInputSampler типа sampler2D) и одну функцию main(). На входе функция принимает переменную типа float2 - двумерный вектор, представляющий координаты точки, для которой требуется вычислить цвет. Выходом из функции служит переменная типа float4 - вектор с четырьмя измерениями, описывающий цвет точки (r, g, b, a). Значения координаты точки меняется в пределах от 0 до 1. Точка с координатами (0,0) - верхний левый угол, (1, 1) - нижний правый. Аналогичным диапазоном измеряется и составляющие цвета: от нуля до единицы.

Переменная типа sampler2D предоставляет механизм доступа к цветам элемента, к которому мы применяем эффект. Для того чтобы узнать какого цвета точка, скажем в правом верхнем углу достаточно вызвать встроенную функцию tex2D(implicitInputSampler, (1, 0)), которая вернет нам вектор со значениями цвета: (r, g, b, a). В примере выше, мы просто получаем истинный цвет точки в своей собственной позиции и изменяем красную составляющую на значение глобальной переменной r. Так, если значение r будет равно нулю мы получим следующее преобразование:


Значения глобальных переменных мы можем установить извне шейдера. Как именно - описано ниже. На этом мы закончим обзор HLSL и двинемся дальше, но пытливый читатель всегда найдет много полезной и интересной информации в разделах MSDN'a, посвященных языку программирования шейдеров.

2. Далее, мы компилируем эффект при помощи утилиты fxc.exe из DirectX SDK.. Просто запускаем компилятор из командной строки:

   fxc.exe /T ps_2_0 /E main /Fo RemoveRed.ps RemoveRed.fx
Здесь мы явно указываем компилятору использовать вторую модель пиксельных шейдеров (/T ps_2_0), в качестве входа в программу шейдера служит функция main (/E main), и, ожидаемым результатом компиляции файла RemoveRed.fx является объектный файл RemoveRed.ps (/Fo RemoveRed.ps RemoveRed.fx). Если у вас нет желания устанавливать дополнительно ПО, всегда можно воспользоваться онлайн компилятором:

3. Добавляем скомпилированный файл шейдеров в наш проект. Устанавливаем его Build Action в Resource:


4.Теперь создаем C# класс, который будет представлять наш шейдер в качестве эффекта:

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Effects;

namespace SilverShaderExample
{
 public class RemoveRedEffect : ShaderEffect
 {
  public static DependencyProperty InputProperty =
   RegisterPixelShaderSamplerProperty("Input",
                                      typeof(RemoveRedEffect), 0);

  public static DependencyProperty RProperty =
   DependencyProperty.Register("R",
                               typeof(double),
                               typeof(RemoveRedEffect),
                               new PropertyMetadata(new double(), PixelShaderConstantCallback(0))
    );

  public RemoveRedEffect()
  {
   var u = new Uri(@"SilverShaderExample;Component/RemoveRed.ps", UriKind.Relative);
   PixelShader = new PixelShader { UriSource = u };

   // Инициализируем свойства, чтобы они получили верные начальные значения.
   UpdateShaderValue(InputProperty);
   UpdateShaderValue(RProperty);
  }

  public virtual Brush Input
  {
   get
   {
    return ((Brush)(GetValue(InputProperty)));
   }
   set
   {
    SetValue(InputProperty, value);
   }
  }

  public virtual double R
  {
   get
   {
    return ((double)(GetValue(RProperty)));
   }
   set
   {
    SetValue(RProperty, value);
   }
  }
 }
}
  

Здесь основной акцент стоит сделать на конструкторе класса и на регистрации Dependency свойств.

Давайте на секунду обратимся к .fx файлу. Помните наши глобальные переменные в шейдере? Так вот, при помощи Dependency-свойств, именно здесь происходит связывание переменных шейдера со свойствами managed-объекта. Сэмплер регистрируется при помощи метода RegisterPixelShaderSamplerProperty(), а переменная R использует PixelShaderConstantCallback() в качестве метода, реагирующего на изменения.

В конструкторе класса мы создаем ссылку на скомпилированный шейдер. Стоит быть очень внимательным в формировании этой ссылки. Например, если вы укажете Uri("RemoveRed.ps", UriKind.Relative) вместо Uri(@"SilverShaderExample;Component/RemoveRed.ps", UriKind.Relative), то сильверлайт упадет с далеко не очевидным объяснением исключения. Затем  мы создаем новый экземпляр класса PixelShader, который представляет собой управляемую обертку над нашим шейдером.

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

Дальше дело за малым: применить эффект можно к любому UIElement'у. Так, следующий код, определяющий Grid с красным фоном будет отображен как черный прямоугольник, благодаря нашему  эффекту:

RemoveRed
В Silverlight 3.0 beta 1 можно применить только один эффект к одному элементу. Но элемент всегда можно вложить в несколько, скажем, Border'ов, и применить по одному из эффектов к каждому Border'у :).

Завершая наш обзор шейдерных эффектов, привожу ссылку на инструмент, значительно упрощающий работу: Shazzam Tool (click-once installer), Shazzam Help (Silverlight-based help). Shazzam позволяет отредактировать HLSL код:



Скомпилировать HLSL-код, посмотреть на результат и на сгенерированный C#- или VB-код для эффекта, не отходя от кассы (WYSIWYG, по-нашему):


Более того, если эффект содержит входящие параметры, их можно модифицировать прямо из Shazzam'a:

Вобщем, must-have, если вы серьезно настроены на создание своих эффектов :).

Алгоритм Genie


Здесь мы посмотрим как заставить Джина "свернуться" вниз. Сворачивание вверх, влево и вправо получается аналогично.

В основе эффекта лежит простая идея: за время t нужно свернуть картинку в точку (а точнее, в отрезок). При t = 0 картинка полностью развернута, а при t = 1 - полностью свернута. Как вы помните, функция пиксельного шейдера возвращает значение цвета точки. Т.е.нам нужно определить цвет заданной точки, с учетом текущего времени t. Это приводит нас к следующему коду:

float t : register(c0); // Время

sampler2D implicitInput : register(s0); // сэмплер

float4 main(float2 uv : TEXCOORD) : COLOR
{
 // [.. вычисление x1, y1 x2, y2 ..]
 
 if (uv.x < x1 || uv.x > x2 || uv.y < y1 || uv.y >y2)
 {
  return float4(0, 0, 0, 0);
 }
  
 float2 pos = float2((uv.x - x1)/(x2 - x1), (uv.y - y1)/(y2 - y1));
  
 return tex2D(implicitInput, pos);
}

Мы проверяем, или выходит текущая точка uv за пределы области (x1, y1, x2, y2). Если да - возвращаем прозрачный цвет (r = 0, g = 0, b = 0, a = 0), если же нет, то мы должны вернуть такой цвет, чтобы создался эффект "сжатия". Для этого мы отображаем ограничивающую нас область на всю картинку, т.е. (x1, x2) -> (0, 1), и (y1, y2) -> (0, 1). Что дает нам необходимые координаты: x = (uv.x - x1)/(x2 - x1), y = (uv.y - y1)/(y2 - y1).

Осталось посчитать x1, x2, y1, y2. Начальные значения переменных устанавливаются таким образом, чтобы вся картинка была видимой: x1 = 0, y1 = 0, x2 = 1, y2 = 1.Теперь мы вводим зависимость этих переменных от времени. Если, например, y1 будет увеличиваться пропорционально времени t: y1 = t, мы получим сжатие по вертикали:


Устанавливая более сложные зависимости, мы получаем Genie-эффект. Эффект состоит из двух фаз. Фаза сжатия по оси Х длится 40% времени:

Фаза сжатия по оси Y (оставшиеся 60%):

Для вычисления x1 и x2  я воспользовался кубическими кривыми Бизье. Одна кривая определяется четырьмя опорными точками P0, P1, P2 и P3. Кривая начинается в P0 и заканчивается в P3. Внутренние точки P1 и P2 управляют формой кривой и определяют начальный и конечный векторы касательных. Другими словами, кривая Бизье третьего порядка начинается в точке P0 направляясь к точке P1 и заканчивается в P3, подходя к ней со стороны точки P2:

Для нас началом кривой всегда является P0 = 0, а конец зависит от времени t, центра лампы и ширины ее "горлышка".  С учетом двухфазового подхода к эффекту, мы получаем следующий HLSL код:
float t : register(c0); // Время
float lampPosition : register(c1); // Центр Лампы
float lampWidth: register(c2); // Ширина Горлышка Лампы

sampler2D implicitInput : register(s0); // Сэмплер

// Параметрическая запись кубической кривой Бизье
// честно стибрена с http://ru.wikipedia.org/wiki/Кривая_Безье
float BezierInterpolate(float4 cp, float t)
{
     float b0 = pow(1-t, 3);
     float b1 = 3*t*pow(1-t,2);
     float b2 = 3*t*t*(1-t);
     float b3 = pow(t, 3);
     return b0*cp[0] + b1*cp[1] + b2*cp[2] + b3*cp[3];
}

float4 main(float2 uv : TEXCOORD) : COLOR
{
     float x1=0, x2=1, y1=0, y2 = 1;
     float4 lp, rp; // Опорные точки левой и правой кривых Бизье
     
     // Фаза сжатия по оси Y
     if (t > 0.4) 
     {
      lp[3] = lampPosition - lampWidth/2;
      rp[3] = lampPosition + lampWidth/2;
      y1 = (5*t - 2)/3.0;
     }
     else // Сжимаемся по оси X.
     {
      lp[3] = t*(lampPosition - lampWidth/2)/0.4;
      rp[3] = 1 + t*(lampPosition + lampWidth/2 - 1.0)/0.4;
      y1 = 0;
     }
     
     // Подсчитываем значения осташихся опорных точек
     // кривых Бизье.
     lp[2] = 0.90*lp[3];
     lp[1] = 0.25*lp[3];
     lp[0] = 0;
     
     rp[2] = 1.25*rp[3];
     rp[1] = 1-(rp[3] - rp[2] - lampWidth/2);
     rp[0] = 1;
     
     lp = saturate(lp); 
     rp = saturate(rp);

     // Вычисляем значение границ по оси X.
     x1 = BezierInterpolate(lp, uv.y);
     x2 = BezierInterpolate(rp, uv.y);
     
     if (uv.x < x1 || uv.x > x2 || uv.y < y1 || uv.y >y2)
     {
          return float4(0, 0, 0, 0);
     }
     
     float2 pos = float2((uv.x - x1)/(x2 - x1), (uv.y - y1)/(y2 - y1));
     
     return tex2D(implicitInput, pos);
}

Единственной темной лошадкой остался вызов HLSL-функции saturate(), применительно к опорным точкам кривых Бизье. Суть этого метода в отбрасывании точек за границей отрезка [0, 1]:
function saturate(x)
{
  if (x < 0) return 0;
  if (x > 1) return 1;
  return x;
}

Очень удобно, для проверки выхода за допустимые границы.

Вот, собственно, и вся магия :).


Что Дальше?


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

Код к статье можно скачать здесь. Буду рад вашим комментариям.

Отличного настроения :)!

вторник, 7 апреля 2009 г.

UX Patterns

Привет!

Наткнулся на очень интересное собрание UX Pattern'ов (user experience patterns), от Infragistics. Сайт написан на Silverlight'e, что не может не радовать. Содержит детальное описание для каждого паттерна (в чем проблема, какое решение, в каком контексте используется, примеры). Можно так же смотреть связанные паттерны.

Вот несколько скринов (кликабельно):



Более того, можно предлагать свои находки. Молодцы, Infragistics. Молодцы!

понедельник, 6 апреля 2009 г.

Triggers + Actions = Fun

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

Сегодня мы посмотрим на часть API MS Expression Blend 3.0 триггеры и действия (Triggers & Actions): что это такое, как их создавать и использовать. На закуску мы разработаем пару полезных действий: обновление стиля элемента по действию пользователя, и реализацию Command'a в Silverlight приложении, что сделает наш MVVM еще более эмвевеэмнее :).
 

Что и как?


Проще всего представить себе эту парочку одним предложением:
Когда произойдет А сделай Б.
Здесь А - это триггер, Б - действие. Например:
  • Когда пользователь нажмет эту кнопку (trigger), показать MessageBox (action).
  • Когда страница загрузится (trigger), установить фокус в это поле ввода (action).
  • Когда пропадет сетевое подключение (trigger), перейти в автономный режим (action).
  • Когда рак на горе свистнет (trigger), получить прибавку к зарплате (action).

Триггеры


Очень похожи на триггеры в WPF, но с бoльшей долей свободы. Если раньше мы были ограничены триггерами, реагирующими на изменения dependency properties или на изменения в привязанных (data bound) свойств, то теперь триггером может быть что угодно (см. список выше).

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

Запутанно? Давайте посмотрим на пример. В подтверждение широкого разнообразия природы триггеров, мы напишем триггер, который выполняет действия, "когда рак на горе свистнет" :). Вот чудо-код:
using System;
using System.Threading;
using System.Windows;
using System.Windows.Threading;

using Microsoft.Expression.Interactivity;

namespace TriggersActions
{
  public class CrabWhistle : TriggerBase<UIElement>
  {
    private readonly Random _rand = new Random();
    private Dispatcher _dispatcher;

    protected override void OnAttached()
    {
      _dispatcher = Dispatcher;

      new Timer(CrabLifeCycle, null, 100, 100);
    }

    private void CrabLifeCycle(object state)
    {
      if (_rand.Next(20) == 10)
      {
        _dispatcher.BeginInvoke(() => InvokeActions(null));
      }
    }
  }
}
Чтобы стать триггером, мы унаследовались от TriggerBase<UIElement>. Generic-параметр сужает круг элементов, к которым может быть прикреплен наш триггер до UIElement'a. Как и с поведениями (Behaviors), наш триггер будет уведомлен о прикреплении к UIElement'у, вызовом метода OnAttached(). Здесь-то мы и начинаем следить за раком: когда же он свистнет? Мы запустили таймер и там доверились генератору случайных чисел. Не самый достоверный показатель события "рак свистит", но достаточный для примера :).

Когда наступает "тот самый" момент, мы вызываем метод InvokeActions(), который просто проходится по коллекции действий и заставляет их выполниться. Коллекция действий всегда доступна разработчику триггеров по свойству Actions.

Из триггера мы так же можем обратиться к объекту, к которому прикреплен сам триггер (свойство AssociatedObject). Значит, мы можем подписаться и на события объекта и реагировать вызовом действий. Например, добавили триггер к кнопке и слушаем ее событие Click. В обработчике события вызываем InvokeActions().

Итак, мы посмотрели на триггеры: объекты, инициирующие действия, по каким-то условиям. Но что же представляют собой действия?

Действия



Действия - это объекты типа TriggerAction, реализующие метод Invoke(). Именно этот метод вызывается методом InvokeActions(), когда срабатывает триггер. "Позвольте, - скажете вы, - это же привязывает действия к триггерам". И будете абсолютно правы: действия имеют смысл лишь в контексте триггера. Давайте посмотрим на код действия, поднимающего нашу зарплату :) :
using System;
using System.Windows.Controls;

using Microsoft.Expression.Interactivity;

namespace TriggersActions
{
  public class IncreaseSalary : TargetedTriggerAction<TextBlock>
  {
    protected override void Invoke(object param)
    {
      Target.Text = string.Format("Зарплата поднята: {0}", DateTime.Now);
    }
  }
}
Здесь мы унаследовались от потомка TriggerAction, а именно от TargetedTriggerAction<TextBlock> - разновидность действия из API Blend'a, позволяющая указать на ком будет выполнятся действие. Мы выбрали TextBlock, чтобы обрадовать пользователя о повышении зарплаты. Целевой TextBlock будет определен в xaml.

Стоит отметить, что действия могут быть исключены из обработки, установкой свойства IsEnabled в false. Например, действие IncreaseSalary может устанавливать это значение в false, если на дворе случится какой-нибудь кризис. Но, поскольку это не наш случай, у нашего действия значение свойства IsEnabled оставлено по умолчанию: true.

Поскольку действия имеют смысл только с триггерами, давайте соберем все вместе и посмотрим, на реализацию "Поднять зарплату, когда рак на горе свистнет".

Триггеры + Действия


После компиляции в Blend'e сильверлайт-проекта с определенными выше классами триггера и действия, на вкладке Behaviors мы найдем только IncreaseSalary:



Перетащив это действие на единственный TextBlock, определенный в нашем xaml-файле, мы получим следующую картину:


Expression Blend создал для нас триггер типа EventTrigger (поставляемый вместе с API), и поместил наше действие в качестве действий триггера:
<TextBlock VerticalAlignment="Top" Text="TexBlock1">
 <i:Interaction.Triggers>
  <i:EventTrigger EventName="Loaded">
   <ta:IncreaseSalary/>
  </i:EventTrigger>
 </i:Interaction.Triggers>
</TextBlock>
EventTrigger - весьма полезная штуковина, позволяющая при наступлении CLR события выполнять actions. Но нам-то нужно не простое CLR-событие, а событие "рак свистнул". С интерфейсом Blend'a менять тип триггера одно удовольствие:

1. В Objects and Timeline под нашим TextBlock'ом выбираем IncreaseSalary.
2. На вкладке свойств (Properties) тыцькаем New напротив TriggerType.
3. В появившемся окошке выбираем наш триггер CrabWhistle
 


После всех проделанных операций мы получим следующую xaml-разметку:
<UserControl
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 xmlns:i="clr-namespace:Microsoft.Expression.Interactivity;assembly=Microsoft.Expression.Interactivity" xmlns:local="clr-namespace:TriggersActions"
 x:Class="TriggersActions.MainControl"
 Width="400" Height="400">
 <Grid x:Name="LayoutRoot">
 <TextBlock VerticalAlignment="Top" Text="TextBlock" TextWrapping="Wrap">
 <i:Interaction.Triggers>
  <local:CrabWhistle>
   <local:IncreaseSalary/>
  </local:CrabWhistle>
 </i:Interaction.Triggers>
</TextBlock>
</Grid>
</UserControl>

Запускаем приложение, и после первого же свистка рака наша зарплата повышается. Свисти, дружок, свисти :)!
Стоит обратить внимание на то, что мы можем указать IncreaseSalary выбрать другой TextBlock для сообщения добрых вестей. Достаточно установить ему свойство TargetName. Например, следующий код будет так же прекрасно работать:
<Grid x:Name="LayoutRoot">
 <i:Interaction.Triggers>
  <local:CrabWhistle>
   <local:IncreaseSalary TargetName="txtInfo"/>
  </local:CrabWhistle>
 </i:Interaction.Triggers>
 <TextBlock Name="txtInfo" Text="TextBlock"/>
</Grid>
Единственный элемент, на который мы не обратили до сих пор внимание - это Attached Property под названием Interaction.Triggers. Суть работы этого свойства ничем не отличается от сути работы аналогичного свойства для поведений Interaction.Behaviors (см. описание в прошлом посте).

Прежде чем мы перейдем к созданию интересных действий, давайте подведем итоги:

1. Прикрепленное свойство Interaction.Triggers позволяет определить коллекцию триггеров для UIElement'a (точнее: для любого DependencyObject'a).
2. Триггер содержит коллекцию действий, которые выполняются вызовом метода InvokeActions().
3. Каждое действие реализует метод Invoke(), вызываемый при срабатывании триггера.

Действие: обновление стиля.


Очень просто в реализации:
using System.Windows;

using Microsoft.Expression.Interactivity;

namespace TriggersActions
{
  public class SetStyleAction : TargetedTriggerAction<FrameworkElement>
  {
    public static readonly DependencyProperty StyleProperty =
      DependencyProperty.Register("Style",
                                  typeof(Style),
                                  typeof(SetStyleAction),
                                  new PropertyMetadata(null));

    public Style Style
    {
      get
      {
        return (Style)GetValue(StyleProperty);
      }
      set
      {
        SetValue(StyleProperty, value);
      }
    }

    protected override void Invoke(object parameter)
    {
      if (Target != null)
      {
        Target.SetValue(FrameworkElement.StyleProperty, Style);
      }
    }
  }
}
Как видите, мы определили свойство Style, и в момент срабатывания триггера просто устанавливаем значение стиля FrameworkElement'у. Так, следующий код, при наведении мышкой установит стиль Canvas'у, превращая цвет фона в зеленый:
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="clr-namespace:Microsoft.Expression.Interactivity;assembly=Microsoft.Expression.Interactivity"
xmlns:ta="clr-namespace:TriggersActions"  
x:Class="TriggersActions.MainControl"
Width="400" Height="400">
<UserControl.Resources>
  <Style x:Key="CanvasStyle" TargetType="Canvas">
     <Setter Property="Background" Value="Green"/>
   </Style>    
</UserControl.Resources>
<Grid x:Name="LayoutRoot">
    <Canvas Width="200" Height="200">
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="MouseEnter">
                <ta:SetStyleAction Style="{StaticResource CanvasStyle}"/>
            </i:EventTrigger>
        </i:Interaction.Triggers>
 <TextBlock Text="Hover me"/>
    </Canvas>
</Grid>
</UserControl>
    
Просто, не правда ли?

Действие: вызов команд ViewModel'a.


В сборке Microsoft.Expression.Interactivity есть класс, под названием InvokeCommandAction. Но это действие предназначено для работы исключительно в контексте поведения (Behavior). Во время вызова действия происходит поиск свойства типа ICommand с именем CommandName у поведения, к которому прикрепелен наш InvokeCommandAction. Мягко говоря, не совсем то, что нам нужно.

Мы хотим создать действие, которое будет выполнятся в рамках триггера (не поведения), и будет запускать команду с указанным именем, определенную в DataContext'e. Т.е создать аналог следующего WPF кода:
<Button Command={Binding DoSomethingCommand} />
Приступим!

Код C#:
using System.Windows;
using System.Windows.Input;

using Microsoft.Expression.Interactivity;

namespace TriggersActions
{
 public class InvokeCommandAction : TriggerAction<FrameworkElement>
 {
  public static readonly DependencyProperty CommandNameProperty=
   DependencyProperty.Register("CommandName", typeof(string), typeof(InvokeCommandAction), 
                               new PropertyMetadata(DependencyProperty.UnsetValue));

  public string CommandName
  {
   get { return (string)GetValue(CommandNameProperty); }
   set { SetValue(CommandNameProperty, value); }
  }

  protected override void Invoke(object parameter)
  {
   var command = ResolveCommand();
   if (command != null && command.CanExecute(parameter))
   {
    command.Execute(parameter);
   }
  }

  private ICommand ResolveCommand()
  {
   var dc = AssociatedObject.GetValue(FrameworkElement.DataContextProperty);
   return (dc.GetType()).GetProperty(CommandName).GetValue(dc, null) as ICommand;
  }
 }
}
Мы выставляем наружу строковое свойство CommandName, и в методе Invoke() пытаемся найти соответствующую этому имени команду. Для поиска команды мы берем DataContext объекта, к которому прикреплен наш TriggerAction, и через reflection пытаемся получить значение свойства с нужным именем.

Но почему бы просто не определить свойство типа ICommand и использовать привычный {Binding SomeCommand}? Дело в классе Binding. Для того, чтобы значению свойства присвоить Binding-выражение, наличия одного лишь DependencyProperty недостаточно в Silverlight'e. Необходимо чтобы объект, в котором определено это dependency-свойство был типа FrameworkElement (именно такие ограничения накладывает Silverlight на использование Binding'a). А TriggerActoin является потомком DependencyObject'a. Ну да не беда. Чем горевать (или радоваться) об отсутствии множественного наследования в .NET, мы взяли на вооружение Reflection.

Пример


Давайте посмотрим на InvokeCommandAction в действии. Мы создадим приложение, показывающее текущее время по требованию пользователя. ViewModel имеет два свойства: команда для обновления текущего времени, и свойство, возвращающее текущее время:
using System;
using System.ComponentModel;
using System.Windows.Input;
using Microsoft.Expression.Interactivity.Input;

namespace TriggersActions
{
  public class MainControlViewModel : INotifyPropertyChanged
  {
    public event PropertyChangedEventHandler PropertyChanged;

    public ICommand UpdateTimeCommand { get;  private set; }

    public string Time { get { return DateTime.Now.ToString(); } }

    public MainControlViewModel()
    {
      UpdateTimeCommand = new ActionCommand(() => InvokePropertyChanged("Time"));
    }

    private void InvokePropertyChanged(string propertyName)
    {
      var handler = PropertyChanged;
      if (handler != null)
      {
        handler(this, new PropertyChangedEventArgs(propertyName));
      }
    }
  }
}
Здесь класс ActionCommand - это реализация DelegateCommand'a, из Composite Application Guidance for WPF. Самим создавать класс не придется: в сборке Microsoft.Expression.Interactivity он уже определен :). Правда, он не позволяет переопределить метод CanExecute(), но вы всегда можете написать свой собственный ActionCommand - не велика беда.

XAML выглядит так:
<UserControl
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 xmlns:i="clr-namespace:Microsoft.Expression.Interactivity;assembly=Microsoft.Expression.Interactivity"
 xmlns:ta="clr-namespace:TriggersActions"  
 x:Class="TriggersActions.MainControl"
 Width="400" Height="400">
    <UserControl.Resources>
        <ta:MainControlViewModel x:Key="vm"/>
    </UserControl.Resources>
    <Grid x:Name="LayoutRoot" DataContext="{StaticResource vm}" >
        <Canvas>
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="MouseEnter">
                    <ta:InvokeCommandAction CommandName="UpdateTimeCommand"/>
                </i:EventTrigger>
            </i:Interaction.Triggers>
            <TextBlock x:Name="textBlock" 
                       Canvas.Left="31" Canvas.Top="46"
                       Text="{Binding Time}"
                       TextWrapping="Wrap"/>
        </Canvas>
    </Grid>
</UserControl>
Теперь каждый раз, при наведении мышки на текст мы получим самое свежее время. Слава MVVM'у :)!

Итак


Для тех, кто пролистал весь этот пост, в поисках выводов :). Мы посмотрели на работу триггеров и действий. Написали никому не нужный триггер "свистящий рак", который свистел по законам рандомайзера. К сожалению, поместить рака на гору мне не удалось - очень много кода не способствовало бы пониманию концепции. Затем мы натравили на каждый свисток рака действие, повышающее зарплату пользователя (сложно назвать это действие бесполезным :)). Ну и самый конец статьи, возможно будет интересен приверженцам MVVM-подхода в Silverlight'e: Мы реализовали типичный DelegateCommand.

С нетерпением буду ждать ваших комментариев и пожеланий :)!

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

Код к статье: здесь.

воскресенье, 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();
}

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


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

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