Руководство для начинающих. Часть 6

Предисловие и благодарности

Я программист .NET. Я много программирую на VB.NET и C#, ASP.NET/Winforms/WPF/WCF Flash Silverlight. Но когда начал писать данные статьи, я, естественно, выбрал мой любимый язык — C#. Через некоторое время я получил сообщение от одного человека с просьбой публиковать исходный код на VB.NET и C# в статьях этой серии. Я ответил, что у меня нет времени. И тогда этот человек, Роберт Рэнк (Robert Ranck), вызвался помочь с преобразованием моих исходных проектов на C# в VB.NET.

За этот и последующие проекты VB.NET следует благодарить Роберта Рэнка. Спасибо, Роберт! Ваше участие, несомненно, сделает эту серию более доступной для всех разработчиков .NET.

Также я хотел бы выразить благодарность Карлу Шифлету (Karl Shifflett) (плодовитый автор статей и блогов, также известный под псевдонимом Molenator) за его ответы на мои глупые вопросы по VB.NET. Замечу, что Карл недавно приступил к написанию серии более продвинутых статей по WPF (примеры в которых будут пока на VB.NET, но, надеюсь, появятся и на C#). У Карла должна получиться отличная серия статей, и я прошу всех поддержать его работу. Непросто заставить себя писать всю серию на одном языке, не говоря уже о двух. Первую статью Карла можно прочитать здесь. Лично мне она нравится.

Введение

Это моя шестая статья из серии статей о WPF для начинающих. На этот раз мы поговорим о стилях и шаблонах. А вот предполагаемое содержание этой серии:

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

  • Что такое стили
  • Примеры стилей в демонстрационном приложении
  • Что такое шаблоны
  • Примеры шаблонов в демонстрационном приложении
  • «Безликие» элементы управления

Я не буду рассматривать использование анимаций в стилях и шаблонах. В статье Джоша Смита приводится отличный пример на эту тему. А в библиотеке MSDN об этом есть хорошая статья.

О чем эта статья

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

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

В платформе WPF эти проблемы решены за счет двух концепций разработки интерфейса пользователя, называемых СТИЛИ и ШАБЛОНЫ. В этой статье рассмотрены обе эти концепции в среде WPF.

Что такое стили

Обзор

Стили позволяют разработчику WPF вести общий список значений свойств в удобном месте. До некоторой степени они напоминают таблицы CSS в веб-разработке. Как правило, стили находятся в разделе ресурсов или в отдельном словаре ресурсов. Благодаря стилям в платформе WPF реализуются элементы управления с поддержкой темы. Об этом есть отличная запись в блоге Chaz.

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

Элемент Style обладает следующими свойствами:

Свойство Описание
BasedOn Возвращает или задает определенный стиль, являющийся основой текущего стиля
Dispatcher Возвращает объект Dispatcher, с которым связан этот объект DispatcherObject (наследуемый от DispatcherObject)
IsSealed Возвращает значение, указывающее, доступен ли стиль только для чтения
Resources Возвращает или задает коллекцию ресурсов, которые могут использоваться в области видимости данного стиля
TargetType Возвращает или задает тип, для которого предназначен данный стиль
Setters Возвращает коллекцию объектов Setter и EventSetter
Triggers Возвращает коллекцию объектов TriggerBase, применяющих значения свойств на основе заданных условий

Из этих свойств самые важные следующие:

  • BasedOn
  • TargetType
  • Setters
  • Triggers

Я полагаю, стоит коротко остановиться на синтаксисе каждого из них.

BasedOn

Это в некотором смысле напоминает наследование, когда один стиль наследует общие свойства другого. Каждый стиль поддерживает только одно значение BasedOn. Вот небольшой пример.

<Style x:Key="Style1">

...

</Style>

<Style x:Key="Style2" BasedOn="{StaticResource Style1}">

...

</Style>

TargetType

Свойство TargetType ограничивает типы элементов управления, которые могут использовать данный стиль. Например, если свойству TargetType объекта Style присвоено значение Button, этот стиль нельзя использовать для элемента управления типа TextBox.

Задать свойство TargetType следует так:

<Style TargetType="{x:Type Button}">

....

</Style>

Setters

Объекты Setter действительно очень простые. Они задают событию или свойству некоторое значение. При задании события они подключают это событие. При задании свойства они присваивают свойству значение.

Объекты EventSetter используются для задания событий, например для связывания события Clickэлемента Button, как в примере ниже.

<Style TargetType="{x:Type Button}">

    <EventSetter Event="Click" Handler="b1SetColor"/>

</Style>

Однако значительно чаще объекты Setterиспользуются просто для задания значений свойствам. Например, следующим образом.

<Style TargetType="{x:Type Button}">

    <Setter Property="BackGround" Value="Yellow"/>

</Style>

Синтаксис элемента свойства

Бывают ситуации, когда необходимо, чтобы значение было не единичным, а сложным набором с множеством элементов XAML. Для этого в XAML используется синтаксис элемента свойства. В стилях такой тип синтаксиса чаще всего используется в объекте Setter для свойства Template. Например, так.

<!-- Стиль элемента вкладки -->

<Style x:Key="TabItemStyle1" TargetType="{x:Type TabItem}">

<Setter Property="FocusVisualStyle" Value="{StaticResource TabItemFocusVisual}"/>

<Setter Property="Foreground" Value="Black"/>

<Setter Property="Padding" Value="6,1,6,1"/>

<Setter Property="BorderBrush" Value="{StaticResource TabControlNormalBorderBrush}"/>

<Setter Property="Background" Value="{StaticResource ButtonNormalBackground}"/>

<Setter Property="HorizontalContentAlignment" Value="Stretch"/>

<Setter Property="VerticalContentAlignment" Value="Stretch"/>

<Setter Property="Template">

    <Setter.Value>

        <ControlTemplate TargetType="{x:Type TabItem}">

            <Grid SnapsToDevicePixels="true" Margin="0,5,0,0">

                .....

                .....

                .....

            </Grid>

        </ControlTemplate>

    </Setter.Value>

</Setter>

</Style>

Самая важная часть здесь — та, где объект Setterразделен на несколько строк с помощью синтаксиса значения свойства.

<Setter Property="Template">

      <Setter.Value>

       .....

       .....

       .....

    </Setter.Value>

</Setter>

Объекты Trigger

Модель стилей и шаблонов WPF позволяет задать в стиле необходимые объекты Trigger. По сути, объекты Trigger позволяют применять изменения при выполнении определенных условий (например, когда значение некоторого свойства становится равным true или когда происходит некоторое событие).

В следующем примере показан именованный стиль, доступный для элементов управления Button. В стиле определен элемент Trigger, который изменяет свойство Foreground элемента Button, когда свойство IsPressed равно true.

<Style x:Key="Triggers" TargetType="Button">

    <Style.Triggers>

        <Trigger Property="IsPressed" Value="true">

        <Setter Property = "Foreground" Value="Green"/>

        </Trigger>

    </Style.Triggers>

</Style>

Существуют и другие типы объектов Trigger, которые могут использоваться в стилях.

Объект DataTrigger

Представляет объект Trigger, который применяет значения свойств или выполняет действия, когда связанные данные отвечают определенному условию.

В примере ниже объект DataTrigger задается так, что если свойство State элемента данных Place равно WA, то передний план элемента ListBoxItem становится красным.

<Style TargetType="ListBoxItem">

    <Style.Triggers>

      <DataTrigger Binding="{Binding Path=State}" Value="WA">

        <Setter Property="Foreground" Value="Red" />

      </DataTrigger>

    </Style.Triggers>

</Style>

Существует также специальный тип триггера, который при проверке условия использует несколько значений. Он называется Multitrigger. Он использует несколько условий в одном объекте MultiDataTrigger. Ниже приведен пример.

  <Style TargetType="ListBoxItem">

    <Style.Triggers>

      <MultiDataTrigger>

        <MultiDataTrigger.Conditions>

          <Condition Binding="{Binding Path=Name}" Value="Portland" />

          <Condition Binding="{Binding Path=State}" Value="OR" />

        </MultiDataTrigger.Conditions>

        <Setter Property="Background" Value="Cyan" />

      </MultiDataTrigger>

    </Style.Triggers>

  </Style>

В этом примере у связанного объекта свойство Name должно быть равно Portland, а свойство State — OR, чтобы передний план элемента ListBoxItem стал красным.

Объект EventTrigger

Эти специальные триггеры выполняет набор действий в ответ на событие. Особенность таких триггеров в том, что они могут ТОЛЬКО инициировать анимации. В отличие от обычных триггеров они не позволяют задавать обычные свойства. Пример объекта Eventrigger приведен ниже.

<EventTrigger RoutedEvent="Mouse.MouseEnter">

  <EventTrigger.Actions>

    <BeginStoryboard>

      <Storyboard>

        <DoubleAnimation

          Duration="0:0:0.2"

          Storyboard.TargetProperty="MaxHeight"

          To="90"  />

      </Storyboard>

    </BeginStoryboard>

  </EventTrigger.Actions>

</EventTrigger>

<EventTrigger RoutedEvent="Mouse.MouseLeave">

  <EventTrigger.Actions>

    <BeginStoryboard>

      <Storyboard>

        <DoubleAnimation

          Duration="0:0:1"

          Storyboard.TargetProperty="MaxHeight"  />

      </Storyboard>

    </BeginStoryboard>

  </EventTrigger.Actions>

</EventTrigger>

Примеры стилей в демонстрационном приложении

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

Ниже дан пример стиля, настроенного и предназначенного для элементов TabItem. Это полноценный стиль, поэтому в нем также используются шаблоны и другие элементы, которые мы только что обсудили (объекты Setter/TargetType и синтаксис элемента свойства, но без триггеров).

<!-- Стиль элемента вкладки -->

<Style x:Key="TabItemStyle1" TargetType="{x:Type TabItem}">

    <Setter Property="FocusVisualStyle" Value="{StaticResource TabItemFocusVisual}"/>

    <Setter Property="Foreground" Value="Black"/>

    <Setter Property="Padding" Value="6,1,6,1"/>

    <Setter Property="BorderBrush" Value="{StaticResource TabControlNormalBorderBrush}"/>

    <Setter Property="Background" Value="{StaticResource ButtonNormalBackground}"/>

    <Setter Property="HorizontalContentAlignment" Value="Stretch"/>

    <Setter Property="VerticalContentAlignment" Value="Stretch"/>

    <Setter Property="HeaderTemplate">

        <Setter.Value>

            <DataTemplate>

                <StackPanel Orientation="Horizontal">

                    <TextBlock Text="{Binding}"/>

                        <Button x:Name="btnClose" Margin="10,3,3,3"

                                Template="{StaticResource closeButtonTemplate}"

                                Background="{StaticResource buttonNormalBrush}"

                                IsEnabled="True"/>

                </StackPanel>

            </DataTemplate>

        </Setter.Value>

    </Setter>

    <Setter Property="Template">

        <Setter.Value>

            <ControlTemplate TargetType="{x:Type TabItem}">

                <Grid SnapsToDevicePixels="true" Margin="0,5,0,0">

                    <Border x:Name="Bd" Background="{TemplateBinding Background}"

                            BorderBrush="{TemplateBinding BorderBrush}"

                            BorderThickness="1,1,1,0" CornerRadius="10,10,0,0"

                            Padding="{TemplateBinding Padding}">

                        <ContentPresenter SnapsToDevicePixels=

                            "{TemplateBinding SnapsToDevicePixels}"

                            HorizontalAlignment="{Binding

                            Path=HorizontalContentAlignment,

                            RelativeSource={RelativeSource

                            AncestorType={x:Type ItemsControl}}}"

                            x:Name="Content" VerticalAlignment="

                            {Binding Path=VerticalContentAlignment,

                            RelativeSource={RelativeSource

                            AncestorType={x:Type ItemsControl}}}"

                            ContentSource="Header" RecognizesAccessKey="True"/>

                    </Border>

                </Grid>

                <ControlTemplate.Triggers>

                    <Trigger Property="IsMouseOver" Value="true">

                        <Setter Property="Background" TargetName="Bd"

                            Value="{StaticResource TabItemHotBackground}"/>

                    </Trigger>

                    <Trigger Property="IsSelected" Value="true">

                        <Setter Property="Panel.ZIndex" Value="1"/>

                        <Setter Property="Background" TargetName="Bd"

                            Value="{StaticResource TabItemSelectedBackground}"/>

                    </Trigger>

                    <MultiTrigger>

                        <MultiTrigger.Conditions>

                            <Condition Property="IsSelected" Value="false"/>

                            <Condition Property="IsMouseOver" Value="true"/>

                        </MultiTrigger.Conditions>

                        <Setter Property="BorderBrush" TargetName="Bd"

                                Value="{StaticResource TabItemHotBorderBrush}"/>

                    </MultiTrigger>

                    <Trigger Property="TabStripPlacement" Value="Bottom">

                        <Setter Property="BorderThickness" TargetName="Bd" Value="1,0,1,1"/>

                    </Trigger>

                    <Trigger Property="TabStripPlacement" Value="Left">

                        <Setter Property="BorderThickness" TargetName="Bd" Value="1,1,0,1"/>

                    </Trigger>

                    <Trigger Property="TabStripPlacement" Value="Right">

                        <Setter Property="BorderThickness" TargetName="Bd" Value="0,1,1,1"/>

                    </Trigger>

                    <MultiTrigger>

                        <MultiTrigger.Conditions>

                            <Condition Property="IsSelected" Value="true"/>

                            <Condition Property="TabStripPlacement" Value="Top"/>

                        </MultiTrigger.Conditions>

                        <Setter Property="Margin" Value="-2,-2,-2,-1"/>

                        <Setter Property="Margin" TargetName="Content" Value="0,0,0,1"/>

                    </MultiTrigger>

                    <MultiTrigger>

                        <MultiTrigger.Conditions>

                            <Condition Property="IsSelected" Value="true"/>

                            <Condition Property="TabStripPlacement" Value="Bottom"/>

                        </MultiTrigger.Conditions>

                        <Setter Property="Margin" Value="-2,-1,-2,-2"/>

                        <Setter Property="Margin" TargetName="Content" Value="0,1,0,0"/>

                    </MultiTrigger>

                    <MultiTrigger>

                        <MultiTrigger.Conditions>

                            <Condition Property="IsSelected" Value="true"/>

                            <Condition Property="TabStripPlacement" Value="Left"/>

                        </MultiTrigger.Conditions>

                        <Setter Property="Margin" Value="-2,-2,-1,-2"/>

                        <Setter Property="Margin" TargetName="Content" Value="0,0,1,0"/>

                    </MultiTrigger>

                    <MultiTrigger>

                        <MultiTrigger.Conditions>

                            <Condition Property="IsSelected" Value="true"/>

                            <Condition Property="TabStripPlacement" Value="Right"/>

                        </MultiTrigger.Conditions>

                        <Setter Property="Margin" Value="-1,-2,-2,-2"/>

                        <Setter Property="Margin" TargetName="Content" Value="1,0,0,0"/>

                    </MultiTrigger>

                    <Trigger Property="IsEnabled" Value="false">

                        <Setter Property="Background" TargetName="Bd"

                                Value="{StaticResource TabItemDisabledBackground}"/>

                        <Setter Property="BorderBrush" TargetName="Bd"

                                Value="{StaticResource TabItemDisabledBorderBrush}"/>

                        <Setter Property="Foreground"

                                Value="{DynamicResource

                            {x:Static SystemColors.GrayTextBrushKey}}"/>

                    </Trigger>

                </ControlTemplate.Triggers>

            </ControlTemplate>

        </Setter.Value>

    </Setter>

