суббота, 9 апреля 2011 г.

ErrorIndicator за 15 мин


В библиотеке Silverlthgt Toolkit есть удобный компонент BusyIndicator. Он позволяет сообщить пользователю о ходе выполнения той или иной операции. Компонент хорошо зарекомендовал себя в mvvm модели.
BusyIndicator

Следуя тем же принципам можно показывать ошибки в приложении. По разным причинам ход выполнения операции может быть прерван ошибкой, данные причины могу от нас и не зависеть, но проинформировать пользователя необходимо всегда. От того, как пользователь будет проинформирован об ошибке, зависит общее впечатление о приложении. С точки зрения пользователя, нет ничего хуже, нежели “страшные” окна со “страшным” StackTrace. В конечном счете компонент будет выглядеть так:

Example
XAML

Code Behind

Чтобы контролировать элемент, который может произвести ошибку, его необходимо включить в состав компонента, информирующего пользователя. Поэтому он должен быть наследован oт ContentControl.
public class ErrorIndicator : ContentControl
Далее необходимо задекларировать атрибутами два состояния Error/NoError. Состояние - это сгруппированные значения свойств компонента с определенной меткой. К состояниям можно обращаться в любой момент времени выполнения программы. При обращении к состоянию, компонент заменяет значения своих свойств (которые могут меняться во время жизни компонента), значениями из состояния. Замена может происходить не сразу, а постепенно с заданным промежутком времени (анимация).
[TemplateVisualState(GroupName = VisualStateGroupErrorStates, Name = VisualStateError)]
[TemplateVisualState(GroupName = VisualStateGroupErrorStates, Name = VisualStateNoError)]
public class ErrorIndicator : ContentControl
{
  private const string VisualStateError = "Error";
  private const string VisualStateNoError = "NoError";
  private const string VisualStateGroupErrorStates = "ErrorStates";
}
После того, как состояния компонента задекларированы, необходимо добавить свойства зависимостей для настройки стилей отображения ошибки и содержимого:
public static readonly DependencyProperty ErrorContentProperty =
  DependencyProperty.Register("ErrorContent", typeof (object), typeof (ErrorIndicator),
    new PropertyMetadata(null, OnErrorContentChanged));

public static readonly DependencyProperty ErrorContentTemplateProperty =
  DependencyProperty.Register("ErrorContentTemplate", typeof(DataTemplate), typeof(ErrorIndicator),
    new PropertyMetadata(null));

public static readonly DependencyProperty OverlayBrushProperty =
  DependencyProperty.Register("OverlayBrush", typeof (Brush), typeof (ErrorIndicator),
    new PropertyMetadata(null));

public static readonly DependencyProperty IsErroredProperty =
  DependencyProperty.Register("IsErrored", typeof (bool), typeof (ErrorIndicator),
    new PropertyMetadata(false, OnIsErroredChanged));
  • ErrorContent - поле, которое содержит объект ошибки (например, текст или исключение).
  • ErrorContentTemplate - определяет шаблон отображения ошибки. Если объект, описывающий ошибку, сложный тип данных, это поле поможет “разобрать” его и показать в доступном для пользователя виде. А в идеальном варианте и методы устранения.
  • OverlayBrush - кисть которая “затеняет” тестируемый элемент при возникновении ошибки.
  • IsErrored - поле индикатор, которое показывает/прячет сообщение об ошибке и в последствии делает недоступным внутреннее содержимое.
Для того, чтобы заставить компонент перейти в состояние ошибки или выйти из него, необходимо реагировать на изменения значения свойства IsErrored. Так как значение может быть “прибито” (установлено методом Binding), реагировать надо в обратном вызове свойства зависимостей.
protected virtual void OnIsErroredChanged(DependencyPropertyChangedEventArgs e)
{
   if ((bool)e.NewValue)
   {
       VisualStateManager.GoToState(this, VisualStateError, true);
   }
   else
   {
       VisualStateManager.GoToState(this, VisualStateNoError, true);
   }
}
VisualStateManager - это класс, который переводит компонент в нужное состояние. Каждое состояние (VisualState) по сути представляет собой Storyboard. Реализация состояния заключается в том, что при переходе в это состояние срабатывает заданный Storyboard и устанавливал значения свойствам, т.е. запускается анимация.
Для удобства использования компонента, можно реагировать на изменение значения свойства “ErrorContent”, и если оно не пустое - отображать ошибку. Так как описывать ошибку может любой тип данных (object), то и проверять переменную на null не достаточно, ведь строковое значение может быть пустым, но не быть null.
protected virtual void OnErrorContentChanged(DependencyPropertyChangedEventArgs e)
{
  if (e.NewValue is string)
  {
    IsErrored = !String.IsNullOrEmpty((string) e.NewValue);
  }
  else
  {
    IsErrored = e.NewValue != null;    
  }
}
Чтобы было проще использовать компонент при визуальном проектировании в программе Expression Bland, необходимо пометить свойства атрибутами, значение которых будет играть роль в отображении свойства в той или иной панели свойств, например:
[Category(“Common Properties”)]
public object ErrorContent
{
  get { return GetValue(ErrorContentProperty); }
  set { SetValue(ErrorContentProperty, value); }
}
[Category(“Brushes”)]
public Brush OverlayBrush
{
  get { return (Brush)GetValue(OverlayBrushProperty); }
  set { SetValue(OverlayBrushProperty, value); }
}

