Опубликован: 13.12.2011 | Уровень: для всех | Доступ: платный
Лекция 7:

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

CustomControl

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

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

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

Шаг 1. Рефакторинг кода адресной формы

Превратим рассмотренную выше адресную форму в элемент управления без внешнего вида (CustomControl). Первый шаг прост — нужно всего лишь изменить объявление класса, как показано ниже:

public class AdressCustomControl: System.Windows.Controls.Control 
{ ... }
Код внутри класса AdressCustomControl

Код точно такой же, как код из пользовательского элемента управления прошлого примера (UserControl) (за исключением того факта, что из конструктора понадобится удалить вызов InitializeComponent()).

Так же необходимо определить свойства зависимости (DependencyProperty), и добавить стандартные оболочки для свойств, которые облегчают доступ к ним и обеспечивают возможность обращения из XAML-разметки. Данный пункт можно было сделать и в версии создаваемого ранее UserControl’a, что собственно лишь еще раз подчеркивает очень тонкую грань между этими двумя понятиями.

Определение свойств зависимости

Нужно создать свойства, методы и события, которые будут поступать в наш CustomControl, и на которые будет опираться приложение, использующее его для обработки адреса. В созданном нами CustomControl будут следующие свойства определяющие адрес: Location, Adress1, Adress2, State.

Первый шаг в создании свойства зависимости — это определение статического поля для него с добавленным словом Property в конце его имени:

public static DependencyProperty LocationProperty; 

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

В свойстве Location мы могли бы ввести проверку значения на равенство фразе Microsoft (в случае равенства автоматически изменить значения свойств Adress1, Adress2 и State на нужные нам значения). Ниже приведен код статического конструктора, регистрирующего пять свойств зависимости для нашей адресной формы.

static AdressCustomControl () 
{ 
    LocationProperty = DependencyProperty.Register ( 
      "Location", typeof(string), typeof(TextBox),
            new PropertyChangedCallback (OnLacationChanged)); 
    Adress1Property = DependencyProperty.Register ( 
      "Adress1", typeof(string), typeof(TextBox),
            new PropertyChangedCallback (null)); 
    Adress2Property = DependencyProperty.Register ( 
      "Adress2", typeof(string), typeof(TextBox),
            new PropertyChangedCallback (null)); 
   StateProperty = DependencyProperty.Register ( 
      "State", typeof(string), typeof(TextBox),
            new PropertyChangedCallback (null)); 
} 

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

public string Location
{
    get { return (string)GetValue(LocationProperty); } 
    set { SetValue(LocationProperty, value); } 
}
...

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

