Роль свойств зависимости

69

Прежде чем вы узнаете, как строить специальное свойство зависимости, давайте взглянем на внутреннюю реализацию свойства Height класса FrameworkElement. Соответствующий код показан ниже (с комментариями); тот же код можно просмотреть самостоятельно с помощью утилиты reflector.exe:

// FrameworkElement "является" DependencyObject.
public class FrameworkElement : UIElement, IFrameworkInputElement, IInputElement, ISupportInitialize, IHaveResources, IQueryAmbient
{
   // Статическое свойство только для чтения DependencyProperty.
   public static readonly DependencyProperty HeightProperty;
   
   // Поле DependencyProperty часто регистрируется в статическом конструкторе класса.
   static FrameworkElement()
   {
      HeightProperty = DependencyProperty.Register (
      "Height",
      typeof(double),
      typeof(FrameworkElement),
      new FrameworkPropertyMetadata((double) 1.0 / (double) 0.0,
         FrameworkPropertyMetadataOptions.AffectsMeasure,
         new PropertyChangedCallback(FrameworkElement.OnTransformDirty)),
      new ValidateValueCallback(FrameworkElement.IsWidthHeightValid));
   }
   
   // Оболочка CLR, реализованная унаследованными
   // методами GetValue()/SetValue().
   public double Height
   {
      get { return (double)base.GetValue(HeightProperty); }
      set { base.SetValue(HeightProperty, value); }
   }
}

Как видите, свойства зависимости требуют порядочного дополнительного кода по сравнению с нормальным свойством CLR. На самом деле зависимость может оказаться еще более сложной, чем здесь показано.

Прежде всего, помните, что если класс желает определить свойство зависимости, он должен иметь DependencyObject в своей цепочке наследования, поскольку в этом классе определены методы GetValue() и SetValue(), используемые оболочкой CLR. Поскольку FrameworkElement "является" DependencyObject, это требование удовлетворено.

Далее вспомните, что сущность, которая будет хранить действительное значение свойства (в случае Height это double), представлено как общедоступное, статическое, предназначенное только для чтения поле типа DependencyProperty. В соответствии с существующим соглашением имя этого свойства всегда должно быть снабжено суффиксом Property, добавленным к имени связанной оболочки CLR.

Учитывая, что свойства зависимости объявлены как статические поля, они обычно создаются (и регистрируются) внутри статического конструктора класса. Объект DependencyProperty создается вызовом статического метода DependencyProperty.Register(). Этот метод многократно переопределен.

Первый аргумент DependencyProperty.Register() —это имя нормального свойства CLR класса (в данном случае Height), в то время как второй аргумент несет информацию о лежащем в основе типе инкапсулированных данных (double). Третий аргумент указывает информацию о типе класса, к которому относится это свойство (в данном случае — FrameworkElement). Хотя это может показаться избыточным (в конце концов, поле HeightProperty уже определено внутри класса FrameworkElement), это очень разумный аспект WPF, который позволяет одному классу регистрировать свойства на другом (даже если определение класса запечатано (sealed)).

Четвертый аргумент, переданный DependencyProperty.Register() в данном примере — это то, что на самом деле обеспечивает свойствам зависимости их уникальные характеристики. Здесь передается объект FrameworkPropertyMetadata, описывающий различные детали о том, как среда WPF должна обрабатывать это свойство в отношении уведомлений обратного вызова (если свойство должно извещать других об изменениях своего значения) и различные опции (представленные перечислением FrameworkPropertyMetadataOptions). Значения FrameworkPropertyMetadataOptions управляют тем, что именно затрагивается данным свойством (работает ли оно с привязкой данных, может ли наследоваться, и т.п.). В данном случае аргументы конструктора FrameworkPropertyMetadata описываются следующим образом:

new FrameworkPropertyMetadata(
   // Значение свойства по умолчанию.
   (double)0.0,
   // Опции метаданных.
   FrameworkPropertyMetadataOptions.AffectsMeasure,
   // Делегат, указывающий на свойство, вызываемое при изменении свойства.
   new PropertyChangedCallback(FrameworkElement.OnTransformDirty)
)

