Пользовательские свойства зависимостей

В этом разделе описываются основания для создания настраиваемых свойств зависимостей для приложений Windows Presentation Foundation (WPF), а также этапы и некоторые варианты реализации, которые могут повысить производительность, удобство использования и универсальность свойств.

Необходимые компоненты

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

Что такое свойство зависимостей?

Реализовать поведение, аналогичное свойству среды выполнения (CLR), для поддержки стилизации, привязки данных, наследования, анимации и значений по умолчанию можно путем реализации его как свойства зависимостей. Свойства зависимостей — это свойства, которые регистрируются в системе свойств WPF путем вызова метода Register (или RegisterReadOnly) и снабжаются полем идентификатора DependencyProperty. Свойства зависимостей могут использоваться только типами DependencyObject, однако тип DependencyObject находится достаточно высоко в иерархии классов WPF, поэтому большинство классов, доступных в WPF, могут поддерживать свойства зависимостей. Дополнительные сведения о свойствах зависимостей и некоторых терминах и соглашениях, используемых для их описания в этом пакете SDK, см. в разделе Общие сведения о свойствах зависимостей.

Примеры свойств зависимостей

Примеры свойств зависимостей, реализуемых в классах WPF, включают среди прочих свойства: Background, Width и Text. Каждое свойство зависимости, предоставленное классом, имеет соответствующее открытое статическое поле типа DependencyProperty, предоставленное в том же классе. Это идентификатор для свойства зависимостей. Идентификатор именуется по следующему соглашению: имя свойства зависимостей, за которым следует строка Property. Например, полем идентификатора DependencyProperty, соответствующим свойству Background, является BackgroundProperty. Идентификатор хранит сведения о свойстве зависимостей, как оно было зарегистрировано. Он затем используется для других операций, в которых участвует свойство зависимостей, например для вызова SetValue.

Как упоминалось в разделе Общие сведения о свойствах зависимостей, все свойства зависимостей в WPF (за исключением наиболее присоединенных свойств) также являются свойствами CLR из-за реализации "оболочки". Таким образом, из кода можно получать или задавать свойства зависимостей путем вызова методов доступа CLR, определяющих оболочки таким же образом, как и для других свойств CLR. Как потребитель созданных свойств зависимостей, вы обычно не используете методы DependencyObjectGetValue и SetValue, являющиеся точками подключения к базовой системе свойств. Вместо этого существующая реализация свойств CLR автоматически вызывает методы GetValue и SetValue в реализации оболочки get и set свойства, соответствующим образом используя поле идентификатора. При собственной реализации настраиваемого свойства зависимостей вы будете определять оболочку аналогичным образом.

Почему требуется реализовывать свойство зависимостей?

При реализации свойства в классе (при условии что класс является производным от DependencyObject) вы можете снабдить свое свойство идентификатором DependencyProperty и, таким образом, определить его как свойство зависимостей. Назначение свойства свойством зависимостей не всегда требуется и не всегда уместно и зависит от конкретной ситуации. В некоторых случаях достаточно просто снабдить свойство закрытым полем. Тем не менее реализовать свойство как свойство зависимостей потребуется, если свойство должно поддерживать одну или несколько из следующих возможностей WPF.

  • Требуется, чтобы свойство было задаваемым в стиле. Более подробную информацию см. в разделе Стилизация и использование шаблонов.

  • Требуется, чтобы свойство поддерживало привязку данных. Дополнительные сведения о свойствах зависимостей привязки данных см. в разделе Привязка свойств двух элементов управления.

  • Требуется, чтобы свойство было задаваемым с помощью динамической ссылки ресурса. Дополнительные сведения см. в разделе Ресурсы XAML.

  • Требуется настроить автоматическое наследование значения свойства из родительского элемента в дереве элементов. В этом случае регистрацию следует выполнять с помощью метода RegisterAttached, даже при создании оболочки свойства для доступа CLR. Дополнительные сведения см. в разделе Наследование значения свойства.

  • Требуется, чтобы свойство поддерживало анимацию. Более подробную информацию см. в разделе Общие сведения об эффектах анимации.

  • Требуется, чтобы система свойств сообщала об изменении предыдущего значения свойства действиями, выполняемыми системой свойств, окружением или пользователем, или путем чтения и использования стилей. Используя метаданные свойства, свойство может указывать метод обратного вызова, который будет вызываться каждый раз, когда система свойств определит, что значение свойства было однозначно изменено. Связанным понятием является приведение значения свойства. Дополнительные сведения см. в разделе Проверка и обратные вызовы свойства зависимостей.

  • Требуется использовать установленные соглашения о метаданных, которые также используются процессами WPF, такими как передача сведений о том, нужно ли системе макета перестраивать визуальные компоненты элемента при изменении значения свойства. Или требуется возможность использовать переопределения метаданных таким образом, чтобы производные классы могли изменять основанные на метаданных характеристики, такие как значение по умолчанию.

  • Требуется, чтобы свойства пользовательского элемента управления получали поддержку конструктора WPF, например редактирование окна Свойства. Дополнительные сведения см. в разделе Общие сведения о разработке элемента управления.

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