private static void OnLocationChanged(DependencyObject sender,
                                       DependencyPropertyChangedEventArgs e) 
{ 
    string newLocation = (string)e.NewValue; 
    AdressCustomControl adressCustomControl = (AdressCustomControl)sender; 

    if(newLocation.Equals("Microsoft")
    {
        adressCustomControl.Adress1 = "One Microsoft Way";
        adressCustomControl.Adress2 = "Building 10";
        adressCustomControl.State = "Redmond, WA 98052";
    }
}

Далее предоставим новый стиль для создаваемого CustomControl’a. Этот стиль будет обеспечен новым шаблоном элемента. (Если пропустить этот шаг, будет использован шаблон, определенный в базовом классе.)

Чтобы сообщить о том, что предоставляется новый стиль, следует вызвать метод OverrideMetadata() в статическом конструкторе класса. Этот метод вызывается на свойстве DefaultStyleKeyProperty, которое является свойством зависимости, определяющим стиль по умолчанию для элемента управления. Необходимый код выглядит так:

DefaultStyleKeyProperty.OverrideMetadata(typeof(AdressCustomControl),
new FrameworkPropertyMetadata(typeof(AdressCustomControl))); 

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

Шаг 2. Рефакторинг кода разметки указателя цвета

После добавления вызова OverrideMetadata понадобится просто подключить правильный стиль. Этот стиль должен быть помещен в словарь ресурсов по имени generic.xaml, который следует сохранить в папке Themes проекта. Таким образом, этот стиль будет распознан как стиль по умолчанию для элемента управления. Для добавления файла generic.xaml выполните следующее:

  1. Щелкните правой кнопкой мыши на проекте библиотеки классов в окне Solution Explorer и выберите в контекстном меню пункт Add -> New Folder. Назовите новую папку Themes.
  2. Щелкните правой кнопкой мыши на папке Themes и выберите в контекстном меню пункт Add -> New Item.
  3. В диалоговом окне Add New Item выберите вариант XML file template, введите имя generic.xaml и щелкните на кнопке Add.

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

<ResourceDictionary 
    xmlns=http://schemas.microsoft.com/winfx/200 6/xaml/presentation
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <ResourceDictionary.MergedDictionaries> 
        <ResourceDictionary Source="/themes/AdressCustomControl.xaml"/> 
    </ResourceDictionary.MergedDictionaries> 
</ResourceDictionary> 

Стиль пользовательского элемента управления должен использовать атрибут TargetType для автоматического присоединения себя к типу. Ниже приведена базовая структура разметки, которая находится в файле AdressCustomControl.xaml:

<ResourceDictionary 
    xmlns=http://schemas.microsoft.com/winfx/200 6/xaml/presentation
    xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml
    xmlns:local="clr-namespace:Example">
    <Style TargetType="{x:Type local:AdressCustomControl}">
        <- Стиль CustomControl ->
    </Style> 
</ResourceDictionary> 

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

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

  1. При создании выражений привязки, которые связываются со свойствами родительского класса элемента управления, нельзя использовать свойство ElementName. Вместо него нужно применять свойство RelativeSource для указания того, что необходимо привязаться к родительскому элементу управления. Если все, что необходимо — это однонаправленная привязка, обычно можно использовать облегченное расширение разметки TemplateBinding вместо полноценного Binding.
  2. Присоединять обработчики событий в шаблоне элемента управления не допускается. Вместо этого потребуется назначить элементам узнаваемые имена и присоединять к ним обработчики событий программно в конструкторе элемента управления.
  3. Не именуйте элемент в шаблоне элемента управления, если только не хотите присоединить обработчик событий для программного взаимодействия с ним. При необходимости именования элемента называйте его в стиле PART_ИмяЭлемента.

Принимая во внимание перечисленные соображения, можно создать следующий шаблон для нашей адресной формы.

<Style TargetType="{x:Type local:AdressCustomControl}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:AdressCustomControl}">
                <Border BorderBrush="Black"  
                        BorderThickness="1" 
                        Margin="{TemplateBinding Padding}">
                  <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="10" />
                        <RowDefinition Height="auto" />
                        <RowDefinition Height="auto" />
                        <RowDefinition Height="auto" />
                        <RowDefinition Height="auto" />
                        <RowDefinition Height="auto" />
                        <RowDefinition Height="10" />
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="10" />
                        <ColumnDefinition Width="auto" />
                        <ColumnDefinition Width="10" />
                        <ColumnDefinition Width="auto"/>
                        <ColumnDefinition Width="10" />
                    </Grid.ColumnDefinitions>
                   <TextBlock Grid.Row="2" Grid.Column="1"
                        Text="Location:"/> 
                     <TextBox Grid.Row="2" Grid.Column="3"
                        Text ="{Binding Path=Location, RelativeSource={
                                        RelativeSource TemplatedParent}}" />

                     <TextBlock Grid.Row="3" Grid.Column="1"
                        Text="Address1" />
                     <TextBox Grid.Row="3" Grid.Column="3"
                        Text="{Binding Path=Address1, RelativeSource={
                                       RelativeSource TemplatedParent}}" />

                     <TextBox Grid.Row="4" Grid.Column="1"
                        Text="Address2"/>
                     <TextBox Grid.Row="4" Grid.Column="3"
                        Text="{Binding Path=Address2, RelativeSource={
                                       RelativeSource TemplatedParent}}" />

                     <TextBlock Grid.Row="5" Grid.Column="1"
                        Text="City, State, Zip " />
                     <TextBox Grid.Row="5" Grid.Column="3"
                        Text="{Binding Path=State, RelativeSource={
                                       RelativeSource TemplatedParent}}" />
                  </Grid>
                </Border>
            </ControlTemplate> 
        </Setter.Value> 
    </Setter> 
</Style> 

Как видите, некоторые выражения привязки заменены расширением TemplateBinding. Другие по-прежнему используют расширение Binding, но имеют свойство RelativeSource, указывающее на родителя шаблона (пользовательский элемент управления). Хотя и TemplateBinding, и Binding с RelativeSource из TemplatedParent служат одной и той же цели — извлечению данных из свойств пользовательского элемента управления, все же облегченный TemplateBinding более предпочтителен. Однако он не будет работать, если нужна двунаправленная привязка (как в случае с адресом).

Анисимов Михаил
Анисимов Михаил
Украина
Наталия Шаститко
Наталия Шаститко
Украина, Днепропетровск, Днепропетровский Гуманитарный Университет, 2014