XAML

Теперь самое интересное - определение внешнего вида компонента. Чтобы поменять внешний вид компоненту и установить значения по умолчанию свойствам, необходимо в файле Themes/Generic.xaml редактировать стиль который будет применяться по умолчанию для всех компонентов с типом ErrorIndicator (в данном случае стиль создается с “нуля”, но можно создавать стили на базе уже имеющегося):
<Style TargetType="ErrorIndicatorProj:ErrorIndicator"/>
и присвоить значения свойствам по средством механизма Setter. Для определения внешнего вида, необходимо присвоить значение свойству Template:
<Setter Property="Template">
 <Setter.Value>
  <ControlTemplate TargetType="ErrorIndicatorProj:ErrorIndicator">
   <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" Margin="{TemplateBinding Padding}" BorderThickness="{TemplateBinding BorderThickness}">
    <Grid>
     <ContentPresenter x:Name="PART_ContentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
     <Border x:Name="PART_OverlayBorder" Background="{TemplateBinding OverlayBrush}" Opacity="0" Visibility="Collapsed"/>
     <ContentPresenter x:Name="PART_ErrorPresenter" Content="{TemplateBinding ErrorContent}" ContentTemplate="{TemplateBinding ErrorContentTemplate}" Opacity="0" VerticalAlignment="Center" HorizontalAlignment="Center" Visibility="Collapsed"/>
    </Grid>
   </Border>
  </ControlTemplate>
 </Setter.Value> 
</Setter>
Шаблон состоит из 3-х главных частей (по соглашению все функциональные части представления должны иметь префикс “PART”) и элементов задающим общий вид компоненты:
  1. Строка 6 “PART_ContentPresenter” представляет содержимое, которое может породить ошибку, и которую необходимо сделать недоступным.
  2. Строка 7 “PART_OverlayBorder” отделяет элемент, который породил ошибку, от информации об ошибки.
  3. Строка 8 “PART_ErrorPresenter” показывает ошибку.
Необходимо заметить, что для первого и последнего можно определить шаблон “вида” (DataTemplate) содержимого. Все части используют свойства компонента, которому они принадлежат, посредством TemplateBinding. С помощью этого механизма можно получить значения свойств компонента “владельца” и присвоить их любой части внутри шаблона. Так, например, часть “PART_ContentPresenter” отображает значение свойства “Content” которое принадлежит классу ContentControl. Элемент ContentPresenter может показать любой тип, от строки до любого наследника UIElement. “PART_ContentPresenter” также использует свойство “ContentTemplate” для определения шаблона вывода содержимого. Используя тот же механизм, настраиваются остальные две функциональные части.
После того, как все части определены, можно описать состояния компонента, которые были задекларированы на уровне компонента:
<VisualStateManager.VisualStateGroups>
 <VisualStateGroup x:Name="ErrorStates">
  <VisualState x:Name="Error">
   <Storyboard>
    <DoubleAnimation Duration="0:0:0.2" To="1" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="PART_OverlayBorder" d:IsOptimized="True"/>
    <DoubleAnimation Duration="0:0:0.2" To="1" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="PART_ErrorPresenter" d:IsOptimized="True"/>
   </Storyboard>
  </VisualState>
  <VisualState x:Name="NoError">
   <Storyboard>
    <DoubleAnimation Duration="0:0:0.3" To="0" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="PART_OverlayBorder" d:IsOptimized="True"/>
    <DoubleAnimation Duration="0:0:0.3" To="0" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="PART_ErrorPresenter" d:IsOptimized="True"/>
   </Storyboard>
  </VisualState>
 </VisualStateGroup>
</VisualStateManager.VisualStateGroups>
Состояния, которые относятся к выводу ошибки, объединены в “ErrorStates” группу.
  1. Error - возникает, когда компонент находится в состоянии ошибки, т.е. необходимо спрятать элемент, который спровоцировал ошибку, и проинформировать пользователя.
  2. NoError - состояние по умолчанию, при котором пользователь может взаимодействовать с “тестируемым” компонентом.
