Главная » Разработка для Windows Phone 7 » Основы ControlTemplate

0

DataTemplate позволяет настраивать представление содержимого ContentControl. ControlTemplate, который можно задать как значение свойства Template любого Control, обеспечивает возможность настраивать представление самого элемента управления, что часто называют визуальным стилем элемента управления. Эти два разных назначения отражены в следующей таблице:

Свойство

Тип свойства

Назначение

Template

ControlTemplate

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

ContentTemplate

DataTemplate

настраивает представление содержимого

Не забывайте, что свойство ContentTemplate описано классом ContentControl, и его можно найти только в классах, наследуемых от ContentControl. Но свойство Template определено классом Control, и его присутствие является, наверное, основным отличием элементов управления от производных от FrameworkElement, таких как TextBlock и Image.

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

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

Как и стили, очень часто шаблоны определяются как ресурсы. Как и Style, ControlTemplate требует задания TargetType:

<ControlTemplate x:Key="btnTemplate" TargetType="Button"> </ControlTemplate>

Очень часто Template описывается как часть Style:

<Style x:Key="btnStyle" TargetType="Button"> <Setter Property="Margin" Value="6" /> <Setter Property="Template"> <Setter.Value>

<ControlTemplate TargetType="Button">

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

Обратите внимание, что для описания метода Setter, который задает в качестве значения свойства Template объект типа ControlTemplate, используется синтаксис свойство-элемент. Определение шаблона как части стиля – очень распространенный подход, потому что обычно требуется задать некоторые свойства элемента управления, чтобы сделать их более соответствующими создаваемому шаблону. Эти теги Setter эффективно переопределяют значения по умолчанию свойств элемента управления, к которому применяется стиль и шаблон, но они могут быть переопределены локальными параметрами конкретного элемента управления.

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

Начнем со стандартного Button с текстовым содержимым, выравнивание которого задано таким образом, что он занимает лишь столько места, сколько ему необходимо для отображения содержимого:

<Button Content="Click me!"

HorizontalAlignment="Center" VerticalAlignment="Center"> </Button>

Чтобы более свободно экспериментировать с ControlTemplate, не будем описывать его как ресурс, но вынесем его свойство Template как свойство-элемент объекта Button и зададим ControlTemplate в качестве его значения:

<Button Content="Click me!"

HorizontalAlignment="Center" VerticalAlignment="Center"> <Button.Template>

<ControlTemplate TargetType="Button">

</ControlTemplate>

</Button.Template> </Button>

Как только свойству Template задается пустой ControlTemplate, кнопка исчезает. Дерева визуальных элементов, которое определяет внешний вид элемента управления, больше не существует. Именно это дерево мы и будем помещать в ControlTemplate. Чтобы гарантированно ничего не повредить, вставим TextBlock в ControlTemplate:

<Button Content="Click me!"

HorizontalAlignment="Center" VerticalAlignment="Center"> <Button.Template>

<ControlTemplate TargetType="Button">

<TextBlock Text="temporary" /> </ControlTemplate> </Button.Template> </Button>

Теперь Button описывается одним словом «temporary» (временно). Он не обеспечивает никакой обратной связи при касании, но во всем остальном это полнофункциональная кнопка. Несомненно, в ней есть серьезные недостатки, потому что на самом деле на Button должна отображаться надпись «Click me!», но это мы скоро исправим.

Зададим рамку вокруг TextBlock:

<Button Content="Click me!"

HorizontalAlignment="Center" VerticalAlignment="Center"> <Button.Template>

<ControlTemplate TargetType="Button">

<Border BorderBrush="{StaticResource PhoneAccentBrush}" BorderThickness="6"> <TextBlock Text="temporary" /> </Border> </ControlTemplate> </Button.Template> </Button>

Вот как это выглядит теперь:

На самом деле, явно задавать значения свойств в коде, как это сделано здесь в шаблоне, не очень хорошая идея, особенно если этот шаблон предполагается для совместного использования многими элементами управления. Вообще нет смысла задавать в шаблоне BorderBrush и BorderThickness, потому что эти свойства определяет сам класс Control. Если мы действительно хотим задать рамку вокруг кнопки, мы должны задавать эти свойства в Button, а не в шаблоне, потому что этот шаблон может использоваться совместно несколькими кнопками, для которых потребуются рамки разной толщины и отрисованные разными кистями.

Итак, перенесем эти свойства из шаблона в саму кнопку:

<Button Content="Click me!"

HorizontalAlignment="Center" VerticalAlignment="Center"

BorderBrush="{StaticResource PhoneAccentBrush}" BorderThickness="6"> <Button.Template>

<ControlTemplate TargetType="Button"> <Border>

<TextBlock Text="temporary" /> </Border> </ControlTemplate> </Button.Template> </Button>

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

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

<Button Content="Click me!"

HorizontalAlignment="Center" VerticalAlignment="Center"

BorderBrush="{StaticResource PhoneAccentBrush}" BorderThickness="6"> <Button.Template>

<ControlTemplate TargetType="Button">

<Border BorderBrush="{TemplateBinding BorderBrush}"

BorderThickness="{TemplateBinding BorderThickness}"> <TextBlock Text="temporary" /> </Border> </ControlTemplate> </Button.Template> </Button>

Применение TemplateBinding означает, что свойства этого конкретного элемента в дереве визуальных элементов шаблона – в частности, свойства BorderBrush и BorderThickness элемента Border – будут иметь значения, такие же как заданы аналогичным свойствам в самом элементе управления. Теперь у Button появилась рамка контрастного цвета:

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

С другой стороны, в TemplateBinding нет ничего особенного. Фактически, это сокращенная форма, и когда вы увидите ее полную версию, вы будете счастливы, что такая сокращенная форма существует. Задание атрибута

BorderBrush="{TemplateBinding BorderBrush}"

является сокращенной записью такого выражения:

BorderBrush="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=BorderBrush}"

Это привязка к объекту Border, и синтаксис RelativeSource ссылается на еще один элемент дерева, имеющий отношение к этому Border. TemplatedParent (Родитель для применения шаблона) – это Button, к которому применяется этот шаблон, так что привязка указывает на BorderBrush этого Button. (Понятно? Нет?) Эта альтернатива TemplateBinding пригодится, когда требуется установить двунаправленную привязку к свойству шаблона, потому что TemplateBinding обеспечивает только однонаправленную привязку и не предоставляет свойства Mode.

Вернемся к рассматриваемому шаблону. Теперь, когда мы описали TemplateBinding для BorderBrush и BorderThickness, возник другой вопрос. Пусть решено, что толщина рамки данной кнопки должна составлять 6 пикселов и быть закрашенной контрастным цветом, но такие значения можно обеспечить, только явно задав свойства BorderBrush и BorderThickness в Button. Было бы здорово, если бы эти свойства не надо было задавать в Button. Иначе говоря, мы хотим, чтобы эти свойства в Button имели значения по умолчанию, которые могут быть переопределены локальными параметрами.

Это можно реализовать, задав желаемые значения по умолчанию в Style. Для удобства я определил такой Style прямо в Button:

<Button Content="Click me!"

HorizontalAlignment="Center" VerticalAlignment="Center"> <Button.Style>

<Style TargetType="Button">

<Setter Property="BorderBrush" Value="{StaticResource PhoneAccentBrush}"

/>

<Setter Property="BorderThickness" Value="6" /> </Style> </Button.Style>

<Button.Template>

<ControlTemplate TargetType="Button">

<Border BorderBrush="{TemplateBinding BorderBrush}"

BorderThickness="{TemplateBinding BorderThickness}"> <TextBlock Text="temporary" /> </Border> </ControlTemplate> </Button.Template> </Button>

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

Очень часто свойство Template описывается как часть Style следующим образом:

<Button Content="Click me!"

HorizontalAlignment="Center" VerticalAlignment="Center"> <Button.Style>

<Style TargetType="Button">

<Setter Property="BorderBrush" Value="{StaticResource PhoneAccentBrush}"

/>

<Setter Property="BorderThickness" Value="6" /> <Setter Property="Template"> <Setter.Value>

<ControlTemplate TargetType="Button">

<Border BorderBrush="{TemplateBinding BorderBrush}"

BorderThickness="{TemplateBinding BorderThickness}"> <TextBlock Text="temporary" /> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> </Button.Style> </Button>

Теперь Style задает значения по умолчанию для свойств, которые также используются шаблоном. Добавим свойство Background в Border и тоже зададим для него значение по умолчанию:

<Button Content="Click me!"

HorizontalAlignment="Center" VerticalAlignment="Center"> <Button.Style>

<Style TargetType="Button">

<Setter Property="BorderBrush" Value="{StaticResource PhoneAccentBrush}"

/>

<Setter Property="BorderThickness" Value="6" />

<Setter Property="Background" Value="{StaticResource PhoneChromeBrush}"

/>

<Setter Property="Template"> <Setter.Value>

<ControlTemplate TargetType="Button">

<Border BorderBrush="{TemplateBinding BorderBrush}"

BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}"> <TextBlock Text="temporary" /> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> </Button.Style> </Button>

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

<Button Content="Click me!"

HorizontalAlignment="Center" VerticalAlignment="Center"> <Button.Style>

<Style TargetType="Button">

<Setter Property="BorderBrush" Value="{StaticResource PhoneAccentBrush}"

/>

<Setter Property="BorderThickness" Value="6" />

<Setter Property="Background" Value="{StaticResource PhoneChromeBrush}"

/>

<Setter Property="Template"> <Setter.Value>

<ControlTemplate TargetType="Button">

<Border BorderBrush="{TemplateBinding BorderBrush}"

BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" CornerRadius="12"> <TextBlock Text="temporary" /> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> </Button.Style> </Button>

Вот что мы имеем на данный момент:

На кнопке по-прежнему отображается надпись «temporary», хотя на самом деле на ней должно быть написано «Click me!» (Щелкни меня!). Велик соблазн вставить здесь TextBlock и задать его свойство Text в TemplateBinding свойства Content элемента управления Button:

<TextBlock Text="{TemplateBinding Content}" />

Такая схема будет вполне работоспособной в данном примере, но это очень и очень неправильно. Проблема в том, что свойство Content класса Button типа object. В качестве его значения может быть задано все, что угодно – Image, Panel, Shape, RadialGradientBrush – и это может обусловить небольшие неприятности для TextBlock.

К счастью в Silverlight есть класс, который существует специально для отображения содержимого в производном от ContentControl классе. Этот класс носит имя ContentPresenter. У него есть свойство Content типа object, и ContentPresenter выводит этот объект на экран независимо от того, является ли он просто строкой или каким-либо иным элементом:

<Button Content="Click me!"

HorizontalAlignment="Center" VerticalAlignment="Center"> <Button.Style>

<Style TargetType="Button">

<Setter Property="BorderBrush" Value="{StaticResource PhoneAccentBrush}"

/>

<Setter Property="BorderThickness" Value="6" />

<Setter Property="Background" Value="{StaticResource PhoneChromeBrush}"

/>

<Setter Property="Template"> <Setter.Value>

<ControlTemplate TargetType="Button">

<Border BorderBrush="{TemplateBinding BorderBrush}"

BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" CornerRadius="12"> <ContentPresenter Content="{TemplateBinding Content}" /> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> </Button.Style> </Button>

