Общие сведения о разработке элементов управления

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

Альтернативы написанию нового элемента управления

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

  • Форматированное содержимое. Многие стандартные элементы управления WPF поддерживают форматированное содержимое. Например, свойство содержимого элемента Button имеет тип Object, поэтому теоретически в Button может отображаться все, что угодно. Чтобы на кнопке отображались изображение и текст, можно добавить изображение и TextBlock в StackPanel и назначить StackPanel свойству Content. Поскольку элементы управления могут отображать визуальные элементы WPF и произвольные данные, это уменьшает необходимость создания нового элемента управления или изменения существующего для поддержки сложной визуализации. Дополнительные сведения о модели содержимого для Button и о других моделях содержимого в WPF см. в разделе Модель содержимого WPF.

  • Стили. Style — это совокупность значений, представляющих свойства для элемента управления. С помощью стилей можно создать повторно используемое представление нужного внешнего вида и поведения элемента управления без написания нового элемента управления. Например, необходимо, чтобы все элементы управления TextBlock имели красный цвет и были выполнены со шрифтом Arial размера 14. Можно создать стиль как ресурс и задать соответствующие свойства. Затем все TextBlock, добавленные в приложение, будут иметь одинаковый внешний вид.

  • Шаблоны данных. DataTemplate позволяет настроить способ отображения данных в элементе управления. Например, DataTemplate можно использовать, чтобы указать, как отображаются данные в ListBox. Пример см. в разделе Общие сведения о шаблонах данных. В дополнение к настройке внешнего вида данных DataTemplate может включать элементы пользовательского интерфейса, которые дадут больше гибкости в пользовательских интерфейсах. Например, с помощью DataTemplate можно создать ComboBox, каждый элемент которого будет содержать флажок.

  • Шаблоны элементов управления. Многие элементы управления в WPF используют шаблон ControlTemplate для определения структуры и внешнего вида элемента управления, который отделяет внешний вид от функциональности элемента управления. Можно существенно изменить внешний вид элемента управления, переопределив его ControlTemplate. Предположим, что нужен элемент управления, который выглядит как светофор. Этот элемент управления имеет простой пользовательский интерфейс и функциональные возможности. Элемент управления состоит из трех кругов, из которых одновременно загорается только один. Чуть позже вы можете решить, что RadioButton предлагает функциональность одновременного выбора только одного элемента, но внешний вид RadioButton по умолчанию не похож на огни светофора. Поскольку RadioButton использует шаблон элемента управления для определения его внешнего вида, он может легко переопределить ControlTemplate согласно требованиям элемента управления, а также использовать переключатели, чтобы сделать светофор.

    Примечание.

    Несмотря на то что RadioButton может использовать DataTemplate, DataTemplate в данном примере недостаточно. DataTemplate определяет внешний вид содержимого элемента управления. В случае с RadioButton содержимое — это все то, что отображается справа от круга, указывающего, выбран ли RadioButton. В примере светофора переключатель должен быть только кругом, который может "загореться". Так как требования к внешнему виду светофора сильно отличаются от внешнего вида RadioButton по умолчанию, необходимо переопределить ControlTemplate. Как правило, DataTemplate используется для определения содержимого (или данных) элемента управления, а ControlTemplate — для определения структуры элемента управления.

  • Триггеры. Trigger позволяет динамически изменять внешний вид и поведение элемента управления без необходимости создания нового элемента управления. Предположим, что в приложении имеется несколько элементов управления ListBox и каждый ListBox при выборе должен выделяться полужирным красным шрифтом. Возможно, чтобы изменить внешний вид выделенного элемента, вы инстинктивно захотите создать класс, наследуемый от ListBox и переопределяющий метод OnSelectionChanged, однако лучше всего добавить триггер в стиль ListBoxItem, меняющий внешний вид выделенного элемента. Триггер позволяет изменять значения свойств или выполнять действия в зависимости от значения свойства. EventTrigger позволяет принимать меры, когда происходит событие.

Дополнительные сведения о стилях, шаблонах и триггерах см. в разделе Использование стилей и шаблонов.

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

Модели для создания элементов управления