Состояние “Error” устанавливает свойство Opacity для части PART_OverlayBorder в значение 1 за 200 мс. Это свойство устанавливается с таким же значением и для элемента PART_ErrorPresenter. Другими словами, просто делает видимыми части, которые описывают ошибку за 200 мс. Состояние NoError делает ту же работу, только наоборот, т.е. он скрывает части, которые показывают ошибку и делает доступным “тестируемый” элемент.
Для того, чтобы показать сообщение об ошибке в удобной форме, а не просто текст, описывается шаблон по умолчанию ErrorContentTemplate, который был определен в классе ErrorIndicator.
<Setter Property="ErrorContentTemplate">
 <Setter.Value>
  <DataTemplate>
   <Grid>
    <Grid.ColumnDefinitions>
     <ColumnDefinition MinWidth="16" Width="Auto"/>   
     <ColumnDefinition MinWidth="40" Width="*"/>   
    </Grid.ColumnDefinitions>
    <Image Source="/ErrorIndicatorProj;component/Assets/Alert.png" Width="32"/>   
    <ContentPresenter Content="{Binding }" VerticalAlignment="Center" Grid.Column="1" Margin="10,0,0,0"/>   
   </Grid>
  </DataTemplate>
 </Setter.Value>
</Setter>
Помимо “голой" информации об ошибке, пользователь увидит пиктограмму. Шаблон вывода ошибки можно переопределить в клиентском коде.
Исходный код можно взять здесь.

воскресенье, 13 марта 2011 г.

Реактивное тестирование

Rx принес некоторые интересные проблемы в сообщество TDD, ибо производить тестирование асинхронной модели задача не из простых, и требует иной подход нежели тестирование синхронного кода. В данной статье будут рассмотрены способы тестирования Rx модели.

Запись событий и анализ

Класс MockObserver<> записывает данные которые "выталкивает" Observable в плоскую коллекцию. Данная коллекция содержит элементы в той последовательности, с которой они были записаны. Кроме того, элементы содержат временной штамп, который показывает в какой момент времени он был помещен в коллекцию. У каждого события есть "вид". Их всего три Next, Complete, Error. Все эти свойства дают крепкий фундамент для анализа результата. Рассмотрим несколько примеров:
[Test]
public void Subscribe_should_push_3_values_and_complete()
{
var observable = Observable.Create<int>(x =>
{
x.OnNext(1);
x.OnNext(2);
x.OnNext(3);
x.OnCompleted();
return () => { };
});

var testScheduler = new TestScheduler();
var mockObserver = new MockObserver<int>(testScheduler);

observable.Subscribe(mockObserver);

Assert.That(mockObserver[0].Value.Value, Is.EqualTo(1));
Assert.That(mockObserver[0].Value.Kind, Is.EqualTo(NotificationKind.OnNext));

Assert.That(mockObserver[1].Value.Value, Is.EqualTo(2));
Assert.That(mockObserver[1].Value.Kind, Is.EqualTo(NotificationKind.OnNext));

Assert.That(mockObserver[2].Value.Value, Is.EqualTo(3));
Assert.That(mockObserver[2].Value.Kind, Is.EqualTo(NotificationKind.OnNext));

Assert.That(mockObserver[3].Value.Kind, Is.EqualTo(NotificationKind.OnCompleted));
}
Hа 4 строке создается последовательность из 3-х целых чисел после чего последовательность завершается. На 14 строке создается экземпляр MockObserver<>, которому в качестве аргумента конструктора передан экземпляр класса TestScheduler. Данный класс является реализацией интерфейса IScheduler и необходим для эмуляции планировщика. Далее он будет рассмотрен отдельным пунктом. На строке 16 происходит подписка и запись результата. Далее идет анализ результата. Класс MockObserver<> позволяет с помощью индекса получить доступ к нужной "записи" (Recorded<>) события и проанализировать его. Данный класс предоставляет два важныx свойства:
  • Time - смещение от момента подписки до возникновение события в тиках
  • Value - источник события
Источник события представляет абстрактный класс Notification<> и его три реализации OnCompleted, OnError, OnNext. Базовый класс также имеет два важный свойства:
  • Kind - перечисление рода события OnNext/OnError/OnCompleted
  • Value - значение, переданное в качестве аргумента события.
Рассмотрим другой пример:
[Test]
public void Subscribe_should_push_value_with_delay_and_complete()
{
var testScheduler = new TestScheduler();
var mockObserver = new MockObserver<int>(testScheduler);

var observable = Observable.Return(42);//Answers to basic questions of life, the universe and everything else :)

observable
.Delay(TimeSpan.FromMilliseconds(1), testScheduler)
.Subscribe(mockObserver);

testScheduler.Run();

Assert.That(mockObserver[0].Value.Value, Is.EqualTo(42));
Assert.That(mockObserver[0].Time, Is.EqualTo(10000));//tiks per milliseconds
Assert.That(mockObserver[0].Value.Kind, Is.EqualTo(NotificationKind.OnNext));

Assert.That(mockObserver[1].Value.Kind, Is.EqualTo(NotificationKind.OnCompleted));
}
После инициализации observable на 7 строке, перед подпиской выполняется пауза в одну миллисекунду, прежде чем "вытолкнется" значение. Затем запускается планировщик (см. ниже зачем это нужно) и производится тестирование результатов. Тут все как и в прошлом примере, за исключением свойства "Time". В данном свойстве и содержится значение задержки после подписки и перед получением значения. Напоминаю, что значение свойства "Time" в тиках.