</Style>

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

Демонстрационное приложение также содержит стили и шаблоны для создания полностью нового визуального представления элементов управления ScrollBar и ScrollViewer. Они показаны на рисунке ниже: слева элемент ScrollBar, справа элемент ScrollViewer.

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

<!-- Кисти-->

<LinearGradientBrush x:Key="VerticalScrollBarBackground"

                     EndPoint="1,0" StartPoint="0,0">

    <GradientStop Color="#E1E1E1" Offset="0"/>

    <GradientStop Color="#EDEDED" Offset="0.20"/>

    <GradientStop Color="#EDEDED" Offset="0.80"/>

    <GradientStop Color="#E3E3E3" Offset="1"/>

</LinearGradientBrush>

<LinearGradientBrush x:Key="HorizontalScrollBarBackground"

                     EndPoint="0,1" StartPoint="0,0">

    <GradientStop Color="#E1E1E1" Offset="0"/>

    <GradientStop Color="#EDEDED" Offset="0.20"/>

    <GradientStop Color="#EDEDED" Offset="0.80"/>

    <GradientStop Color="#E3E3E3" Offset="1"/>

</LinearGradientBrush>

<LinearGradientBrush x:Key="ListBoxBackgroundBrush"

StartPoint="0,0" EndPoint="1,0.001">

    <GradientBrush.GradientStops>

        <GradientStopCollection>

            <GradientStop Color="White" Offset="0.0" />

            <GradientStop Color="White" Offset="0.6" />

            <GradientStop Color="#DDDDDD" Offset="1.2"/>

        </GradientStopCollection>

    </GradientBrush.GradientStops>

