суббота, 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
 
Исходный код можно слить отсюда

Newer Posts Older Posts