Планировщик TestScheduler

Планировщик является неотъемлемой частью Rx, непонимание его принципов ведет к трудноуловимым ошибкам и Deadlock'ам. Планировщик выполняет работу кода определенным способом. Всего существует 7 реализаций планировщика, это:
  • Scheduler.Dispatcher выполняет работу в текущем "Dispatcher", без которого не обойтись в приложениях Silverlight и WPF. Другими словами, реализация этого планировщика - это просто делегирование кода в "Dispatcher" (Dispatcher.BeginInvoke(Action)).
  • Scheduler.NewThread выполняет работу в новом потоке.
  • Scheduler.ThreadPool выполняет работу в пуле потоков.
  • Scheduler.TaskPool выполняет работу в пуле задач.
  • Scheduler.Immediate немедленно выполняет работу в текущем потоке.
  • Scheduler.CurrentThread выполняет работу в текущем потоке. Разница между Immediate в том, что CurrentThread ставит работу в очередь выполнения.
  • TestScheduler эмулирует работу планировщика.
Планировщики можно разделить на два вида их применения:
  1. Выполнение работы подписки (SubscribeOn())
  2. Публикация результатов работы (ObserveOn())
TestScheduler применяет концепцию виртуального планирования и позволяет контролировать и управлять временем при выполнении операции. Концепцию виртуального планирования можно представить в виде очереди операций, которые необходимо выполнить в определенное время. Если операции запланирована на одно и тоже время, они будут выполнены последовательно и помечены одинаковым временным штампом. При использовании TestSheduler, можно приказать ему выполнить все запланированные операции TestSheduler.Run(), или выполнить операции до определенного момента времени, которое передается в качестве аргумента методу TestSheduler.RunTo(TimeInTicks). В предыдущем примере планировщику приказано выполнить все операции (строка 13) не дожидаясь задержки в 1 миллисекунду, причем в записи будет указано, что операция выполнилась ровно через одну миллисекунду после подписки. Рассмотрим пример, где запланированные операции выполняются не все сразу, а до определенного момента времени:
var scheduler = new TestScheduler();
var oneMinentsTiks = scheduler.FromTimeSpan(TimeSpan.FromMinutes(1));
var twoMinentsTiks = scheduler.FromTimeSpan(TimeSpan.FromMinutes(2));

scheduler.Schedule(() => Debug.WriteLine("1"), oneMinentsTiks);
scheduler.Schedule(() => Debug.WriteLine("2"), oneMinentsTiks);
scheduler.Schedule(() => Debug.WriteLine("3"), twoMinentsTiks);
scheduler.Schedule(() => Debug.WriteLine("4"), twoMinentsTiks);
Debug.WriteLine("RunTo(oneMinentsTiks)");
scheduler.RunTo(oneMinentsTiks);
Debug.WriteLine("Run()");
scheduler.Run();
После того, как планировщик создан, идет “планирование” выполнения двух операций на 4-м тике, затем еще две операции на 5 тике. Таким образом последняя операция выполнится, когда на часах будет 5 тиков. Затем на строке 10 планировщик выполняет операции, которые должны быть выполнены до 4-го тика, а затем и все запланированные. Результат выполнения этого кода представлен ниже:
/* Output:
RunTo(oneMinentsTiks)
1
2
Run()
3
4
*/
Далее рассмотрим более жизненный пример. Необходимо запрашивать данные с сервера через каждых 15 мин. на протяжении 2-х часов. Понятно, что при тестировании не хочется получить результат теста через два часа, а сразу.
[TestFixture]
public class TestSchedulerTest
{
[Test]
public void Subscrible_should_push_values_for_two_h_with_interval_15_min_and_complete()
{
var scheduler = new TestScheduler();
var pushCollection = Observable.Interval(TimeSpan.FromMinutes(15), scheduler)
.SelectMany(x=> Observable.Return(String.Format("NextData from Server #{0}", x)))
.Take(8);

mockObserver = new MockObserver<string>(scheduler);

pushCollection.Subscribe(mockObserver);
scheduler.Run();
}
}
На моей машине тест отработал за 0:00:00.26, а результат представлен ниже:
На заметку. После знака @ (at) показано время, когда было вытолкнуто значение (((9000000000 tic/10000)msec)/1000)sec/60)min = 15 мин.

HotObservable/ColdObservable