Модель форматированного содержимого, стили, шаблоны и триггеры уменьшают необходимость создания новых элементов управления. Однако если нужно создать новый элемент управления, важно разбираться в разных моделях создания элементов управления в WPF. WPF предоставляет три общие модели для создания элемента управления, которые имеют различный набор функций и уровень гибкости. Базовые классы для трех моделей: UserControl, Control и FrameworkElement.

Создание производных классов от UserControl

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

При правильном построении UserControl может использовать форматированное содержимое, стили и триггеры. Однако если элемент управления наследуется от UserControl, пользователи, работающие с этим элементом управления, не смогут использовать DataTemplate или ControlTemplate для настройки его внешнего вида. Для создания пользовательского элемента управления, поддерживающего шаблоны, нужно наследовать от класса Control или одного из его производных классов (отличных от UserControl).

Преимущества использования производного класса от UserControl

Рассмотрите возможность наследования от UserControl, если актуально нижеперечисленное.

  • Нужно создать элемент управления аналогично созданию приложения.

  • Элемент управления состоит только из существующих компонентов.

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

Создание производного от элемента управления

Наследование от класса Control — это модель, используемая большинством существующих элементов управления WPF. При создании элемента управления, который наследует от класса Control, его внешний вид определяется с помощью шаблонов. Таким образом, можно отделить рабочую логику от визуального представления. Можно также обеспечить разделение пользовательского интерфейса и логики с помощью команд и привязок вместо событий и не использовать ссылки на элементы в ControlTemplate, если это возможно. Если пользовательский интерфейс и логика элемента управления правильно разделены, то пользователи могут переопределить элемент управления ControlTemplate для настройки внешнего вида. И хотя создать пользовательский Control не так просто, как создать UserControl, пользовательский Control обеспечивает максимальную гибкость.

Преимущества использования производного от элемента управления

Рассмотрите возможность наследования от Control вместо использования класса UserControl, если актуально нижеперечисленное:

  • Вам требуется возможность настройки внешнего вида элемента управления с помощью ControlTemplate.

  • Элемент управления должен поддерживать различные темы.

Создание производного от FrameworkElement

Элементы управления, которые наследуются от UserControl или Control, зависят от создания текущих элементов. Во многих сценариях такое решение является приемлемым, потому что любой объект, который наследуется от FrameworkElement, может находиться в ControlTemplate. Однако бывают случаи, когда для внешнего вида элемента управления требуется больше, чем функциональность простой композиции элементов. В этих сценариях правильно использовать компонент, основанный на FrameworkElement.

Существует два стандартных метода построения компонентов на основе FrameworkElement: прямая отрисовка и пользовательская композиция элементов. Прямая отрисовка включает переопределение метода OnRender элемента FrameworkElement и предоставление операций DrawingContext, которые явно определяют визуальные элементы компонента. Это метод, используемый Image и Border. Пользовательская композиция элемента включает использование объектов типа Visual для создания внешнего вида компонента. Например, см. раздел Использование объектов DrawingVisual. Track — это пример элемента управления в WPF, который использует пользовательскую композицию элементов. Можно также комбинировать прямую отрисовку и пользовательскую композицию элемента в одном элементе управления.

Преимущества использования производного от FrameworkElement

Рассмотрите возможность наследования от FrameworkElement, если актуально что-либо из нижеперечисленного.

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

  • Необходимо определить внешний вид элемента управления путем определения собственной логики отрисовки.

  • Вам требуется создать существующие элементы новыми способами, выходящими за пределы того, что возможно с использованием UserControl и Control.

Основы создания элементов управления

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

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

Если свойство является свойством зависимостей, то можно сделать следующее:

  • Установить свойство в стиле.

  • Привязать свойство к источнику данных.

  • Использовать динамический ресурс в качестве значения свойства.

  • Анимировать свойство.