Обратите внимание, как свойство Content класса ContentPresenter связано со свойством Content класса Button. ContentPresenter обладает отличительным преимуществом возможности работы с любыми видами объектов. ContentPresenter может создавать собственное дерево визуальных элементов. Например, если Content строкового типа, ContentPresenter создает TextBlock для отображения этой строки. ContentPresenter также доверено создавать дерево визуальных элементов для отображения содержимого на основании DataTemplate, заданного для Control. Для этой цели у ContentPresenter есть собственное свойство ContentTemplate, которое можно связать посредством привязки с ContentTemplate элемента управления:

<Button Content="Click me!"

HorizontalAlignment="Center" VerticalAlignment="Center"> <Button.Style>

<Style TargetType="Button">

<Setter Property="BorderBrush" Value="{StaticResource PhoneAccentBrush}"

/>

<Setter Property="BorderThickness" Value="6" />

<Setter Property="Background" Value="{StaticResource PhoneChromeBrush}"

/>

<Setter Property="Template"> <Setter.Value>

<ControlTemplate TargetType="Button">

<Border BorderBrush="{TemplateBinding BorderBrush}"

BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" CornerRadius="12"> <ContentPresenter

Content="{TemplateBinding Content}"

ContentTemplate="{TemplateBinding ContentTemplate}"

/>

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

Эти два присваивания TemplateBinding для ContentPresenter настолько стандартные, что они не требуют явного задания! Все будет сделано автоматически. Но я чувствую себя комфортнее, когда вижу их заданными явно.

Давайте вспомним, что класс Control описывает свойство Padding, предназначенное для создания небольшого отступа вокруг содержимого элемента управления. Зададим свойство Padding для нашего Button:

<Button Content="Click me!"

HorizontalAlignment="Center"

VerticalAlignment="Center"

Padding="24">

</Button>

Ничего не происходит. Дерево визуальных элементов должно включить это свойство Padding. Оно должно обеспечить небольшой зазор между Border и ContentPresenter. Как это сделать? Одним из решений будет применить TemplateBinding к свойству Padding объекта Border. Но если в Border будет располагаться еще какое-то содержимое, кроме ContentPresenter, ничего не получится. Стандартный подход в данном случае – задать TemplateBinding для свойства Margin объекта ContentPresenter:

<ContentPresenter

Content="{TemplateBinding Content}"

ContentTemplate="{TemplateBinding ContentTemplate}" Margin="{TemplateBinding Padding}" />

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

Теперь зададим свойствам HorizontalAlignment и VerticalAlignment нашего Button значение Stretch. Все работает нормально, поэтому в шаблоне об этом можно не беспокоиться. Аналогично можно задать для Button свойство Margin, и оно тоже будет распознаваться системой компоновки.

Но после того, как свойствам HorizontalAlignment и VerticalAlignment кнопки задано значение Stretch, все содержимое Button остается в его верхнем левом углу:

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

Это свидетельствует о том, что в шаблон требуется что-то добавить для обработки этих свойств. Мы должны выровнять ContentPresenter в Border с помощью свойств HorizontalContentAlignment и VerticalContentAlignment. Осуществляется это путем предоставления разметки TemplateBinding, целевыми свойствами которой задаются свойства HorizontalAlignment и VerticalAlignment объекта ContentPresenter:

<ContentPresenter

Content="{TemplateBinding Content}"

ContentTemplate="{TemplateBinding ContentTemplate}" Margin="{TemplateBinding Padding}"

HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />

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

Задание свойств шрифта или свойства Foreground в Button приводит к изменению текста кнопки соответствующим образом. Эти свойства наследуются по дереву визуальных элементов шаблона, и разработчику не надо что-либо добавлять в шаблон, чтобы применить их. (Но в стиле темы для Button явно заданы свойства Foreground, FontFamily и FontSize, поэтому сам Button не может унаследовать эти свойства через дерево визуальных элементов, и очевидно, что мы ничего не можем сделать в пользовательском Style для изменения этого поведения.)

Источник: Чарльз Петзольд, Программируем Windows Phone 7, Microsoft Press, © 2011.

По теме:

  • Комментарии