</LinearGradientBrush>

<LinearGradientBrush x:Key="StandardBrush"

StartPoint="0,0" EndPoint="0,1">

    <GradientBrush.GradientStops>

        <GradientStopCollection>

            <GradientStop Color="#FFF" Offset="0.0"/>

            <GradientStop Color="#CCC" Offset="1.0"/>

        </GradientStopCollection>

    </GradientBrush.GradientStops>

</LinearGradientBrush>

<LinearGradientBrush x:Key="PressedBrush"

StartPoint="0,0" EndPoint="0,1">

    <GradientBrush.GradientStops>

        <GradientStopCollection>

            <GradientStop Color="#BBB" Offset="0.0"/>

            <GradientStop Color="#EEE" Offset="0.1"/>

            <GradientStop Color="#EEE" Offset="0.9"/>

            <GradientStop Color="#FFF" Offset="1.0"/>

        </GradientStopCollection>

    </GradientBrush.GradientStops>

</LinearGradientBrush>

<SolidColorBrush x:Key="ScrollBarDisabledBackground" Color="#F4F4F4"/>

<SolidColorBrush x:Key="StandardBorderBrush" Color="#888" />

<SolidColorBrush x:Key="StandardBackgroundBrush" Color="#FFF" />

<SolidColorBrush x:Key="HoverBorderBrush" Color="#DDD" />

<SolidColorBrush x:Key="SelectedBackgroundBrush" Color="Gray" />

<SolidColorBrush x:Key="SelectedForegroundBrush" Color="White" />

<SolidColorBrush x:Key="DisabledForegroundBrush" Color="#888" />

<SolidColorBrush x:Key="NormalBrush" Color="#888" />

<SolidColorBrush x:Key="NormalBorderBrush" Color="#888" />

<SolidColorBrush x:Key="HorizontalNormalBrush" Color="#888" />

<SolidColorBrush x:Key="HorizontalNormalBorderBrush" Color="#888" />

<SolidColorBrush x:Key="GlyphBrush" Color="#444" />

<!-- Кнопка вертикальной полосы прокрутки -->

<Style x:Key="VerticalScrollBarPageButton" TargetType="{x:Type RepeatButton}">

    <Setter Property="OverridesDefaultStyle" Value="true"/>

    <Setter Property="Background" Value="Transparent"/>

    <Setter Property="Focusable" Value="false"/>

    <Setter Property="IsTabStop" Value="false"/>

    <Setter Property="Template">

        <Setter.Value>

            <ControlTemplate TargetType="{x:Type RepeatButton}">

                ...

                ...

              </ControlTemplate>

        </Setter.Value>

    </Setter>

</Style>

<!-- Кнопка горизонтальной полосы прокрутки -->

<Style x:Key="HorizontalScrollBarPageButton" TargetType="{x:Type RepeatButton}">

    <Setter Property="OverridesDefaultStyle" Value="true"/>

    <Setter Property="Background" Value="Transparent"/>

    <Setter Property="Focusable" Value="false"/>

    <Setter Property="IsTabStop" Value="false"/>

    <Setter Property="Template">

        <Setter.Value>

            <ControlTemplate TargetType="{x:Type RepeatButton}">

                ...

                ...

            </ControlTemplate>

        </Setter.Value>

    </Setter>

</Style>

<!-- Кнопки прокрутки вниз -->

<Style x:Key="RepeatButtonStyleDown" TargetType="{x:Type RepeatButton}">

    <Setter Property="OverridesDefaultStyle" Value="true"/>

    <Setter Property="Focusable" Value="false"/>

    <Setter Property="IsTabStop" Value="false"/>

    <Setter Property="Template">

        <Setter.Value>

            <ControlTemplate TargetType="{x:Type RepeatButton}">

                ...

                ...

            </ControlTemplate>

        </Setter.Value>

    </Setter>

</Style>

<!-- Кнопки прокрутки вверх -->

<Style x:Key="RepeatButtonStyleUp" TargetType="{x:Type RepeatButton}">

    <Setter Property="OverridesDefaultStyle" Value="true"/>

    <Setter Property="Focusable" Value="false"/>

    <Setter Property="IsTabStop" Value="false"/>

    <Setter Property="Template">

        <Setter.Value>

            <ControlTemplate TargetType="{x:Type RepeatButton}">

                ...

                ...

              </ControlTemplate>

        </Setter.Value>

    </Setter>

</Style>

<!-- Стиль бегунка прокрутки -->

<Style x:Key="ThumbStyle1" TargetType="{x:Type Thumb}">

    <Setter Property="OverridesDefaultStyle" Value="true"/>

    <Setter Property="IsTabStop" Value="false"/>

    <Setter Property="Template">

        <Setter.Value>

            <ControlTemplate TargetType="{x:Type Thumb}">

                ...

                ...

              </ControlTemplate>

        </Setter.Value>

    </Setter>

</Style>

<!-- Стиль полосы прокрутки -->

<Style x:Key="ScrollBarStyle1" TargetType="{x:Type ScrollBar}">

    <Setter Property="Background"

            Value="{StaticResource VerticalScrollBarBackground}"/>

    <Setter Property="Stylus.IsPressAndHoldEnabled" Value="false"/>

    <Setter Property="Stylus.IsFlicksEnabled" Value="false"/>

    <Setter Property="Foreground"

            Value="{DynamicResource

        {x:Static SystemColors.ControlTextBrushKey}}"/>

    <Setter Property="Width"

            Value="{DynamicResource

        {x:Static SystemParameters.VerticalScrollBarWidthKey}}"/>

    <Setter Property="MinWidth"

            Value="{DynamicResource

        {x:Static SystemParameters.VerticalScrollBarWidthKey}}"/>

    <Setter Property="Template">

        <Setter.Value>

            <ControlTemplate TargetType="{x:Type ScrollBar}">

                ...

                ...

              </ControlTemplate>

        </Setter.Value>

    </Setter>

    <Style.Triggers>

        <Trigger Property="Orientation" Value="Horizontal">

            <Setter Property="Width" Value="Auto"/>

            <Setter Property="MinWidth" Value="0"/>

            <Setter Property="Height" Value="{DynamicResource

                {x:Static SystemParameters.HorizontalScrollBarHeightKey}}"/>

            <Setter Property="MinHeight"

                    Value="{DynamicResource

                {x:Static SystemParameters.HorizontalScrollBarHeightKey}}"/>

            <Setter Property="Background"

                    Value="{StaticResource HorizontalScrollBarBackground}"/>

            <Setter Property="Template">

                <Setter.Value>

                    <ControlTemplate TargetType="{x:Type ScrollBar}">

                         ...

                        ...

                    </ControlTemplate>

                </Setter.Value>

            </Setter>

        </Trigger>

    </Style.Triggers>