Если свойство элемента управления должно поддерживать подобную функциональность, то следует реализовать его как свойство зависимостей. В следующем примере определяется свойство зависимостей с именем Value следующим способом:

  • Определите идентификатор DependencyProperty с именем ValueProperty как поле publicstaticreadonly.

  • Зарегистрируйте имя свойства в системе свойств, вызвав DependencyProperty.Register, чтобы задать следующее:

    • Имя свойства.

    • Тип свойства.

    • Тип, к которому принадлежит это свойство.

    • Метаданные для свойства. Метаданные содержат значение свойства по умолчанию, CoerceValueCallback и PropertyChangedCallback.

  • Определите свойство программы-оболочки CLR с именем Value, которое используется для регистрации свойства зависимостей, путем реализации методов доступа get и set свойства. Обратите внимание, что методы доступа get и set могут вызывать только GetValue и SetValue соответственно. Рекомендуется не включать дополнительную логику в методы доступа свойств зависимости, так как клиенты и WPF могут обходить методы доступа и вызывать GetValue и SetValue напрямую. Например, если свойство привязано к источнику данных, то метод доступа set свойства не вызывается. Вместо того чтобы добавлять дополнительную логику в методы доступа get и set, используйте делегаты ValidateValueCallback, CoerceValueCallback и PropertyChangedCallback, чтобы проверить значение при изменении или ответить на него. Дополнительные сведения об этих обратных вызовах см. в разделе Проверка и обратные вызовы свойства зависимостей.

  • Определите метод для элемента CoerceValueCallback с именем CoerceValue. CoerceValue гарантирует, что Value больше или равно MinValue и меньше или равно MaxValue.

  • Определите метод для элемента PropertyChangedCallback с именем OnValueChanged. OnValueChanged создает объект RoutedPropertyChangedEventArgs<T> и подготавливается к созданию перенаправляемого события ValueChanged. Перенаправляемые события рассматриваются в следующем разделе.

/// <summary>
/// Identifies the Value dependency property.
/// </summary>
public static readonly DependencyProperty ValueProperty =
    DependencyProperty.Register(
        "Value", typeof(decimal), typeof(NumericUpDown),
        new FrameworkPropertyMetadata(MinValue, new PropertyChangedCallback(OnValueChanged),
                                      new CoerceValueCallback(CoerceValue)));

/// <summary>
/// Gets or sets the value assigned to the control.
/// </summary>
public decimal Value
{
    get { return (decimal)GetValue(ValueProperty); }
    set { SetValue(ValueProperty, value); }
}

private static object CoerceValue(DependencyObject element, object value)
{
    decimal newValue = (decimal)value;
    NumericUpDown control = (NumericUpDown)element;

    newValue = Math.Max(MinValue, Math.Min(MaxValue, newValue));

    return newValue;
}

private static void OnValueChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    NumericUpDown control = (NumericUpDown)obj;			

    RoutedPropertyChangedEventArgs<decimal> e = new RoutedPropertyChangedEventArgs<decimal>(
        (decimal)args.OldValue, (decimal)args.NewValue, ValueChangedEvent);
    control.OnValueChanged(e);
}
''' <summary>
''' Identifies the Value dependency property.
''' </summary>
Public Shared ReadOnly ValueProperty As DependencyProperty = DependencyProperty.Register("Value", GetType(Decimal), GetType(NumericUpDown), New FrameworkPropertyMetadata(MinValue, New PropertyChangedCallback(AddressOf OnValueChanged), New CoerceValueCallback(AddressOf CoerceValue)))

''' <summary>
''' Gets or sets the value assigned to the control.
''' </summary>
Public Property Value() As Decimal
    Get
        Return CDec(GetValue(ValueProperty))
    End Get
    Set(ByVal value As Decimal)
        SetValue(ValueProperty, value)
    End Set
End Property

Private Shared Overloads Function CoerceValue(ByVal element As DependencyObject, ByVal value As Object) As Object
    Dim newValue As Decimal = CDec(value)
    Dim control As NumericUpDown = CType(element, NumericUpDown)

    newValue = Math.Max(MinValue, Math.Min(MaxValue, newValue))

    Return newValue
End Function

Private Shared Sub OnValueChanged(ByVal obj As DependencyObject, ByVal args As DependencyPropertyChangedEventArgs)
    Dim control As NumericUpDown = CType(obj, NumericUpDown)

    Dim e As New RoutedPropertyChangedEventArgs(Of Decimal)(CDec(args.OldValue), CDec(args.NewValue), ValueChangedEvent)
    control.OnValueChanged(e)
