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

Обновлен: Ноябрь 2007

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

В этом разделе содержатся следующие подразделы.

  • Альтернативы написанию новых элементов управления
  • Модели для создания элемента управления
  • Основы разработки элементов управления
  • Наследование от UserControl и использование ControlTemplate
  • Связанные разделы

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

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

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

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

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

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

    ms745025.alert_note(ru-ru,VS.90).gifПримечание.

    Хотя 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 (Extensible Application Markup Language). Это позволяет впоследствии ссылаться на именованные элементы и определять обработчики событий в коде. Эта модель разработки очень схожа с моделью, используемой для разработки приложений в 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, в качестве поля public static readonly.

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

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

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

    • Тип владельца свойства.

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

  • Определите свойство-оболочку CLR, называемое Value (это имя совпадает с именем, используемым для регистрации свойства зависимостей), путем реализации методов доступа get и set свойства. Обратите внимание, что методы доступа get и set вызывают только GetValue и SetValue соответственно. Рекомендуется, чтобы методы доступа к свойствам зависимостей не содержали дополнительной логики, так как клиенты и WPF могут пропускать методы доступа и вызывать GetValue и SetValue напрямую. Например, если свойство привязано к источнику данных, то метод доступа свойства 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);
}

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

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

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

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

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

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

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

  • Определите идентификатор RoutedEvent, называемый ValueChangedEvent, в качестве поля public static readonly.

  • Зарегистрируйте перенаправленное событие путем вызова метода 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);
}

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

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

Для отделения пользовательского интерфейса элемента управления от логики попробуйте использовать привязку данных. Это особенно важно, если внешний вид элемента управления определяется при помощи 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();
}

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

<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>

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

Определение и использование команд

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

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

  <RepeatButton Name="upButton" Click="upButton_Click" 
                  Grid.Column="1" Grid.Row="0">Up</RepeatButton>
                  
    <RepeatButton Name="downButton" Click="downButton_Click" 
                  Grid.Column="1" Grid.Row="1">Down</RepeatButton>
private void upButton_Click(object sender, EventArgs e)
{
        Value++;
}

private void downButton_Click(object sender, EventArgs e)
{
        Value--;
}

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

public static RoutedCommand IncreaseCommand
{
    get
    {
        return _increaseCommand;
    }
}
public static RoutedCommand DecreaseCommand
{
    get
    {
        return _decreaseCommand;
    }
}

private static void InitializeCommands()
{
    _increaseCommand = new RoutedCommand("IncreaseCommand", typeof(NumericUpDown));
    CommandManager.RegisterClassCommandBinding(typeof(NumericUpDown), 
                            new CommandBinding(_increaseCommand, OnIncreaseCommand));
    CommandManager.RegisterClassInputBinding(typeof(NumericUpDown), 
                            new InputBinding(_increaseCommand, new KeyGesture(Key.Up)));

    _decreaseCommand = new RoutedCommand("DecreaseCommand", typeof(NumericUpDown));
    CommandManager.RegisterClassCommandBinding(typeof(NumericUpDown), 
                            new CommandBinding(_decreaseCommand, OnDecreaseCommand));
    CommandManager.RegisterClassInputBinding(typeof(NumericUpDown), 
                            new InputBinding(_decreaseCommand, new KeyGesture(Key.Down)));
}

private static void OnIncreaseCommand(object sender, ExecutedRoutedEventArgs e)
{
    NumericUpDown control = sender as NumericUpDown;
    if (control != null)
    {
        control.OnIncrease();
    }
}
private static void OnDecreaseCommand(object sender, ExecutedRoutedEventArgs e)
{
    NumericUpDown control = sender as NumericUpDown;
    if (control != null)
    {
        control.OnDecrease();
    }
}

protected virtual void OnIncrease()
{
    Value++;
}
protected virtual void OnDecrease()
{
    Value--;
}

private static RoutedCommand _increaseCommand;
private static RoutedCommand _decreaseCommand;

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