</Style>

<!-- Кнопка повторения полосы прокрутки -->

<Style x:Key="ScrollBarLineButton" TargetType="{x:Type RepeatButton}">

    <Setter Property="SnapsToDevicePixels" Value="True"/>

    <Setter Property="OverridesDefaultStyle" Value="true"/>

    <Setter Property="Focusable" Value="false"/>

    <Setter Property="Template">

        <Setter.Value>

            <ControlTemplate TargetType="{x:Type RepeatButton}">

                 ...

                ...

              </ControlTemplate>

        </Setter.Value>

    </Setter>

</Style>

<!-- Кнопка повторения полосы прокрутки -->

<Style x:Key="ScrollBarPageButton" TargetType="{x:Type RepeatButton}">

    <Setter Property="SnapsToDevicePixels" Value="True"/>

    <Setter Property="OverridesDefaultStyle" Value="true"/>

    <Setter Property="IsTabStop" Value="false"/>

    <Setter Property="Focusable" Value="false"/>

    <Setter Property="Template">

        <Setter.Value>

            <ControlTemplate TargetType="{x:Type RepeatButton}">

                ...

                ...

              </ControlTemplate>

        </Setter.Value>

    </Setter>

</Style>

<!-- Стиль бегунка прокрутки -->

<Style x:Key="ScrollBarThumb" TargetType="{x:Type Thumb}">

    <Setter Property="SnapsToDevicePixels" Value="True"/>

    <Setter Property="OverridesDefaultStyle" Value="true"/>

    <Setter Property="IsTabStop" Value="false"/>

    <Setter Property="Focusable" Value="false"/>

    <Setter Property="Template">

        <Setter.Value>

            <ControlTemplate TargetType="{x:Type Thumb}">

                ...

                ...

              </ControlTemplate>

        </Setter.Value>

    </Setter>

</Style>

<!-- Шаблон вертикальной полосы прокрутки -->

<ControlTemplate x:Key="VerticalScrollBar" TargetType="{x:Type ScrollBar}">

                ...

                ...

</ControlTemplate>

<!-- Стиль полосы прокрутки -->

<Style x:Key="{x:Type ScrollBar}" TargetType="{x:Type ScrollBar}">

    <Setter Property="SnapsToDevicePixels" Value="True"/>

    <Setter Property="OverridesDefaultStyle" Value="true"/>

    <Style.Triggers>

        <Trigger Property="Orientation" Value="Horizontal">

            <Setter Property="Width" Value="Auto"/>

            <Setter Property="Height" Value="18" />

            <Setter Property="Template"

        Value="{StaticResource HorizontalScrollBar}" />

        </Trigger>

        <Trigger Property="Orientation" Value="Vertical">

            <Setter Property="Width" Value="18"/>

            <Setter Property="Height" Value="Auto" />

            <Setter Property="Template"

        Value="{StaticResource VerticalScrollBar}" />

        </Trigger>

    </Style.Triggers>

</Style>

<!-- Шаблон элемента ScrollViewer -->

<ControlTemplate x:Key="ScrollViewerControlTemplate" TargetType="{x:Type ScrollViewer}">

        ...

        ...

</ControlTemplate>

Только сумасшедший (Джош и Карл, без обид) будет делать это вручную. Мы пойдем другим путем. Вообще, я не люблю инструменты, но иногда без них просто не обойтись. Чтобы не сойти с ума, выполняя все это собственноручно, лучше запустить Expression Blend и изменить в нем шаблоны. Вам все равно нужно будет знать свой код XAML, но Expression Blend, несомненно, поможет в этой области.

Естественно, потребуется немало разметки. Но принцип остается тем же. В стиле есть только объекты Setter и Trigger, о которых мы уже говорили, так что все должно быть ясно.

Организация демонстрационного приложения

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

У него следующая структура.

VariousControlTemplatesWindow.xaml

Содержит примеры изменения стиля кнопки, вкладок и полос прокрутки.

Вот снимок экрана, который вы еще не видели.

HierarchicalDataTemplateWindow.xaml

Содержит примеры изменения стиля иерархических данных. Вот снимок экрана.

DemoLauncherWindow.xaml

Содержит пример использования стиля для данных на основе элементов.

PlanetsListBoxWindow.xaml (список планет Беатриц Коста)

Содержит примеры реального использования стилей и шаблонов. Он используется с разрешения Беатриц Коста (Beatriz Costa) из корпорации Microsoft. Я мог бы сделать такое сам, но Беатриц уже проделала всю работу, которая превосходно подходит для демонстрации мощных возможностей стилей и шаблонов. Спасибо, Би, я у тебя в долгу.

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

Что такое шаблоны

Если все элементы управления одинаково оформлены, что перед нами? Правильно, шаблон. Но как он выглядит? В приложении Expression Blend можно увидеть шаблон при его редактировании, как показано на снимке экрана ниже, где я редактирую элемент управления ScrollViewer.

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

Некоторое время назад я достаточно подробно (мы ВСЕГДА можем сделать лучше) говорил об элементе управления ScrollViewer в моем блоге. Также стоит упомянуть статью Примеры ControlTemplate в библиотеке MSDN, где можно посмотреть, как выглядит каждый из стандартных элементов управления WPF.

Обзор

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

Не так-то просто точно сформулировать, что будет внутри шаблона, поскольку это зависит от его типа.

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

Я буду упоминать такие вещи, как шаблоны DataTempate/HierarchicalDataTemplate, однако, если вы не знаете, что это такое, не стоит беспокоиться. Мы рассмотрим их после базового синтаксиса шаблонов. Чтобы разобраться с WPF, придется научиться азам, отринуть все земное, побрить голову и стать монахом. У них хотя бы есть вино. Ну да ладно.

Свойство Triggers

У элементов Style, ControlTemplate и DataTemplate есть свойство Triggers, которое может содержать набор триггеров. Триггер задает свойства или инициирует действия, например анимацию, при изменении значений свойства или возникновении события. Мы уже видели свойство Triggers в стилях, а как обстоит дело с шаблонами? Тут все практически то же самое. Типов триггеров не так уж много. Остановимся на них подробнее.

Триггеры свойств

Используются для присвоения свойству значения при выполнении определенных условий. В этом примере свойству Border.opacity присваивается значение 0.4, когда значение свойства Button.IsEnabled равно false.

<!-- Простая кнопка с некоторыми событиями мыши и захватом свойства-->

<ControlTemplate x:Key="bordereredButtonTemplateWithMouseAndPropHiJacking" TargetType="{x:Type Button}">

    <Border x:Name="border" CornerRadius="3" Background="{TemplateBinding Background}"

            BorderBrush="{TemplateBinding Foreground}"

            BorderThickness="2" Width="auto" Visibility="Visible">

        ....

        ....

    </Border>

    <ControlTemplate.Triggers>

        <Trigger Property="IsEnabled" Value="false">

            <Setter TargetName="border" Property="Opacity" Value="0.4"/>

        </Trigger>

    </ControlTemplate.Triggers>

</ControlTemplate>

Триггеры событий

Работают так же, как в стилях. Мы уже рассматривали их выше.

Триггеры MultiTrigger, DataTrigger и MultiDataTrigger

Кроме триггеров Trigger и EventTrigger есть и другие типы. Триггер MultiTrigger позволяет задавать значение свойств при выполнении нескольких условий. Триггеры DataTrigger и MultiDataTrigger используются, когда свойство условия привязано к данным.

<!-- Шаблон типа DemoListItem для элемента Listbox -->