End Sub

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

Использование перенаправляемых событий

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

  • События могут обрабатываться в родительском элементе нескольких элементов управления. Если событие является событием восходящей маршрутизации, то один родительский элемент в дереве элементов может подписаться на это событие. Разработчики приложений могут использовать один обработчик для реагирования на событие нескольких элементов управления. Например, если элемент управления является частью каждого элемента в ListBox (поскольку включен в DataTemplate), разработчик приложения может определить обработчик событий для события вашего элемента управления в ListBox. Обработчик событий вызывается при возникновении события в любом элементе управления.

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

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

Следующий пример определяет перенаправляемое событие:

  • Определите идентификатор RoutedEvent с именем ValueChangedEvent как поле publicstaticreadonly.

  • Зарегистрируйте перенаправляемое событие, вызвав метод EventManager.RegisterRoutedEvent. В примере задается следующая информация при вызове RegisterRoutedEvent:

    • Имя события ValueChanged.

    • Стратегия маршрутизации Bubble. Это означает, что обработчик событий в источнике (объект, вызывающий событие) вызывается первым, а затем последовательно вызываются обработчики событий родительских элементов источника, начиная с обработчика события для ближайшего родительского элемента.

    • Тип обработчика событий — RoutedPropertyChangedEventHandler<T>, он создан с типом Decimal.

    • Тип — владелец события — NumericUpDown.

  • Объявите общее событие с именем ValueChanged, которое включает объявления метода доступа к событию. В этом примере AddHandler вызывается в объявлении метода доступа add, а RemoveHandler вызывается в объявлении метода доступа remove, чтобы можно было использовать службы событий WPF.

  • Создайте защищенный виртуальный метод с именем OnValueChanged, вызывающий событие ValueChanged.

/// <summary>
/// Identifies the ValueChanged routed event.
/// </summary>
public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent(
    "ValueChanged", RoutingStrategy.Bubble,
    typeof(RoutedPropertyChangedEventHandler<decimal>), typeof(NumericUpDown));

/// <summary>
/// Occurs when the Value property changes.
/// </summary>
public event RoutedPropertyChangedEventHandler<decimal> ValueChanged
{
    add { AddHandler(ValueChangedEvent, value); }
    remove { RemoveHandler(ValueChangedEvent, value); }
}

/// <summary>
/// Raises the ValueChanged event.
/// </summary>
/// <param name="args">Arguments associated with the ValueChanged event.</param>
protected virtual void OnValueChanged(RoutedPropertyChangedEventArgs<decimal> args)
{
    RaiseEvent(args);
}
''' <summary>
''' Identifies the ValueChanged routed event.
''' </summary>
Public Shared ReadOnly ValueChangedEvent As RoutedEvent = EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Bubble, GetType(RoutedPropertyChangedEventHandler(Of Decimal)), GetType(NumericUpDown))

''' <summary>
''' Occurs when the Value property changes.
''' </summary>
Public Custom Event ValueChanged As RoutedPropertyChangedEventHandler(Of Decimal)
    AddHandler(ByVal value As RoutedPropertyChangedEventHandler(Of Decimal))
        MyBase.AddHandler(ValueChangedEvent, value)
    End AddHandler
    RemoveHandler(ByVal value As RoutedPropertyChangedEventHandler(Of Decimal))
        MyBase.RemoveHandler(ValueChangedEvent, value)
    End RemoveHandler
    RaiseEvent(ByVal sender As System.Object, ByVal e As RoutedPropertyChangedEventArgs(Of Decimal))
    End RaiseEvent
End Event

''' <summary>
''' Raises the ValueChanged event.
''' </summary>
''' <param name="args">Arguments associated with the ValueChanged event.</param>
Protected Overridable Sub OnValueChanged(ByVal args As RoutedPropertyChangedEventArgs(Of Decimal))
    MyBase.RaiseEvent(args)
End Sub

Дополнительные сведения см. в разделах Общие сведения о перенаправляемых событиях и Создание пользовательских перенаправляемых событий.

Использование привязки