Данные классы позволяют записать события, которые Observable будет “выталкивать”. Событие включает в себя время возникновения и значение. Классы HotObservable/ColdObservable ведут себя ожидаемым образом исходя из их названия. Первый начинает выталкивать значения независимо от подписки, второй ждет подписку и начитает публиковать значения. Рассмотрим пример использования HotObservable:
[Test]
public void HotObservable_should_push_values_without_subscrible()
{
var scheduler = new TestScheduler();
var mockObserver = new MockObserver<int>(scheduler);

var hotObservable = new HotObservable<int>(scheduler,
new Recorded<Notification<int>>(1, new Notification<int>.OnNext(1)),
new Recorded<Notification<int>>(2, new Notification<int>.OnNext(2)),
new Recorded<Notification<int>>(3, new Notification<int>.OnNext(3)),
new Recorded<Notification<int>>(4, new Notification<int>.OnNext(4)),
new Recorded<Notification<int>>(5, new Notification<int>.OnNext(5)),
new Recorded<Notification<int>>(6, new Notification<int>.OnNext(6)),
new Recorded<Notification<int>>(7, new Notification<int>.OnNext(7)),
new Recorded<Notification<int>>(8, new Notification<int>.OnNext(8)),
new Recorded<Notification<int>>(9, new Notification<int>.OnNext(9)),
new Recorded<Notification<int>>(10, new Notification<int>.OnCompleted())
);

scheduler.RunTo(4);
hotObservable.Subscribe(mockObserver);
scheduler.Run();

Assert.That(mockObserver.Count, Is.EqualTo(6));
Assert.That(mockObserver[0].Time, Is.EqualTo(5));

Assert.That(mockObserver[5].Value.Kind, Is.EqualTo(NotificationKind.OnCompleted));
}
На строке 7 создается HotObservable и записывает туда последовательность событий с точным указанием времени “выталкивания”. Например, первое событие будет опубликовано в 1 тик времени со значением 1. После записи последовательности, запускается выполнение до 4-го тика и выполняется подписка. В результирующей коллекции оказалось 6 значений из 10, т.к. первые четыре опубликовались “в никуда”.
Результат:

Во-втором примере все точно также:
[Test]
public void ColdObservable_should_push_values_upon_subscrible()
{
var scheduler = new TestScheduler();
var mockObserver = new MockObserver<int>(scheduler);

ar hotObservable = new ColdObservable<int>(scheduler,
new Recorded<Notification<int>>(1, new Notification<int>.OnNext(1)),
new Recorded<Notification<int>>(2, new Notification<int>.OnNext(2)),
new Recorded<Notification<int>>(3, new Notification<int>.OnNext(3)),
new Recorded<Notification<int>>(4, new Notification<int>.OnNext(4)),
new Recorded<Notification<int>>(5, new Notification<int>.OnNext(5)),
new Recorded<Notification<int>>(6, new Notification<int>.OnNext(6)),
new Recorded<Notification<int>>(7, new Notification<int>.OnNext(7)),
new Recorded<Notification<int>>(8, new Notification<int>.OnNext(8)),
new Recorded<Notification<int>>(9, new Notification<int>.OnNext(9)),
new Recorded<Notification<int>>(10, new Notification<int>.OnCompleted())
);

scheduler.RunTo(4);
hotObservable.Subscribe(mockObserver);
scheduler.Run();

Assert.That(mockObserver.Count, Is.EqualTo(10));
Assert.That(mockObserver[0].Time, Is.EqualTo(1));

Assert.That(mockObserver[9].Value.Kind, Is.EqualTo(NotificationKind.OnCompleted));
}
за исключением того, что не зависимо от времени, при каждой подписке будет публиковаться вся коллекция полностью.
Результат:
Для более удобного использования HotObservable/ColdObservable существыют ExtensionsMethods для TestScheduler:
public static class TestSchedulerExtensions
{
   public static ColdObservable<T> CreateColdObservable<T>(this TestScheduler scheduler, params Recorded<Notification<T>>[] messages);
   public static HotObservable<T> CreateHotObservable<T>(this TestScheduler scheduler, params Recorded<Notification<T>>[] messages);
}
Все классы, рассмотренные выше, находятся в сборке System.Reactive.Testing.dll, которая поставляется вместе с Reactive Extensions v1.0.2856.104. В данной сборке есть еще много сюрпризов интересных методов, например, проверка на равенство двух Observable. Но это уже для самостоятельного исследования.... и рефлектор в помощь ;)

Помните:
"Documentation is like sex. If it is good, it is REALLY good, and if it is bad, it is better than nothing!" Gabe Helou

среда, 23 февраля 2011 г.

Ликбез. Свойства зависимостей (DependencyProperty).