<RepeatButton 
    Command="{x:Static local:NumericUpDown.IncreaseCommand}"  
    Grid.Column="1" Grid.Row="0">Up</RepeatButton>
<RepeatButton 
    Command="{x:Static local:NumericUpDown.DecreaseCommand}"  
    Grid.Column="1" Grid.Row="1">Down</RepeatButton>

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

Определение того, что элемент является обязательным в ControlTemplate

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

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

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

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

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

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

Вложенные свойства зависимостей

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

  • Воспользуйтесь public static readonlyDependencyProperty из формы PropertyNameProperty, которая производила создание с помощью метода RegisterAttached. Имя свойства, которое передается RegisterAttached должно соответствовать PropertyName.

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

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

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

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

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

Когда приложение выполняет поиск ресурса, оно использует три уровня в следующем порядке:

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

  2. Уровень приложения — ресурсы, определенные объектом Application.

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

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

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

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

<ResourceDictionary 
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://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;
}

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

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

}

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

WPF позволяет создавать ресурсы для разных тем Windows.  Автор элемента управления может определить ресурс для отдельной темы, чтобы изменить внешний вид элемента управления в зависимости от используемой темы. Например, внешний вид Button в теме Windows Classic (тема по умолчанию для 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

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

Пример пользовательского элемента управления NumericUpDown с поддержкой тем и модели автоматизации пользовательского интерфейса содержит два словаря ресурсов для элемента управления NumericUpDown: один в файле generic.xaml и другой в файле Luna.NormalColor.xaml. Можно запустить приложение и переключаться между серебристой темой в Windows XP и другой темой, чтобы увидеть разницу между двумя шаблонами элемента управления. (При работе в Windows Vista можно переименовать Luna.NormalColor.xaml в Aero.NormalColor.xaml и переключаться между двумя темами, например классической Windows и темой по умолчанию для Windows Vista.)

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

static NumericUpDown()
{
    DefaultStyleKeyProperty.OverrideMetadata(typeof(NumericUpDown),
               new FrameworkPropertyMetadata(typeof(NumericUpDown)));
}

Определяющие и ссылочные ключи для ресурсов тем

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

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

Определение местоположения ресурсов тем

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

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

[assembly: ThemeInfo(ResourceDictionaryLocation.SourceAssembly, 
           ResourceDictionaryLocation.SourceAssembly)]

Наследование от UserControl и использование ControlTemplate

Несколько примеров демонстрируют различные методы написания и пакетирования элемента управления NumericUpDown. В Пример пользовательского элемента управления NumericUpDown с DependencyProperty и RoutedEvent, NumericUpDown наследует от UserControl; в Пример пользовательского элемента управления NumericUpDown с поддержкой тем и модели автоматизации пользовательского интерфейса, NumericUpDown наследует от класса Control и использует ControlTemplate. В этом разделе кратко описываются различия между этими двумя объектами и объясняется, почему элемент управления, использующий ControlTemplate, является более расширяемым.

Первым главным отличием является то, что объект NumericUpDown, наследующий от класса UserControl, не использует ControlTemplate, а элемент управления, наследующий непосредственно от класса Control, — использует. В следующем примере показан XAML элемента управления, наследующего от класса UserControl. Как можно видеть, XAML очень походит на то, что отображается при создании приложения и начале работы с Window или Page.

<!--XAML for NumericUpDown that inherits from UserControl.-->
<UserControl x:Class="MyUserControl.NumericUpDown"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:MyUserControl">
    <Grid Margin="3">
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <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>

        <RepeatButton Name="upButton" Click="upButton_Click" 
                      Grid.Column="1" Grid.Row="0">Up</RepeatButton>

        <RepeatButton Name="downButton" Click="downButton_Click" 
                      Grid.Column="1" Grid.Row="1">Down</RepeatButton>

    </Grid>
</UserControl>

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

<!--ControlTemplate for NumericUpDown that inherits from
    Control.-->
<Style TargetType="{x:Type local:NumericUpDown}">
  <Setter Property="HorizontalAlignment" Value="Center"/>
  <Setter Property="VerticalAlignment" Value="Center"/>
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type local:NumericUpDown}">
        <Grid Margin="3">
          <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
          </Grid.RowDefinitions>
          <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
          </Grid.ColumnDefinitions>

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

            <TextBlock Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Value}" 
                       Width="60" TextAlignment="Right" Padding="5"/>
          </Border>

          <RepeatButton Command="{x:Static local:NumericUpDown.IncreaseCommand}"  
                        Grid.Column="1" Grid.Row="0">Up</RepeatButton>

          <RepeatButton Command="{x:Static local:NumericUpDown.DecreaseCommand}"
                        Grid.Column="1" Grid.Row="1">Down</RepeatButton>

        </Grid>

      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