Для отделения пользовательского интерфейса от логики элемента управления можно использовать привязку данных. Это особенно важно, если внешний вид элемента управления определяется с помощью ControlTemplate. При использовании привязки данных можно избавиться от необходимости ссылаться на определенные части пользовательского интерфейса из кода. Не рекомендуется ссылаться на элементы в ControlTemplate, потому что когда код ссылается на элементы в ControlTemplate и ControlTemplate меняется, элемент, на который приводится ссылка, необходимо включить в новый ControlTemplate.

В следующем примере обновляется TextBlock элемента управления NumericUpDown. Ему назначается имя, и указывается ссылка на текстовое поле по имени в коде.

<Border BorderThickness="1" BorderBrush="Gray" Margin="2" 
        Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Stretch">
  <TextBlock Name="valueText" Width="60" TextAlignment="Right" Padding="5"/>
</Border>
private void UpdateTextBlock()
{
    valueText.Text = Value.ToString();
}
Private Sub UpdateTextBlock()
    valueText.Text = Value.ToString()
End Sub

Следующий пример использует привязку для достижения такого же результата.

<Border BorderThickness="1" BorderBrush="Gray" Margin="2" 
        Grid.RowSpan="2" VerticalAlignment="Center" HorizontalAlignment="Stretch">

    <!--Bind the TextBlock to the Value property-->
    <TextBlock 
        Width="60" TextAlignment="Right" Padding="5"
        Text="{Binding RelativeSource={RelativeSource FindAncestor, 
                       AncestorType={x:Type local:NumericUpDown}}, 
                       Path=Value}"/>

</Border>

Дополнительные сведения о привязке данных см. в разделе Общие сведения о привязке данных.

Разработка для конструкторов

Чтобы получить поддержку пользовательских элементов управления WPF в конструкторе WPF для Visual Studio (например, редактирование свойства в окне "Свойства"), следуйте рекомендациям ниже. Дополнительные сведения о разработке для конструктора WPF см. в разделе Конструктор XAML в Visual Studio.

Свойства зависимостей

Обязательно реализуйте методы доступа get и set среды CLR, как описано выше, в разделе "Использование свойств зависимостей". Конструкторы могут использовать программу-оболочку для обнаружения присутствия свойства зависимостей, но им, как и WPF и клиентам элемента управления, не требуется вызывать методы доступа при получении или настройке свойства.

Присоединенные свойства

При реализации вложенных свойств в пользовательских элементах управления учитывайте следующие рекомендации:

  • Требуется наличие publicstaticreadonlyDependencyProperty формы PropertyNameProperty, созданной с помощью метода RegisterAttached. Имя свойства, переданное RegisterAttached, должно соответствовать PropertyName.

  • Реализуйте пару методов CLR publicstatic с именем SetPropertyName и GetPropertyName. Оба метода должны принимать производный класс от DependencyProperty в качестве первого аргумента. Метод SetPropertyName также принимает аргумент, тип которого соответствует зарегистрированному типу данных для свойства. Метод GetPropertyName должен возвращать значение такого же типа. Если метод SetPropertyName отсутствует, свойство отмечается как "только для чтения".

  • SetPropertyName и GetPropertyName должны вести непосредственно к методам GetValue и SetValue в целевом объекте зависимости соответственно. Разработчики могут получить доступ к вложенному свойству, вызвав программу-оболочку метода или с помощью прямого вызова целевого объекта зависимостей.

Дополнительные сведения о вложенных свойствах см. в разделе Общие сведения о вложенных свойствах.

Определение и использование общих ресурсов

Можно включить элемент управления в ту же сборку, что и приложение, или упаковать его в отдельную сборку, которая может использоваться в нескольких приложениях. В большинстве случаев сведения, рассматриваемые в данном разделе, применяются независимо от используемого метода. Однако есть одно отличие, о котором следует упомянуть. При помещении элемента управления в ту же сборку, что и приложение, можно добавить глобальные ресурсы в файл App.xaml. Но сборка, содержащая только элементы управления, не связана с объектом Application, поэтому файл App.xaml будет недоступен.