Свойства зависимостей являются совершенно новым воплощением свойств. Без них невозможно работать с основными средствами WPF/Silverlight, такими как анимация/раскадровка, привязка данных и стили. В концептуальном отношении поведение свойств зависимостей не отличается от поведения обычных свойств CLR (common language runtime), однако “за кулисами” совершенно иная модель. Причина проста  - производительность. Обычные свойства не могут поддержать всех характеристик свойств зависимостей без приличной нагрузки на систему.
В свойствах зависимостей можно выделить несколько ключевых моментов:
  • Проверка достоверности значений;
  • Приоритезация;
  • Наследование поведения;
  • Совместное использование свойства зависимостей.

Проверка достоверности и коррекция значений

Проверка достоверности значений разделяется на три этапа:
  • Приведение
  • Проверка
  • Обратный вызов

Проверка достоверности

При регистрации DP можно указать обратный вызов проверки. Данный метод позволяет произвести проверку, которая обычно добавляется в установочную часть процедуры свойства. Данный метод должен принимать объектный параметр и возвращать булево значение. Данный метод проверки имеет одно ограничение: он является статическим, что не позволяет учесть в проверке состояние объекта. Эту проблему лучше решать с помощью Приведения
public static readonly DependencyProperty ValueProperty =
   DependencyProperty.Register("Value", typeof (double), typeof (MainWindow),
    new FrameworkPropertyMetadata(0), ValidateValueCallback);

private static bool ValidateValueCallback(object value)
{
 throw new NotImplementedException();
}

Приведения

Приведения – это действие, которое происходит перед самой проверкой, и позволяет менять значение (обычно для согласования с остальными свойствами) или отклонить изменения (DependencyProperty.UnsetValue)
public static readonly DependencyProperty ValueProperty =
   DependencyProperty.Register("Value", typeof (double), typeof (MainWindow),
    new FrameworkPropertyMetadata(0, PropertyChangedCallback, CoerceValueCallback));

private static object CoerceValueCallback(DependencyObject sender, object baseValue)
{
 throw new NotImplementedException();
}

Обратный вызов

После того как оба предыдущих метода успешно отработали, начинает действовать обратный вызов, который может поднять событие изменения или среагировать на изменение состояния.
public static readonly DependencyProperty ValueProperty =
   DependencyProperty.Register("Value", typeof (double), typeof (MainWindow),
    new FrameworkPropertyMetadata(0, PropertyChangedCallback, CoerceValueCallback));

private static void PropertyChangedCallback(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
 throw new NotImplementedException();
}

Приоритезация

Следующей особенностью является динамическое разрешение значений. Это значит что при получении значения из свойства, будет учтено несколько факторов. Например, некоторое значение должно попадать в заданном промежутке между минимальным и максимальным и при необходимости может быть автоматически исправлено в методе Приведения. Т.е. данное свойство зависит от множества поставщиков значений, который имеет свой уровень приоритета. Наивысший приоритет имеет анимация, а самый низкий – значение по умолчанию.
  1. Анимации. Для достижения желаемого эффекта, анимация должна иметь возможность затереть базовое (не анимированное) значение, даже если значение установлено локально.
  2. Локальное значение. Локальные значения присваивается через модель свойств CLR, что приравнивается к присваиванию значения атрибуту в XAML. К данному уровню приоритета относится позднее связывание или ресурсы.
  3. Свойства шаблонов. Значения свойств шаблона, если элемент был создан как часть шаблона.(ControlTemplate или DataTemplate).
  4. Стили. Значения, которые присваиваются при помощи механизма Setter.
  5. Значение по умолчанию. Значение, которое было присвоено свойству при его регистрации.

Наследование поведения

Наследование поведения – это механизм, при котором значения свойств родителя наследуют дочерние элементы. Данным поведением обладает свойство FontSize или FontFamily. Например, если панели задать размер шрифта 14pt, то всем дочерним элементам на данной панели, будет присвоено значение размера шрифта 14pt. Чтобы заставить это работать, необходимо при регистрации свойства передать экземпляр класса FrameworkPropertyMetadata, в котором необходимо указать одну или несколько установок параметров метаданных (в данном случае FrameworkPropertyMetadataOptions.Inherits).
public static readonly DependencyProperty FontSizeProperty =
   DependencyProperty.Register("FontSize", typeof (double), typeof (MainWindow),
    new FrameworkPropertyMetadata(12,FrameworkPropertyMetadataOptions.Inherits));

Совместное использование свойства зависимостей

Некоторые классы совместно используют одно и то же свойства зависимостей, даже если они имеют резные иерархии классов. Например, TextBlock.FontFamily и Control. FontFamily указывают на одно и то же статическое свойство зависимостей, которое определенно в классе TextElement. В классе TextElement регистрируется свойство, а в классах TextBlock и Control просто повторно используют его посредством вызова метода DependencyProperty.AddOwner():
FontSizeProperty = TextElement.FontSizeProperty.AddOwner(typeof (MainWindow));
Таким образом, элемент получает свойство «задаром».