Контрольный список определения свойства зависимостей

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

  • (Необязательно.) Создайте метаданные для свойства зависимостей.

  • Зарегистрируйте имя свойства в системе свойств, указав тип владельца и тип значения свойства. Также укажите метаданные свойства, если они используются.

  • Определите идентификатор DependencyProperty в качестве поля publicstaticreadonly для типа владельца.

  • Определите свойство-"оболочку" CLR, имя которого соответствует имени свойства зависимостей. Реализуйте методы доступа get и set свойства-"оболочки" CLR для подключения к свойству зависимостей, которое оно поддерживает.

Регистрация свойства в системе свойств

Чтобы назначить свойство свойством зависимостей, необходимо зарегистрировать это свойство в таблице, обслуживаемой системой свойств, и предоставить ему уникальный идентификатор, используемый в качестве квалификатора для последующих операций системы свойств. Эти операции могут быть внутренними операциями или вашим собственным кодом, вызывающим API системы свойств. Чтобы зарегистрировать свойство, нужно вызвать метод Register в теле класса (внутри класса, но вне определения любого из членов). Поле идентификатора также предоставляется вызовом метода Register как возвращаемое значение. Причина, по которой вызов Register выполняется за пределами определений других членов, связана с тем, что это возвращаемое значение используется для назначения и создания поля publicstaticreadonly типа DependencyProperty как части вашего класса. Это поле становится идентификатором для вашего свойства зависимостей.

public static readonly DependencyProperty AquariumGraphicProperty = DependencyProperty.Register(
  "AquariumGraphic",
  typeof(Uri),
  typeof(AquariumObject),
  new FrameworkPropertyMetadata(null,
      FrameworkPropertyMetadataOptions.AffectsRender,
      new PropertyChangedCallback(OnUriChanged)
  )
);
Public Shared ReadOnly AquariumGraphicProperty As DependencyProperty = DependencyProperty.Register("AquariumGraphic", GetType(Uri), GetType(AquariumObject), New FrameworkPropertyMetadata(Nothing, FrameworkPropertyMetadataOptions.AffectsRender, New PropertyChangedCallback(AddressOf OnUriChanged)))

Соглашения об именовании свойств зависимостей

Существуют установленные соглашения об именовании, касающиеся свойств зависимостей, которым необходимо следовать, если ситуация не является исключительной.

Само свойство зависимостей будет иметь базовое имя (в этом примере — AquariumGraphic), которое указывается как первый параметр Register. Это имя должно быть уникальным в пределах каждого регистрирующего типа. Считается, что свойства зависимостей, наследуемые через базовые типы, уже являются частью регистрирующего типа: имена наследуемых свойств нельзя зарегистрировать повторно. Однако существует способ добавления класса как владельца свойства зависимостей даже в том случае, если это свойство зависимостей не наследуется. Дополнительные сведения см. в разделе Метаданные свойств зависимостей.

При создании поля идентификатора назовите это поле по зарегистрированному имени свойства, добавив суффикс Property. Это поле является идентификатором для свойства зависимостей и будет использоваться в дальнейшем в качестве входных данных для вызовов SetValue и GetValue, которые будут выполняться в оболочках, при получении доступа любым другим кодом к свойству через ваш код, при доступе любого внешнего кода, который вы разрешите, системой свойств и, вероятно, обработчиками XAML.

Примечание.

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

Реализация "оболочки"

Реализация оболочки должна вызывать GetValue в реализации get и SetValue в реализации set (исходный вызов регистрации и поле также показаны здесь для ясности).

Как правило, реализации оболочки должны выполнять только действия GetValue и SetValue соответственно. Причина этого обсуждается в разделе Загрузка кода XAML и свойства зависимостей.

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