Приложение выполняет поиск ресурса на трех уровнях в следующем порядке:

  1. Уровень элемента.

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

  2. Уровень приложения.

    Ресурсы, определенные объектом Application.

  3. Уровень темы.

    Словари уровня темы хранятся в подпапке "Темы". Файлы в папке "Темы" соответствуют темам. Например, могут присутствовать файлы Aero.NormalColor.xaml, Luna.NormalColor.xaml, Royale.NormalColor.xaml и т. д. Также может присутствовать файл с именем generic.xaml. Когда система ищет ресурс на уровне темы, она сначала ищет его в файле конкретной темы, а затем в файле generic.xaml.

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

Определение ресурсов на уровне элемента

Общие ресурсы на уровне элемента можно определить путем создания пользовательского словаря ресурсов и его объединения со словарем ресурсов элемента управления. При использовании этого метода можно присвоить файлу ресурсов любое имя и его можно поместить в одну папку с элементами управления. Ресурсы на уровне элемента также могут использовать простые строки как ключи. В следующем примере создается файл ресурсов LinearGradientBrush с именем Dictionary1.xaml.

<ResourceDictionary 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <LinearGradientBrush 
    x:Key="myBrush"  
    StartPoint="0,0" EndPoint="1,1">
    <GradientStop Color="Red" Offset="0.25" />
    <GradientStop Color="Blue" Offset="0.75" />
  </LinearGradientBrush>
  
</ResourceDictionary>

После определения словаря необходимо его объединить со словарем ресурсов элемента управления. Это можно сделать с помощью XAML или кода.

Следующий пример объединяет словарь ресурса с помощью XAML.

<UserControl.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Dictionary1.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</UserControl.Resources>

Недостатком этого подхода является то, что объект ResourceDictionary создается при каждой ссылке на него. Например, если в библиотеке есть 10 пользовательских элементов управления и выполняется объединение словарей общих ресурсов для каждого элемента управления с помощью XAML, то будут созданы 10 идентичных объектов ResourceDictionary. Этого можно избежать, создав статический класс, который объединяет ресурсы в коде и возвращает полученный ResourceDictionary.

В следующем примере создается класс, который возвращает общий ResourceDictionary.

internal static class SharedDictionaryManager
{
    internal static ResourceDictionary SharedDictionary
    {
        get
        {
            if (_sharedDictionary == null)
            {
                System.Uri resourceLocater =
                    new System.Uri("/ElementResourcesCustomControlLibrary;component/Dictionary1.xaml",
                                    System.UriKind.Relative);

                _sharedDictionary =
                    (ResourceDictionary)Application.LoadComponent(resourceLocater);
            }

            return _sharedDictionary;
        }
    }

    private static ResourceDictionary _sharedDictionary;
}

В следующем примере общий ресурс объединяется с ресурсами пользовательского элемента управления в конструкторе элемента управления, прежде чем он вызывает InitializeComponent. Поскольку SharedDictionaryManager.SharedDictionary является статическим свойством, ResourceDictionary создается только один раз. Поскольку словарь ресурсов был объединен до вызова InitializeComponent, ресурсы доступны для элемента управления в его файле XAML.

public NumericUpDown()
{
    this.Resources.MergedDictionaries.Add(SharedDictionaryManager.SharedDictionary);
    InitializeComponent();
}

Определение ресурсов на уровне темы

WPF позволяет создавать ресурсы для разных тем Windows. Как разработчик элемента управления, вы можете определить ресурс для определенной темы, чтобы изменить внешний вид элемента управления в зависимости от того, какая тема используется. Например, внешний вид Button в классической теме Windows (тема по умолчанию для Windows 2000) отличается от Button в теме Windows Luna (тема по умолчанию для Windows XP), так как Button использует разный ControlTemplate для каждой темы.

Ресурсы, относящиеся к теме, хранятся в словаре ресурсов с заданным именем файла. Эти файлы должны находиться в папке с именем Themes, которая является подпапкой папки, содержащей элемент управления. В следующей таблице перечислены файлы словаря ресурсов и темы, связанные с каждым файлом.