<DataTemplate x:Key="demoItemTemplate" DataType="x:Type local:DemoListItem">

    <StackPanel Orientation="Horizontal" Margin="10">

        <Path Name="pathSelected" Fill="White" Stretch="Fill" Stroke="White" Width="15"

            Height="20" Data="M0,0 L 0,15 L 7.5,7.5"

            Visibility="Hidden"/>

        <Border BorderBrush="White" BorderThickness="4" Margin="5">

            <Image Source="Images/DataLogo.png" Width="45" Height="45"/>

        </Border>

        <StackPanel Orientation="Vertical" VerticalAlignment="Center">

            <TextBlock FontFamily="Arial Black" FontSize="20"

                   FontWeight="Bold"

                   Width="auto" Height="auto"

                   Text="{Binding Path=DemoName}"    />

            <TextBlock FontFamily="Arial" FontSize="10"

                   FontWeight="Normal"

                   Width="auto" Height="auto"

                   Text="{Binding Path=WindowName}" />

        </StackPanel>

    </StackPanel>

    <DataTemplate.Triggers>

        <DataTrigger

            Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor,

            AncestorType={x:Type ListBoxItem}, AncestorLevel=1}, Path=IsSelected}" Value="True">

            <Setter TargetName="pathSelected" Property="Visibility" Value="Visible"  />

        </DataTrigger>

    </DataTemplate.Triggers>

</DataTemplate>

</Window.Resources>

Это шаблон типа DataTemplate для связанных данных, здесь можно использовать триггеры данных DataTrigger. Этот триггер проверяет, не имеет ли свойство IsSelected привязанных объектов значение true, и, если это так, задает свойство Visibility другого элемента в этом шаблоне данных.

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

<ControlTemplate TargetType="{x:Type TabItem}">

    <Grid SnapsToDevicePixels="true" Margin="0,5,0,0">

        <Border x:Name="Bd" Background="{TemplateBinding Background}"

                BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="1,1,1,0"

                CornerRadius="10,10,0,0" Padding="{TemplateBinding Padding}">

            <ContentPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"

                HorizontalAlignment="{Binding Path=HorizontalContentAlignment,

                RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"

                x:Name="Content" VerticalAlignment="{Binding Path=VerticalContentAlignment,

                RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"

                ContentSource="Header" RecognizesAccessKey="True"/>

        </Border>

    </Grid>

    <ControlTemplate.Triggers>

            <MultiTrigger>

            <MultiTrigger.Conditions>

                <Condition Property="IsSelected" Value="false"/>

                <Condition Property="IsMouseOver" Value="true"/>

            </MultiTrigger.Conditions>

            <Setter Property="BorderBrush" TargetName="Bd"

                    Value="Black"/>

        </MultiTrigger>

        </ControlTemplate.Triggers>

</ControlTemplate>

В этом примере свойствоIsSelected элемента TabItem должно быть равно false, а свойствоIsMouseOver — true, чтобы сработал триггер и присвоил свойству BorderBrush элемента Bd значение Black.

Расширение разметки TemplateBinding

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

Посмотрите страницу в библиотеке MSDN, все очень просто. Мы пытаемся сделать так, чтобы наши элементы управления реагировали в соответствии с потребностями пользователя. Нехорошо, если пользователь задает свойству BackGround элемента значение Blue, а мы предоставляем шаблон, который присваивает свойству BackGround значение Green. Есть способ получше. Мы просто используем расширение разметки {TemplateBinding}, чтобы указать, что шаблон элемента управления получает значение из родительского элемента управления — шаблона. Что-то наподобие этого:

<!-- Простая кнопка с некоторыми событиями мыши и захватом свойства-->

<ControlTemplate x:Key="bordereredButtonTemplateWithMouseAndPropHiJacking" TargetType="{x:Type Button}">

    <Border x:Name="border" CornerRadius="3" Background="{TemplateBinding Background}"

        .....

    </Border>

    <ControlTemplate.Triggers>

    ....

    ....

    </ControlTemplate.Triggers>

</ControlTemplate>

Этого важного выражения Background="{TemplateBinding Background}" достаточно, чтобы в шаблоне элемента управления использовалось то же значение, что и в родительском элементе управления — шаблоне.

Захват свойств

Иногда возникают ситуации, когда нужно использовать несколько свойств из исходного элемента управления источника — шаблона, но нет свойства для необходимого элемента. Представим, что требуется создать шаблон кнопки с текстом и изображением. С текстом все просто: можно применить свойство Button.Content, если оно используется для текста. А где получать значение для изображения? Конечно, можно воспользоваться присоединенным свойством (как описано в моей предыдущей статье о свойствах зависимости), но лучше найти неиспользуемые свойства и захватить их для применения в шаблоне элемента управления. Например, свойство Tag всех элементов управления может принимать в качестве значения объект, что очень удобно.

Затем можно использовать это свойство в привязке, как в примере ниже.

<!-- Простая кнопка с некоторыми событиями мыши и захватом свойства-->

<ControlTemplate x:Key="bordereredButtonTemplateWithMouseAndPropHiJacking" TargetType="{x:Type Button}">

    <Border x:Name="border" CornerRadius="3" Background="{TemplateBinding Background}"

            BorderBrush="{TemplateBinding Foreground}"

            BorderThickness="2" Width="auto" Visibility="Visible">

        <StackPanel Orientation="Horizontal">

            <Image Source="{Binding RelativeSource={RelativeSource TemplatedParent},

                 Path=Tag}" Width="20" Height="20" HorizontalAlignment="Left"

                 Margin="{TemplateBinding Padding}" />

            <ContentPresenter

                Margin="{TemplateBinding Padding}"

                Content="{TemplateBinding Content}"

                Width="auto" Height="auto"/>

        </StackPanel>

    </Border>

    <ControlTemplate.Triggers>

    ....

    ....

    </ControlTemplate.Triggers>

</ControlTemplate>

Заметьте — на самом деле используются оба свойства: свойство Content исходного элемента управления (Button) для текста (он будет показан в объекте ContentPresenter, который способен отображать любую единицу содержимого, исходный текст свойства Button.Context в данном случае) и свойство Tag исходного элемента управления (Button) для изображения в шаблоне.

Чтобы узнать, как использовать в шаблоне присоединенное свойство вместо захвата неиспользуемого свойства, см. отличную запись в блоге Джоша Смита.

Примеры шаблонов

Мы хорошо поработали, осталось еще немного. Мне бы хотелось привести описание нескольких типов шаблонов и в конце рассмотреть довольно интересный пример.

Шаблон ControlTemplate

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

Ниже приведен чрезвычайно простой пример, где стандартный шаблон элемента управления Button заменяется на эллипс, а свойство ContentPresenter используется для отображения свойства Button.Content.

      <ControlTemplate TargetType="Button">

        <Grid>

          <Ellipse Fill="{TemplateBinding Background}"/>

          <ContentPresenter HorizontalAlignment="Center"

                            VerticalAlignment="Center"/>

        </Grid>

      </ControlTemplate>

Ниже приводится пара примеров (снова для элемента Button), использующих понятия, о которых мы говорили выше: триггеры свойств, захват свойств Jacking (это мой термин, а не официальный, так что не пытайтесь искать его), TemplateBinding.

<!-- Простая кнопка с простыми свойствами-->

<ControlTemplate x:Key="bordereredButtonTemplate" TargetType="{x:Type Button}">

    <Border x:Name="border" CornerRadius="3" Background="Transparent"

            BorderBrush="{TemplateBinding Foreground}" BorderThickness="2"

            Width="auto" Visibility="Visible">

        <ContentPresenter  Margin="3"

            Content="{TemplateBinding Content}" Width="auto" Height="auto"/>

    </Border>

    <ControlTemplate.Triggers>

        <Trigger Property="IsEnabled" Value="false">

            <Setter TargetName="border" Property="Opacity" Value="0.4"/>

        </Trigger>

    </ControlTemplate.Triggers>

</ControlTemplate>

<!-- Простая кнопка с некоторыми событиями мыши-->