public static readonly DependencyProperty AquariumGraphicProperty = DependencyProperty.Register(
  "AquariumGraphic",
  typeof(Uri),
  typeof(AquariumObject),
  new FrameworkPropertyMetadata(null,
      FrameworkPropertyMetadataOptions.AffectsRender,
      new PropertyChangedCallback(OnUriChanged)
  )
);
public Uri AquariumGraphic
{
  get { return (Uri)GetValue(AquariumGraphicProperty); }
  set { SetValue(AquariumGraphicProperty, value); }
}

Public Shared ReadOnly AquariumGraphicProperty As DependencyProperty = DependencyProperty.Register("AquariumGraphic", GetType(Uri), GetType(AquariumObject), New FrameworkPropertyMetadata(Nothing, FrameworkPropertyMetadataOptions.AffectsRender, New PropertyChangedCallback(AddressOf OnUriChanged)))
Public Property AquariumGraphic() As Uri
    Get
        Return CType(GetValue(AquariumGraphicProperty), Uri)
    End Get
    Set(ByVal value As Uri)
        SetValue(AquariumGraphicProperty, value)
    End Set
End Property

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

  • Не будут работать определенные аспекты стилей и шаблонов.

  • Большинство инструментов и конструкторов полагается на соглашения об именовании для правильной сериализации XAML, а также для предоставления помощи разработчику на уровне свойств.

  • Текущая реализация загрузчика WPF XAML полностью обходит оболочки и основывается на соглашении об именовании при обработке значений атрибутов. Дополнительные сведения см. в разделе Загрузка кода XAML и свойства зависимостей.

Метаданные свойства для нового свойства зависимостей

При регистрации свойства зависимостей через систему свойств создается объект метаданных, который хранит характеристики свойства. Многие из этих характеристик имеют значения по умолчанию, которые задаются, если свойство регистрируется с помощью простых сигнатур Register. Другие сигнатуры Register позволяют самостоятельно указывать нужные метаданные при регистрации свойства. Как правило, метаданные, задаваемые для свойства зависимостей, предполагают предоставление значений по умолчанию, применяемых для новых экземпляров, которые используют свойство.

При создании свойства зависимостей, которое существует в производном классе FrameworkElement, можно использовать более специализированный класс метаданных FrameworkPropertyMetadata вместо базового класса PropertyMetadata. Конструктор для класса FrameworkPropertyMetadata имеет несколько сигнатур, в которых можно указать различные характеристики метаданных в сочетании. Если требуется указать только значение по умолчанию, используйте сигнатуру, которая принимает один параметр типа Object. Передайте этот параметр объекта как значение по умолчанию для конкретного типа для свойства (предоставленное значение по умолчанию должно быть типом, указанным как параметр propertyType в вызове Register).

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