Имя файла словаря ресурсов Тема Windows
Classic.xaml Классический вид Windows 9x/2000 для Windows XP
Luna.NormalColor.xaml Синяя тема по умолчанию в Windows XP
Luna.Homestead.xaml Оливковая тема в Windows XP
Luna.Metallic.xaml Серебристая тема в Windows XP
Royale.NormalColor.xaml Тема по умолчанию в Windows XP Media Center Edition
Aero.NormalColor.xaml Тема по умолчанию в Windows Vista

Не нужно определять ресурс для каждой темы. Если ресурс не определен для конкретной темы, элемент управления проверяет Classic.xaml для ресурса. Если ресурс не определен в файле, соответствующем текущей теме, или в Classic.xaml, то элемент управления использует общий ресурс, который находится в файле словаря ресурса с именем generic.xaml. Файл generic.xaml расположен в той же папке, что и файлы словаря ресурсов, связанные с темами. Хотя generic.xaml не соответствует конкретной теме Windows, он по-прежнему является словарем уровня темы.

Пример пользовательского элемента управления NumericUpDown C# или Visual Basic с поддержкой тем и автоматизации пользовательского интерфейса содержит два словаря ресурсов для элемента управления NumericUpDown: один — в файле generic.xaml, а второй — в Luna.NormalColor.xaml.

Помещая ControlTemplate в любой из файлов словаря ресурсов для конкретной темы, необходимо создать статический конструктор для вашего элемента управления и вызвать метод OverrideMetadata(Type, PropertyMetadata) для DefaultStyleKey, как показано в следующем примере.

static NumericUpDown()
{
    DefaultStyleKeyProperty.OverrideMetadata(typeof(NumericUpDown),
               new FrameworkPropertyMetadata(typeof(NumericUpDown)));
}
Shared Sub New()
    DefaultStyleKeyProperty.OverrideMetadata(GetType(NumericUpDown), New FrameworkPropertyMetadata(GetType(NumericUpDown)))
End Sub
Определение и создание ссылок на ключи для ресурсов тем

При определении ресурса на уровне элемента можно назначить строку в качестве его ключа и получить доступ к ресурсу через эту строку. При определении ресура на уровне темы необходимо использовать ComponentResourceKey в качестве ключа. В следующем примере определяется ресурс в файле generic.xaml.

<LinearGradientBrush 
     x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type local:Painter}, 
                                  ResourceId=MyEllipseBrush}"  
                                  StartPoint="0,0" EndPoint="1,0">
    <GradientStop Color="Blue" Offset="0" />
    <GradientStop Color="Red" Offset="0.5" />
    <GradientStop Color="Green" Offset="1"/>
</LinearGradientBrush>

В следующем примере ссылка на ресурс указана с помощью ComponentResourceKey в качестве ключа.

<RepeatButton 
    Grid.Column="1" Grid.Row="0"
    Background="{StaticResource {ComponentResourceKey 
                        TypeInTargetAssembly={x:Type local:NumericUpDown}, 
                        ResourceId=ButtonBrush}}">
    Up
</RepeatButton>
<RepeatButton 
    Grid.Column="1" Grid.Row="1"
    Background="{StaticResource {ComponentResourceKey 
                    TypeInTargetAssembly={x:Type local:NumericUpDown}, 
                    ResourceId=ButtonBrush}}">
    Down
 </RepeatButton>
Определение местоположения ресурсов тем

Чтобы найти ресурсы для элемента управления, ведущее приложение должно знать, что сборка содержит ресурсы для элемента управления. Это можно сделать, добавив ThemeInfoAttribute в сборку, содержащую элемент управления. ThemeInfoAttribute содержит свойство GenericDictionaryLocation, указывающее расположение универсальных ресурсов, а также свойство ThemeDictionaryLocation, указывающее расположение ресурсов для конкретной темы.

В следующем примере свойства GenericDictionaryLocation и ThemeDictionaryLocation задаются равными SourceAssembly, чтобы указать, что универсальные и относящиеся к конкретной теме ресурсы находятся в той же сборке, что и элемент управления.

[assembly: ThemeInfo(ResourceDictionaryLocation.SourceAssembly,
           ResourceDictionaryLocation.SourceAssembly)]
<Assembly: ThemeInfo(ResourceDictionaryLocation.SourceAssembly, ResourceDictionaryLocation.SourceAssembly)>

См. также