Вместо заключения

Одно из преимуществ этой системы в том, что она является очень экономичной. Если значение свойства не было задано локально, система извлекает его значения из стиля другого элемент, или выбирает значение, заданное по умолчанию. При этом не надо выделать память! для хранения значений.

понедельник, 7 февраля 2011 г.

iOS Silverlight ListControl

Не так давно закончил еще один интересный компонент из списка iOS :)
Данный компонент предназначен для прокрутки длинных списков. "Прибить" можно любые данные и описать их с помощью соответствующих шаблонов. Компонент еще "сырой", есть ошибки, поэтому используйте на свой страх и риск.
Пример с двумя вариантами использования ниже.


IPhone toggle button
 
Исходный код можно слить отсюда

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

iOS Silverlight theme

Стиль iOS очень продуман и очень не плох. Немного подумав, решил реализовать наиболее интересные контролы этой операционной системы в Silverlight. Ниже представлен “Переключатель” в стиле IPhone.

IPhone toggle button
Загрузить исходный код можно здесь.
Новые компоненты буду выкладывать по мере реализации.

пятница, 17 декабря 2010 г.

Autofac extensions for PRISM 4

PRISM
Prism это набор практик , для более простого проектирование богатых, гибких и легких в обслуживании Windows Presentation Foundation (WPF), десктоп приложений, Silverlight Rich Internet Applications (RIA), и Windows 7 Phone приложений. Использование паттернов, воплощают важные архитектурные принципы дизайна, такие как разделение ответственности и слабую связь. Prism позволяет проектировать и строить приложения с использованием слабо связанных компонентов, которые могут развиваться самостоятельно, но которые могут быть легко интегрированы. Такой тип приложений известен как «Модульные приложения».
Последняя версия Prism направленна на Microsoft. NET Framework 4.0 и Silverlight 4 и включает в себя новый принцип реализации модели Model-View-ViewModel (MVVM) паттерна, навигации и Managed Extensibility Framework (MEF).

Подробно.

Autofac
Autofac это IoC контейнер для Microsoft .NET. Он управляет зависимостями между классами таким образом, чтобы приложения оставалось легко изменяемым по мере роста  размера и сложности. Это достигается путем обработки обычных классов в виде компонентов.
Последняя версия (2.3) распространяется в виде одной dll’ки размером около 100KB.
Подробно.

Enable Autofac in PRISM
Контейнер Инъекции Зависимостей (Dependency injection containers), дальше просто "контейнер" используются для удовлетворения зависимостей между компонентами; удовлетворение таких зависимостей, как правило, предполагает регистрацию и разрешение компонентов. Из коробки призм обеспечивает поддержку Unity контейнер и Mef, но он является независимым от конкретной реализации контейнера. Поскольку библиотека получает доступ к контейнеру через интерфейс IServiceLocator, он может быть заменен. Для этого, ваш контейнер должен реализовать  интерфейс IServiceLocator. Обычно, если вы заменяете контейнер, вы должны также предоставить собственный загрузчик приложения (Bootstrapper) который зависит от реализации контейнера. Какой контейнер использовать – дело личных предпочтений. Я предпочитаю использовать Autofac, поэтому для того чтобы связать его и Prism, необходимо написать собственную реализацию Bootstrapper и IServiceLocator.
Prism очень гибок и расширяем, что позволяет его «заточить» под решение конкретной задачи. При этом архитектура приложения будет «правильной», т.е. готова к изменениям на любом уровне и без последствий (ну или с минимальными). Что еще не маловажно, Prism помогает писать тестируемые приложения с помощью UnitTest’ов.

AutofacBootstrapper
AutofacBootstrapper – реализация абстрактного класса Bootstrapper. Данный класс обеспечивает последовательность начальной загрузки приложения, и регистрирует необходимые компоненты в контейнере Autofac. Реализация ключевых методов класса AutofacBootstrapper выглядит сл. образом:
 
 public abstract class AutofacBootstrapper : Bootstrapper
 {
  
  protected virtual IContainer CreateContainer()
  {
   return ConfigureContainer().Build();
  }

  protected virtual ContainerBuilder ConfigureContainer()
  {
    var containerBuilder = new ContainerBuilder();

    containerBuilder.RegisterInstance(Logger);
    containerBuilder.RegisterInstance(ModuleCatalog);
    containerBuilder.RegisterSource(new ResolveAnythingRegistrationSource());

    if (_useDefaultConfiguration)
    {
      containerBuilder.Register(x => new AutofacServiceLocatorAdapter(Container))
      .As<IServiceLocator>().SingleInstance();
   
      containerBuilder.RegisterType<ModuleInitializer>()
      .As<IModuleInitializer>().SingleInstance();
   
      containerBuilder.RegisterType<ModuleManager>().
      As<IModuleManager>().SingleInstance();
   
      containerBuilder.RegisterType<EventAggregator>()
      .As<IEventAggregator>().SingleInstance();

      containerBuilder.RegisterModule(new RegionInitializerModule());
      containerBuilder.RegisterModule(new RegionAdapterInitializerModule());
    }

    return containerBuilder;
  }

  public override void Run(bool runWithDefaultConfiguration)
  {
   //run workflow
  }
 }