Поскольку финальный аргумент конструктора FrameworkPropertyMetadata является делегатом, обратите внимание, что этот параметр конструктора указывает на статический метод класса FrameworkElement по имени OnTransformDirty(). Хотя код этого метода подробно рассматриваться не будет, имейте в виду, что всякий раз, когда строится специальное свойство зависимости, можно специфицировать делегат PropertyChangeCallback для указания на метод, который будет вызван, когда изменяется значение свойства.

Это приводит к финальному параметру, переданному в метод DependencyProperty.Register() — второму делегату типа ValidateValueCallback, который указывает на метод класса FrameworkElement, вызываемый для проверки достоверности значения, присвоенного свойству. Этот метод содержит логику, которую можно ожидать найти в блоке set свойства.

Как только объект DependencyProperty зарегистрирован, остается решить последнюю задачу — поместить поле в оболочку обычного свойства CLR (в данном случае — Height). Однако обратите внимание, что блоки get и set не просто возвращают или устанавливают значение double переменной-члена класса, но делают это непрямо, используя методы GetValue() и SetValue() из базового класса System.Windows.DependencyObject:

public double Height
   {
      get { return (double)base.GetValue(HeightProperty); }
      set { base.SetValue(HeightProperty, value); }
   }

Важные замечания относительно оболочек свойств CLR

Резюмируя сказанное выше, отметим, что свойства зависимости выглядят как обычные нормальные свойства, когда вы получаете или устанавливаете их значения в XAML-разметке или в коде, но "за кулисами" они реализованы с применением намного более изощренной техники кодирования. Помните, что основная причина для прохождения этого процесса состоит в построении специального элемента управления, имеющего специальные свойства, которые должны быть интегрированы со службами WPF, требующими взаимодействия со свойством зависимости (например, анимацией, привязкой данных и стилями).

Несмотря на то что часть реализации свойства зависимости включает определение оболочки CLR, вы никогда не должны помещать логику проверки достоверности в блок set. И потому оболочка CLR свойства зависимости никогда не должна делать ничего помимо вызовов GetValue() или SetValue().

Причина в том, что исполняющая среда WPF сконструирована таким образом, что в случае написания XAML-разметки, устанавливающей свойство, как показано ниже:

<Button х:Name="myButton" Height="100" .../>

исполняющая среда полностью минует блок set свойства Height и непосредственно вызывает SetValue()! Причина столь странного поведения кроется в оптимизации. Если бы исполняющей среде WPF пришлось непосредственно вызывать блок set свойства Height, то ей нужно было бы выполнить рефлексию времени выполнения для нахождения поля DependencyProperty (указанного в первом аргументе SetValue()), обращаться к нему в памяти, и т.д.

Но раз так, то зачем вообще строить оболочку CLR? Дело в том, что XAML в WPF не позволяет вызывать функции в разметке, так что следующий фрагмент был бы ошибочным:

<!-- Ошибка1 Вызывать методы в XAML-разметке для WPF нельзя! -->
<Button х:Name="myButton" this.SetValue("100") .../>

Установка или получение значения в разметке с использованием оболочки CLR должна восприниматься как способ указать исполняющей среде WPF о необходимости вызвать GetValue()/SetValue(), поскольку напрямую это делать в разметке не разрешается.

Что произойдет, если вызвать оболочку CLR в коде следующим образом:

Button b = new Button();
b.Height =10;

В этом случае, если бы блок set свойства Height содержал какой-то код помимо вызова SetValue(), он должен был бы выполняться, так как оптимизация анализатора WPF XAML не вызывается. Краткий ответ может быть сформулирован так: когда вы регистрируете свойство зависимости, применяйте делегат ValidateValueCallback для указания на метод, выполняющий проверку достоверности данных. Это гарантирует правильное поведение, независимо от того, используется XAML или код для получения/установки свойства зависимости.

Пройди тесты
Лучший чат для C# программистов