<ControlTemplate x:Key="bordereredButtonTemplateWithMouseEvents" TargetType="{x:Type Button}">

    <Border x:Name="border" CornerRadius="3" Background="Transparent"

            BorderBrush="{TemplateBinding Foreground}"

            BorderThickness="2" Width="auto" Visibility="Visible">

        <ContentPresenter  Margin="3" Content="{TemplateBinding Content}"

            Width="auto" Height="auto"/>

    </Border>

    <ControlTemplate.Triggers>

        <Trigger Property="IsEnabled" Value="false">

            <Setter TargetName="border" Property="Opacity" Value="0.4"/>

        </Trigger>

        <Trigger Property="IsMouseOver" Value="true">

            <Setter TargetName="border" Property="Background" Value="Orange"/>

        </Trigger>

    </ControlTemplate.Triggers>

</ControlTemplate>

<!-- Простая кнопка с некоторыми событиями мыши и захватом свойства-->

<ControlTemplate x:Key="bordereredButtonTemplateWithMouseAndPropHiJacking"

            TargetType="{x:Type Button}">

    <Border x:Name="border" CornerRadius="3" Background="{TemplateBinding Background}"

            BorderBrush="{TemplateBinding Foreground}"

            BorderThickness="2" Width="auto" Visibility="Visible">

        <StackPanel Orientation="Horizontal">

            <Image Source="{Binding RelativeSource={RelativeSource TemplatedParent},

                 Path=Tag}" Width="20" Height="20" HorizontalAlignment="Left"

                 Margin="{TemplateBinding Padding}" />

            <ContentPresenter

                Margin="{TemplateBinding Padding}"

                Content="{TemplateBinding Content}"

                Width="auto" Height="auto"/>

        </StackPanel>

    </Border>

    <ControlTemplate.Triggers>

        <Trigger Property="IsEnabled" Value="false">

            <Setter TargetName="border" Property="Opacity" Value="0.4"/>

        </Trigger>

        <Trigger Property="IsMouseOver" Value="true">

            <Setter TargetName="border" Property="Background" Value="Orange"/>

        </Trigger>

    </ControlTemplate.Triggers>

</ControlTemplate>

Эти шаблоны элементов управления входят в файл демонстрационного приложения VariousControlTemplatesWindow.xaml и выглядят так:

Дополнительные сведения о шаблонах элементов управления см. здесь.

Шаблон DataTemplate

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

В файле демонстрационного приложения DemoLauncherWindow.xaml я добавил в элемент ListBox множество пользовательских объектов типа DemoListItem.

Объекты DemoListItem определяются так:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Windows;

namespace Styles_And_Templates

{

    /// <summary>

    /// Используется в <see cref="DemoLauncherWindow">

    /// демонстрационном окне запуска</see> в качестве отдельных элементов

    /// списка listBox

    /// </summary>

    public class DemoListItem

    {

        #region Public Properties

        public string WindowName { get; set; }

        public string DemoName { get; set; }

        #endregion

    }

}

И на VB.NET.

Imports System

Imports System.Collections.Generic

Imports System.Linq

Imports System.Text

Imports System.Windows

''' <summary>

''' Используется в <see cref="DemoLauncherWindow">

''' демонстрационном окне запуска</see> в качестве отдельных элементов

''' списка listBox

''' </summary>

Public Class DemoListItem

#Region "Public Properties"

    Private m_WindowName As String

    Public Property WindowName() As String

        Get

            Return m_WindowName

        End Get

        Set(ByVal value As String)

            m_WindowName = value

        End Set

    End Property

    Private m_DemoName As String

    Public Property DemoName() As String

        Get

            Return m_DemoName

        End Get

        Set(ByVal value As String)

            m_DemoName = value

        End Set

    End Property

#End Region

End Class

С этими знаниями можно создать шаблон DataTemplate для списка ListBox, содержащего такие объекты. Посмотрим на код.

<!-- Шаблон типа DemoListItem для элемента Listbox -->

<DataTemplate x:Key="demoItemTemplate" DataType="x:Type local:DemoListItem">

    <StackPanel Orientation="Horizontal" Margin="10">

        <Path Name="pathSelected" Fill="White" Stretch="Fill" Stroke="White" Width="15"

            Height="20" Data="M0,0 L 0,15 L 7.5,7.5"

            Visibility="Hidden"/>

        <Border BorderBrush="White" BorderThickness="4" Margin="5">

            <Image Source="Images/DataLogo.png" Width="45" Height="45"/>

        </Border>

        <StackPanel Orientation="Vertical" VerticalAlignment="Center">

            <TextBlock FontFamily="Arial Black" FontSize="20"

                   FontWeight="Bold"

                   Width="auto" Height="auto"

                   Text="{Binding Path=DemoName}"    />

            <TextBlock FontFamily="Arial" FontSize="10"

                   FontWeight="Normal"

                   Width="auto" Height="auto"

                   Text="{Binding Path=WindowName}" />

        </StackPanel>

    </StackPanel>

    <DataTemplate.Triggers>

        <DataTrigger

            Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor,

            AncestorType={x:Type ListBoxItem}, AncestorLevel=1}, Path=IsSelected}" Value="True">

            <Setter TargetName="pathSelected" Property="Visibility" Value="Visible"  />

        </DataTrigger>

    </DataTemplate.Triggers>

</DataTemplate>

Обратите внимание, что здесь применяются расширения разметки {Binding}, чтобы получить свойства базовых связанных объектов данных для использования в шаблоне DataTemplate. Запомните также, что в шаблоне для связанного объекта данных необходимо использовать триггеры DataTrigger.

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

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

Шаблон HierarchicalDataTemplate

Шаблон HierarchichalDataTemplate — это, по сути, шаблон DataTemplate, который можно использовать для иерархических структур, таких как представление в виде дерева или меню.

Пример из демонстрационного приложения HierarchicalDataTemplateWindow.xaml я нашел в документации MSDN. Основная идея заключается в использовании иерархического списка в качестве источника данных для дерева и меню. Затем применяется шаблон HierarchichalDataTemplate для оптимальной визуализации привязанных данных иерархического списка.

Ниже приведен исходный код на C#.

using System;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Data;

using System.Windows.Documents;

using System.Windows.Media;

using System.Windows.Shapes;

using System.Collections.ObjectModel;

using System.Collections.Generic;

namespace Styles_And_Templates

{

    #region Inner data classes

    public class League

    {

        public string Name { get; private set; }

        public List<Division> Divisions { get; private set; }

        public League(string name)

        {

            Name = name;

            Divisions = new List<Division>();

        }

    }

    public class Division

    {

        public string Name { get; private set; }

        public List<Team> Teams { get; private set; }

        public Division(string name)

        {

            Name = name;

            Teams = new List<Team>();

        }

    }

    public class Team

    {

        public string Name { get; private set; }

        public Team(string name)

        {

            Name = name;

        }

    }

    #endregion

    #region LeagueList

    /// <summary>

    /// Предоставляет просто элемент LeagueList, содержащий фиктивные

    /// данные для демонстрации привязки к иерархическим

    /// структурам данных

    /// </summary>

    public class LeagueList : List<League>

    {

        public LeagueList()

        {

            League l;

            Division d;

            Add(l = new League("League A"));

            l.Divisions.Add((d = new Division("Division A")));

            d.Teams.Add(new Team("Team I"));

            d.Teams.Add(new Team("Team II"));

            d.Teams.Add(new Team("Team III"));

            d.Teams.Add(new Team("Team IV"));

            d.Teams.Add(new Team("Team V"));

            l.Divisions.Add((d = new Division("Division B")));

            d.Teams.Add(new Team("Team Blue"));

            d.Teams.Add(new Team("Team Red"));

            d.Teams.Add(new Team("Team Yellow"));

            d.Teams.Add(new Team("Team Green"));

            d.Teams.Add(new Team("Team Orange"));

            l.Divisions.Add((d = new Division("Division C")));

            d.Teams.Add(new Team("Team East"));

            d.Teams.Add(new Team("Team West"));

            d.Teams.Add(new Team("Team North"));

            d.Teams.Add(new Team("Team South"));

            Add(l = new League("League B"));

            l.Divisions.Add((d = new Division("Division A")));

            d.Teams.Add(new Team("Team 1"));

            d.Teams.Add(new Team("Team 2"));

            d.Teams.Add(new Team("Team 3"));

            d.Teams.Add(new Team("Team 4"));

            d.Teams.Add(new Team("Team 5"));

            l.Divisions.Add((d = new Division("Division B")));

            d.Teams.Add(new Team("Team Diamond"));

            d.Teams.Add(new Team("Team Heart"));

            d.Teams.Add(new Team("Team Club"));

            d.Teams.Add(new Team("Team Spade"));

            l.Divisions.Add((d = new Division("Division C")));

            d.Teams.Add(new Team("Team Alpha"));

            d.Teams.Add(new Team("Team Beta"));

            d.Teams.Add(new Team("Team Gamma"));

            d.Teams.Add(new Team("Team Delta"));

            d.Teams.Add(new Team("Team Epsilon"));

        }

        public League this[string name]

        {

            get

            {

                foreach (League l in this)

                    if (l.Name == name)

                        return l;

                return null;

            }

        }

    }

    #endregion

}

И на VB.NET.

Imports System

Imports System.Windows

Imports System.Windows.Controls

Imports System.Windows.Data

Imports System.Windows.Documents

Imports System.Windows.Media

Imports System.Windows.Shapes

Imports System.Collections.ObjectModel

Imports System.Collections.Generic

#Region "Inner data classes"

Public Class League

    Private m_Name As String

    Public Property Name() As String

        Get

            Return m_Name

        End Get

        Private Set(ByVal value As String)

            m_Name = value

        End Set

    End Property

    Private m_Divisions As List(Of Division)

    Public Property Divisions() As List(Of Division)

        Get

            Return m_Divisions

        End Get

        Private Set(ByVal value As List(Of Division))

            m_Divisions = value

        End Set

    End Property

    Public Sub New(ByVal newname As String)

        Name = newname

        Divisions = New List(Of Division)()

    End Sub

End Class

Public Class Division

    Private m_Name As String

    Public Property Name() As String

        Get

            Return m_Name

        End Get

        Private Set(ByVal value As String)

            m_Name = value

        End Set

    End Property

    Private m_Teams As List(Of Team)

    Public Property Teams() As List(Of Team)

        Get

            Return m_Teams

        End Get

        Private Set(ByVal value As List(Of Team))

            m_Teams = value

        End Set

    End Property

    Public Sub New(ByVal newname As String)

        Name = newname

        Teams = New List(Of Team)()

    End Sub

End Class

Public Class Team

    Private m_Name As String

    Public Property Name() As String

        Get

            Return m_Name

        End Get

        Private Set(ByVal value As String)

            m_Name = value

        End Set

    End Property

    Public Sub New(ByVal newname As String)

        Name = newname

    End Sub

End Class

#End Region

#Region "LeagueList"

''' <summary>