Задание соответствующих флагов метаданных

  • Если свойство (или изменения в его значении) влияет на пользовательский интерфейс и, в частности, на то, как система макета задает размер или отрисовку элемента на странице, задайте один или несколько из следующих флагов: AffectsMeasure, AffectsArrange, AffectsRender.

    • AffectsMeasure указывает, что изменение этого свойства требует изменения в отрисовке пользовательского интерфейса, когда объект-контейнер может потребовать больше или меньше места в родительском элементе. Например, этот флаг следует задать для свойства "Ширина".

    • AffectsArrange указывает, что изменение этого свойства требует изменения в отрисовке пользовательского интерфейса, что, как правило, не требует изменения в выделенном пространстве, однако означает, что изменилось размещение в пространстве. Например, этот флаг следует задать для свойства "Выравнивание".

    • AffectsRender указывает, что произошло некоторое другое изменение, не влияющее на макет и измерение, но требующее повторной отрисовки. Примером будет свойство, изменяющее цвет существующего элемента, например "Фон".

    • Эти флаги часто используются в качестве протокола в метаданных для собственных реализаций переопределения системы свойств или обратных вызовов макета. Например, код может содержать обратный вызов OnPropertyChanged, который будет вызывать InvalidateArrange, если любое свойство экземпляра сообщает об изменении значения и содержит AffectsArrange со значением true в своих метаданных.

  • Некоторые свойства могут влиять на характеристики отрисовки содержащего их родительского элемента указанными выше способами и помимо изменений в обязательном размере, упомянутых выше. Примером является свойство MinOrphanLines, используемое в модели документов нефиксированного формата, где изменения этого свойства могут изменять общую отрисовку документа нефиксированного формата, содержащего абзац. Используйте AffectsParentArrange или AffectsParentMeasure для определения аналогичных случаев в ваших собственных свойствах.

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

  • По умолчанию привязка данных Mode для свойств зависимостей имеет значение OneWay. Вы можете изменить режим привязки на TwoWay для каждого экземпляра привязки. Дополнительные сведения см. в разделе Указание направления привязки. Тем не менее, как автор свойства зависимостей, вы можете задать для свойства режим привязки TwoWay по умолчанию. Примером существующего свойства зависимостей является MenuItem.IsSubmenuOpen; сценарий для этого свойства подразумевает взаимодействие логики задания IsSubmenuOpen и компоновки MenuItem со стилями темы по умолчанию. Логика свойства IsSubmenuOpen изначально использует привязку данных для поддержания состояния свойства в соответствии с другими свойствами состояния и вызовами методов. Еще одним примером свойства, которое привязывает TwoWay по умолчанию, является TextBox.Text.

  • Вы также можете включить наследование свойств в настраиваемое свойство зависимостей, задав флаг Inherits. Наследование свойств удобно для ситуации, когда родительские и дочерние элементы имеют общее свойство и для дочерних элементов имеет смысл задать то же значение свойства, что и в родительском элементе. Примером наследуемого свойства является DataContext, которое используется для операций привязки, чтобы реализовать важный сценарий "основной — подчиненный" для представления данных. Благодаря наследованию DataContext дочерние элементы также наследуют этот контекст данных. Благодаря наследованию значения свойства вы можете указать контекст данных в корне приложения или страницы и вам не потребуется уточнять его для привязок во всех возможных дочерних элементах. DataContext также является хорошим примером для демонстрации того, что наследование переопределяет значение по умолчанию, но может всегда быть локально задано для любого конкретного дочернего элемента. Дополнительные сведения см. в разделе Использование шаблона "основной — подчиненный" с иерархическими данными. Наследование значения свойства может влиять на производительность, и таким образом, его следует использовать с осторожностью. Дополнительные сведения см. в разделе Наследование значения свойства.

  • Задайте флаг Journal, чтобы указать, будет ли свойство зависимостей обнаруживаться или использоваться службами ведения журнала переходов. Примером является свойство SelectedIndex: любой элемент, выбранный в элементе управления выбора, должен сохраняться при навигации по истории ведения журналов.

Свойства зависимости "только для чтения"

Можно определить свойство зависимости, которое доступно только для чтения. Однако ситуации для такого использования немного отличаются, как и процедура регистрации свойства в системе свойств и предоставление идентификатора. Дополнительные сведения см. в разделе Свойства зависимостей "только для чтения".

Свойства зависимостей типа коллекции

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

Замечания по безопасности свойств зависимостей

Свойства зависимостей должны объявляться как открытые свойства. Поля идентификаторов свойств зависимостей должны объявляться как открытые статические поля. Даже при попытке объявить другие уровни доступа (например, защищенный) к свойству зависимостей всегда можно обращаться через идентификатор в сочетании с API системы свойств. Даже защищенное поле идентификатора потенциально доступно из-за API передачи метаданных или определения значения, которые являются частью системы свойств, например LocalValueEnumerator. Дополнительные сведения см. в разделе Безопасность свойств зависимостей.

Свойства зависимостей и конструкторы класса

Есть общий принцип в программировании управляемого кода (он часто принудительно применяется средствами анализа кода, такими как FxCop), подразумевающий, что конструкторы класса не должны вызывать виртуальные методы. Этот принцип обусловлен тем, что конструкторы могут быть вызваны в качестве базовой инициализации конструктора производного класса, а ввод виртуального метода через конструктор может произойти в состоянии неполной инициализации конструируемого экземпляра объекта. При наследовании от любого класса, который уже является производным от DependencyObject, следует иметь в виду, что сама система свойств вызывает и предоставляет виртуальные методы. Эти виртуальные методы являются частью служб системы свойств WPF. Переопределение методов позволяет производным классам участвовать в определении значения. Чтобы избежать потенциальных проблем при инициализации среды выполнения, не задавайте значения свойств зависимостей в конструкторах классов (если только вы не следуете конкретному шаблону конструктора). Подробнее см. в разделе Шаблоны безопасного конструктора для DependencyObjects.

См. также