Наибольшее отличие в двух предыдущих примерах заключается в том, что в одном, где используется ControlTemplate, имеется возможность настраиваемого внешнего вида, а в другом, где выполняется наследование от класса UserControl, такой возможности нет. В случае, где NumericUpDown наследует от класса UserControl, разработчик приложения никаким способом не может изменить внешний вид элемента управления. Действительно, даже если NumericUPDown имеет свойство ControlTemplate (так как UserControl наследует от Control), при попытке задать его последует исключение во время выполнения. С другой стороны, разработчик приложения, использующий объект NumericUpDown, который наследует от класса Control, может свободно создать новый класс ControlTemplate для элемента управления. Например, можно было бы создать ControlTemplate, который помещает кнопки слева или справа от TextBlock, а не сверху и снизу.

Разница между этими двумя подходами наглядно видна в различии синтаксиса в предыдущих примерах. Элемент управления, использующий ControlTemplate, задает свойство Template в классе Style для NumericUpDown. Это обычный метод создания шаблонов элементов управления. С помощью задания свойства Template в стиле определяется то, что все экземпляры элемента управления будут использовать этот класс ControlTemplate. Разработчики приложений могут свободно изменять свойство Template объекта NumericcUpDown, чтобы настроить его внешний вид. В противоположность, XAML для элемента управления, наследующего от класса UserControl, заполняет свойство Content объекта NumericUpDown (<UserControl.Content> предполагается на XAML). Если разработчик не может изменить свойство Content объект NumericUpDown недоступен.

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

Другое различие заключается в синтаксисе привязки данных между свойством Text объекта TextBlock и свойством Value. В случае объекта UserControl, привязка данных определяет, что свойство RelativeSource является элементом управления NumericUpDown родителя и выполняет привязку к свойству Value. В случае объекта ControlTemplate, RelativeSource является элементом управления, которому принадлежит шаблон. Они выполняют те же функции, однако следует упомянуть, что синтаксис привязки данных различается в этих двух примерах.

В примере Пример пользовательского элемента управления NumericUpDown с поддержкой тем и модели автоматизации пользовательского интерфейса элемент управления NumericUpDown находится в сборке отдельно от приложения и определяет и использует ресурсы уровня темы, а в примере Пример пользовательского элемента управления NumericUpDown с DependencyProperty и RoutedEvent элемент управления NumericUpDown находится в одной сборке с приложением и не определяет и не использует ресурсы на уровне темы.

Наконец, в примере Пример пользовательского элемента управления NumericUpDown с поддержкой тем и модели автоматизации пользовательского интерфейса показано, как создать объект AutomationPeer для элемента управления NumericUpDown. Дополнительные сведения о поддержке Модель автоматизации пользовательского интерфейса для пользовательских элементов управления см. в разделе Поддержка автоматизации пользовательского интерфейса для стандартных элементов управления.

См. также

Основные понятия

URI типа "pack" в Windows Presentation Foundation

Другие ресурсы

Конструктор WPF

Настройка элементов управления

Примеры настройки элементов управления