''' Предоставляет просто элемент LeagueList, содержащий фиктивные

''' данные для демонстрации привязки к иерархическим

''' структурам данных

''' </summary>

Public Class LeagueList

    Inherits List(Of League)

    Public Sub New()

        Dim l As League

        Dim d As Division

        l = New League("League A")

        Add(l)

        d = New Division("Division A")

        l.Divisions.Add(d)

        d.Teams.Add(New Team("Team I"))

        d.Teams.Add(New Team("Team II"))

        d.Teams.Add(New Team("Team III"))

        d.Teams.Add(New Team("Team IV"))

        d.Teams.Add(New Team("Team V"))

        d = New Division("Division B")

        l.Divisions.Add(d)

        d.Teams.Add(New Team("Team Blue"))

        d.Teams.Add(New Team("Team Red"))

        d.Teams.Add(New Team("Team Yellow"))

        d.Teams.Add(New Team("Team Green"))

        d.Teams.Add(New Team("Team Orange"))

        d = New Division("Division C")

        l.Divisions.Add(d)

        d.Teams.Add(New Team("Team East"))

        d.Teams.Add(New Team("Team West"))

        d.Teams.Add(New Team("Team North"))

        d.Teams.Add(New Team("Team South"))

        l = New League("League B")

        Add(l)

        d = New Division("Division A")

        l.Divisions.Add(d)

        d.Teams.Add(New Team("Team 1"))

        d.Teams.Add(New Team("Team 2"))

        d.Teams.Add(New Team("Team 3"))

        d.Teams.Add(New Team("Team 4"))

        d.Teams.Add(New Team("Team 5"))

        d = New Division("Division B")

        l.Divisions.Add(d)

        d.Teams.Add(New Team("Team Diamond"))

        d.Teams.Add(New Team("Team Heart"))

        d.Teams.Add(New Team("Team Club"))

        d.Teams.Add(New Team("Team Spade"))

        d = New Division("Division C")

        l.Divisions.Add(d)

        d.Teams.Add(New Team("Team Alpha"))

        d.Teams.Add(New Team("Team Beta"))

        d.Teams.Add(New Team("Team Gamma"))

        d.Teams.Add(New Team("Team Delta"))

        d.Teams.Add(New Team("Team Epsilon"))

    End Sub

    Default Public Overloads ReadOnly Property Item(ByVal name As String) As League

        Get

            For Each l As League In Me

                If l.Name = name Then

                    Return l

                End If

            Next

            Return Nothing

        End Get

    End Property

End Class

#End Region

А здесь шаблон HierarchichalDataTemplate.

<!-- Шаблон, соответствующий лиге -->

<HierarchicalDataTemplate DataType="{x:Type local:League}"

                    ItemsSource = "{Binding Path=Divisions}">

    <TextBlock Text="{Binding Path=Name}" Background="Red"/>

</HierarchicalDataTemplate>

<!-- Шаблон, соответствующий дивизиону -->

<HierarchicalDataTemplate DataType="{x:Type local:Division}"

                    ItemsSource = "{Binding Path=Teams}">

    <TextBlock Text="{Binding Path=Name}" Background="Green"/>

</HierarchicalDataTemplate>

<!-- Шаблон, соответствующий команде -->

<DataTemplate DataType="{x:Type local:Team}">

    <TextBlock Text="{Binding Path=Name}" Background="CornflowerBlue"/>

</DataTemplate>

В результате получается нечто подобное — каждый базовый связанный объект данных получает собственный шаблон.

Дополнительные сведения можно найти здесь, а если вы хотите посмотреть на действительно крутое оформление, которое можно создать с помощью шаблона HierarchichalDataTemplate, прочитайте отличную запись в блоге Карла Шифлета (Karl Shifflett), наиболее ценного специалиста Codeproject, — шаблон HierarchichalDataTemplate используется для создания простого представления в виде дерева типа обозревателя (фактически это ответ на реакцию Джоша на мою исходную статью о простом представлении в виде дерева типа обозревателя).

О небольшом демонстрационном приложении (поскольку оно классное)

Я читаю много блогов о WPF и, что касается привязки данных, не нашел ничего лучше, чем блог Беатриц Коста (Beatriz Costa) из корпорации Microsoft. Она действительно в этом разбирается. В частности, на меня произвел огромное впечатление один из ее примеров, который, по-моему, показывает мощные возможности привязок и шаблонов в WPF.

Вот ссылка на ее оригинальный пост: Мощные возможности стилей и шаблонов в WPF. Я попросил у нее разрешения использовать его в свой статье. Она любезно согласилась, сказав, что с меня пиво (звучит неплохо). Спасибо за разрешение, Би.

Сначала снимок экрана.

Что вы скажете, узнав, что это элемент ListBox? Круто, да? Чтобы узнать, как он работает, давайте его проанализируем.

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

Я просто хотел продемонстрировать, что можно сделать с парой хороших стилей и шаблонов. Посмотрим важную часть, код XAML.