Метод “ConfigureContainer” конфигурирует контейнер и возвращает объект, «ContainerBuilder» который, после операции “Build” превратится в рабочий контейнер. В обязательном порядке, контейнер регистрируются “Logger” и “ModuleCatalog”. В контейнер они регистрируются как созданные и сконфигурированные объекты, потому при инъекциях, не будут создаваться новые, а будут использоваться те, что использовались при регистрации. Если приложение использует конфигурацию по умолчанию, в контейнер регистрируются следующие компоненты:
•    AutofacServiceLocatorAdapter - адаптер контейнера, для абстракции от конкретной реализации контейнера
•    ModuleInitializer – компонент, который создает экземпляр загружаемого модуля и инициализирует его.
•    ModuleManager – компонент, который производит загрузку удаленного модуля и зависимостей. После загрузки, модуль инициализируется при помощи компонента “ModuleInitializer”.
•    EventAggregator – компонент, который реализует одноименный паттерн.
•    RegionInitializerModule – класс, который производит настройку компонентов для работы с регионами.

Также в контейнер регистрируется компонент “ResolveAnythingRegistrationSource”, который расширяет возможности Autofac способностью создавать типы не зарегистрированые в нем непосредственно. Это необходимо для создания экземпляров модулей, которые описаны с помощью структуры «ModuleInfo», которая обеспечивает позднее связывание.
Это метод необходимо перегрузить для регистрации собственных компонентов. Ниже представлен пример из кода “Quickstarts- Modularity”:
protected override ContainerBuilder ConfigureContainer()
{
 var containerBuilder = base.ConfigureContainer();

 containerBuilder.RegisterType()
                 .As().SingleInstance();
 containerBuilder.RegisterInstance(callbackLogger);
 containerBuilder.RegisterType();

 return containerBuilder;
}
Метод «CreateContainer» создает контейнер из сконфигурированного «СontainerBuilder». Практическт во всех случаях его не нужно переопределять.

Метод «Run» запускает процесс инициализации приложения в строго определенной последовательности.


Последовательность инициализиции компонентов

 
Данную последовательность необходимо придерживать при реализации собственного метода.
Параметр, передаваемый в метод, регулирует процесс инициализиции. Если  метод был вызван с параметром «True», то нициализация пройдет в обычном режиме и будут проинициализированы и зарегистрированы все необходимые компоненты. Иначе, класс наследник обязывается провести инициализацию этих компонентов самостоятельно. В большинстве случаев нет необходимости менять логику работы этого метода.

AutofacServiceLocatorAdapter
Определяет адаптер для интерфейса IServiceLocator, которые будут использоваться Composite Application библиотеки. Интерфейс IServiceLocator находится  в  Common Service Locator библиотеке. Эта библиотека с открытым исходным кодом, предназначена для того, чтобы обеспечить абстракцию от IoC (Inversion of Control) контейнера, и сервис локатора (service locator). Цель использования этой библиотеки заключается в привлечении IoC и Service Location без привязки к конкретной реализации. Prism не ссылается или полагаться на конкретный контейнер, но приложение может полагаться на конкретный контейнер. Это означает, что целесообразно для конкретного приложения ссылаться на контейнер, но Prism этого не делает непосредственно.

RegionInitializerModule
Данный модуль использоваться для расслоения на множество связанных с ним компонентов для упрощения настройки и развертывания. Модуль предоставляет преднамеренно ограниченный набор конфигурационных параметров, которые могут изменяться независимо от компонентов, используемых для реализации модуля.

ResolveAnythingRegistrationSource
Данное расширание к Autofac позволяет решить проблему инстенцирования компонентов,  которые не были зарегистрированиы в контейнер во время инициализации приложения. Это нужно в момент загрузки и инициализации распределенных модулей приложения. Необходимо отметить, что его использование для инстенцеирование заранее известных компонентов не рекомендуется, так как скорость поиска объектов в контейнере снижается.

Source code

суббота, 9 октября 2010 г.

Rhino Mocks 3.6 for Silverlight


Порт популярной библиотеки (Rhino Mocks 3.6) для создания Mock объектов на платформу Silverlight. Странно что этого не сделал автор библиотеки. Скачать можно здесь.


Update: Core 2.5.1 - September 21st, 2010, исправлены ошибки портирования

Older Posts