<Window x:Class="PlanetsListBox.PlanetsListBoxWindow"

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:local="clr-namespace:PlanetsListBox"

    Title="PlanetsListBox" Height="700" Width="700"

    >

    <Window.Resources>

        <local:SolarSystem x:Key="solarSystem" />

        <local:ConvertOrbit x:Key="convertOrbit" />

        <DataTemplate DataType="{x:Type local:SolarSystemObject}">

            <Canvas Width="20" Height="20" >

                <Ellipse

                    Canvas.Left="{Binding Path=Orbit,

                        Converter={StaticResource convertOrbit},

                        ConverterParameter=-1.707}"

                    Canvas.Top="{Binding Path=Orbit,

                        Converter={StaticResource convertOrbit},

                        ConverterParameter=-0.293}"

                    Width="{Binding Path=Orbit,

                        Converter={StaticResource convertOrbit},

                        ConverterParameter=2}"

                    Height="{Binding Path=Orbit,

                        Converter={StaticResource convertOrbit},

                        ConverterParameter=2}"

                    Stroke="White"

                    StrokeThickness="1"/>

                <Image Source="{Binding Path=Image}" Width="20" Height="20">

                    <Image.ToolTip>

                        <StackPanel Width="250" TextBlock.FontSize="12">

                            <TextBlock FontWeight="Bold" Text="{Binding Path=Name}" />

                            <StackPanel Orientation="Horizontal">

                                <TextBlock Text="Orbit: " />

                                <TextBlock Text="{Binding Path=Orbit}" />

                                <TextBlock Text=" AU" />

                            </StackPanel>

                            <TextBlock Text="{Binding Path=Details}"

                                TextWrapping="Wrap"/>

                        </StackPanel>

                    </Image.ToolTip>

                </Image>

            </Canvas>

        </DataTemplate>

        <Style TargetType="ListBoxItem">

            <Setter Property="Canvas.Left" Value="{Binding Path=Orbit,

                    Converter={StaticResource convertOrbit}, ConverterParameter=0.707}"/>

            <Setter Property="Canvas.Bottom" Value="{Binding Path=Orbit,

                    Converter={StaticResource convertOrbit}, ConverterParameter=0.707}"/>

            <Setter Property="Template">

                <Setter.Value>

                    <ControlTemplate TargetType="{x:Type ListBoxItem}">

                        <Grid>

                            <Ellipse x:Name="selectedPlanet" Margin="-10" StrokeThickness="2"/>

                            <ContentPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"

                                HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"

                                VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>

                        </Grid>

                        <ControlTemplate.Triggers>

                            <Trigger Property="IsSelected" Value="true">

                                <Setter Property="Stroke" TargetName="selectedPlanet" Value="Yellow"/>

                            </Trigger>

                        </ControlTemplate.Triggers>

                    </ControlTemplate>

                </Setter.Value>

            </Setter>

        </Style>

        <Style TargetType="ListBox">

            <Setter Property="ItemsPanel">

                <Setter.Value>

                    <ItemsPanelTemplate>

                        <Canvas Width="590" Height="590" Background="Black" />

                    </ItemsPanelTemplate>

                </Setter.Value>

            </Setter>

        </Style>

    </Window.Resources>

    <Grid HorizontalAlignment="Center" VerticalAlignment="Center" ClipToBounds="True">

        <ListBox ItemsSource="{Binding Source={StaticResource solarSystem},

            Path=SolarSystemObjects}" Focusable="False" />

    </Grid>

</Window>

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

Отличная работа, Би. Спасибо!

«Безликие» элементы управления

Теперь, когда мы приближаемся к концу статьи, я хочу сообщить кое-что. Мы знаем, что с помощью стилей и шаблонов можно полностью изменить внешний вид элементов управления. Можно даже представить ситуацию, когда дизайнер элементов управления создает, например, элемент выбора изображения с одной кнопкой, но с помощью стилей и шаблонов мы меняем вид этого элемента так, как нам нужно. Это, конечно, хорошо, но нет никаких гарантий, что человек, который разрабатывает стили и шаблоны, знает, что делает этот элемент управления и как он работает. Корпорация Microsoft считает, что это две разные роли: дизайнер и разработчик. И ни одного из них не должно беспокоить, что делает другой.

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

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

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

Итак, представим следующую ситуацию.

Разработчик создает простой элемент управления, например такой:

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

Дизайнер немного знает о WPF и Expression Blend, поэтому он решает, что этот элемент управления должен выглядеть следующим образом.

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

Это хорошо. А нехорошо то, что элемент управления не будет работать так, как разработчик запрограммировал.

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

Ниже приводится описание рекомендуемой процедуры.

Во-первых, разработчик должен сообщить о своих намерениях относительно элемента управления, снабдив его некоторыми атрибутами (атрибутов может быть столько, сколько нужно) с TemplatePartAttribute, которые информируют дизайнера о том, что он должен включить в шаблон элемента управления. Кроме того, разработчик ДОЛЖЕН либо переопределить метод OnApplyTemplate(), получить от дизайнера части шаблона и подключить эти события в коде программной части, либо использовать перенаправленные команды.

using System;

using System.Collections.Generic;

using System.Text;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Data;

using System.Windows.Documents;

using System.Windows.Input;

using System.Windows.Media;

using System.Windows.Media.Imaging;

using System.Windows.Navigation;

using System.Windows.Shapes;

namespace UntitledProject1

{

    /// <summary>

    /// Логика взаимодействия для UserControl1.xaml

    /// </summary>

    ///

    [TemplatePart(Name="PART_PickNew",Type=typeof(Button))]

    public partial class UserControl1 : UserControl

    {

        public UserControl1()

        {

            InitializeComponent();

        }

        public override void OnApplyTemplate()

        {

            base.OnApplyTemplate();

            Button PART_PickNew = this.Template.FindName("PART_PickNew") as Button;

            if (PART_PickNew != null)

            {

                PART_PickNew.Click += new RoutedEventHandler(PART_PickNew_Click);

            }

        }

        void PART_PickNew_Click(object sender, RoutedEventArgs e)

        {

            // далее нужно добавить обработку логики PART_PickNew

        }

    }

}

С точки зрения разработчика все нормально. Перейдем к дизайнеру. Если он действительно использует кнопку и задает ей имя, соответствующее используемому в коде (решение заключается в использовании атрибута TemplatePartAttribute), все должно работать правильно. Ниже показан пример.

<Image Source="C:\Users\sacha\Pictures\BLACK_OR_WHITE.jpg"

    Stretch="UniformToFill" Width="50" Height="50"/>

<Button x:Name="PART_PickNew" Content="Pick New Image"/>

ALWAYS

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

Вот что такое «безликий» элемент управления. Позвольте дизайнеру оформить его по собственному усмотрению, но убедитесь, что он работает, как ожидалось.

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

Постойте, ведь будет еще одна статья!!!

Самые внимательные из вас, наверное, заметили, что это моя последняя статья в серии для начинающих.

Но...

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

Я также хочу написать еще одну статью о WPF после грядущей, заключительной. Так что еще две в итоге, а затем я уже займусь чем-то другим. Шаблоны, потоки, WF, PLINQ, динамические запросы, вся платформа — кто знает? Поживем — увидим.

Ссылки

  1. Блог Беатриц Коста (Beatriz "The Binding Queen" Costa)
  2. Би Коста. Мощные возможности стилей и шаблонов в WPF (PLanetListBox). Я использовал этот элемент с ее любезного разрешения

Спасибо, Би! Я угощу тебя пивом при случае.

Другие полезные источники

  1. Джош Смит. Пошаговое руководство по WPF. Часть 4. Шаблоны данных и триггеры
  2. Джош Смит. Пошаговое руководство по WPF. Часть 5. Стили
  3. Chaz. Темы в WPF
  4. MSDN. Класс Style
  5. MSDN. Стили и шаблоны
  6. MSDN. Практическое руководство. Запуск анимации при изменении данных

С уважением,

Саша Барбер.