Шаблоны элементов управления
Шаблоны элементов управления многоплатформенных приложений .NET (.NET MAUI) позволяют определить визуальную структуру производных пользовательских ContentView элементов управления и ContentPage производных страниц. Шаблоны элементов управления отделяют пользовательский интерфейс элемента управления или страницы от логики, которая их реализует. Вы можете добавить дополнительное содержимое в шаблонный пользовательский элемент управления или шаблонную страницу в заранее определенное расположение.
Например, можно создать шаблон элемента управления, который переопределяет пользовательский интерфейс, предоставленный пользовательским элементом управления. Шаблон элемента управления затем можно использовать в обязательном экземпляре пользовательского элемента управления. Кроме того, можно создать шаблон элемента управления, определяющий любой общий пользовательский интерфейс, который будет использоваться несколькими страницами в приложении. Шаблон элемента управления затем можно использовать на нескольких страницах. Каждая страница будет по-прежнему отображать уникальное содержимое.
Создание шаблона ControlTemplate
В следующем примере показан код для пользовательского элемента управления CardView
:
public class CardView : ContentView
{
public static readonly BindableProperty CardTitleProperty =
BindableProperty.Create(nameof(CardTitle), typeof(string), typeof(CardView), string.Empty);
public static readonly BindableProperty CardDescriptionProperty =
BindableProperty.Create(nameof(CardDescription), typeof(string), typeof(CardView), string.Empty);
public string CardTitle
{
get => (string)GetValue(CardTitleProperty);
set => SetValue(CardTitleProperty, value);
}
public string CardDescription
{
get => (string)GetValue(CardDescriptionProperty);
set => SetValue(CardDescriptionProperty, value);
}
...
}
Класс CardView
, производный от класса ContentView, представляет пользовательский элемент управления, который отображает данные в карточном представлении макета. Класс содержит свойства, поддерживаемые привязываемыми свойствами, для отображаемых данных. Однако класс CardView
не определяет никаких пользовательских интерфейсов. Вместо этого пользовательский интерфейс будет определен с помощью шаблона элемента управления. Дополнительные сведения о создании производных пользовательских элементов управления ContentView см. в статье ContentView.
Создаваемый шаблон элемента управления имеет тип ControlTemplate. При создании ControlTemplate вы объединяете объекты View, чтобы создать пользовательский интерфейс для пользовательского элемента управления или страницы. Шаблон ControlTemplate должен иметь только один объект View в качестве корневого элемента. Однако корневой элемент обычно содержит другие объекты View. Комбинация объектов составляет визуальную структуру элемента управления.
Хотя ControlTemplate можно определить в строке кода, обычно объект ControlTemplate объявляют как ресурс в словаре ресурсов. Так как шаблоны элементов управления являются ресурсами, для них действуют те же правила определения области, которые применяются ко всем ресурсам. Например, если вы объявляете шаблон элемента управления в словаре ресурсов на уровне приложения, шаблон можно использовать в любом месте приложения. Если вы определяете шаблон на странице, шаблон элемента управления можно будет использовать только на ней. Дополнительные сведения о ресурсах см . в словарях ресурсов.
В следующем примере кода XAML показан шаблон ControlTemplate для объектов CardView
:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
...>
<ContentPage.Resources>
<ControlTemplate x:Key="CardViewControlTemplate">
<Frame BindingContext="{Binding Source={RelativeSource TemplatedParent}}"
BackgroundColor="{Binding CardColor}"
BorderColor="{Binding BorderColor}"
...>
<!-- UI objects that define the CardView visual structure -->
</Frame>
</ControlTemplate>
</ContentPage.Resources>
...
</ContentPage>
Если шаблон ControlTemplate объявлен как ресурс, он должен иметь ключ, заданный с помощью атрибута x:Key
, чтобы его можно было определить в словаре ресурсов. В этом примере корневым элементом шаблона CardViewControlTemplate
является объект Frame. Объект Frame использует расширение разметки RelativeSource
, чтобы установить для BindingContext значение экземпляра объекта среды выполнения, к которому применяется шаблонный родительский элемент. Объект Frame использует сочетание элементов управления для определения визуальной CardView
структуры объекта. Выражения привязки этих объектов разрешаются с учетом свойств CardView
из-за наследования BindingContext от корневого элемента Frame. Дополнительные сведения о расширении разметки см. в RelativeSource
разделе "Относительные привязки".
Использование шаблона ControlTemplate
Шаблон ControlTemplate можно применить к производному пользовательскому элементу управления ContentView, присвоив свойству ControlTemplate значение объекта шаблона элемента управления. Аналогичным образом, шаблон ControlTemplate можно применить к производной странице ContentPage, присвоив свойству ControlTemplate значение объекта шаблона элемента управления. Когда во время выполнения применяется шаблон ControlTemplate, все элементы управления, определенные в ControlTemplate, добавляются к визуальному дереву шаблонного пользовательского элемента управления или шаблонной страницы.
В следующем примере показано CardViewControlTemplate
назначение свойству ControlTemplate двух CardView
объектов:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:ControlTemplateDemos.Controls"
...>
<StackLayout Margin="30">
<controls:CardView BorderColor="DarkGray"
CardTitle="John Doe"
CardDescription="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla elit dolor, convallis non interdum."
IconBackgroundColor="SlateGray"
IconImageSource="user.png"
ControlTemplate="{StaticResource CardViewControlTemplate}" />
<controls:CardView BorderColor="DarkGray"
CardTitle="Jane Doe"
CardDescription="Phasellus eu convallis mi. In tempus augue eu dignissim fermentum. Morbi ut lacus vitae eros lacinia."
IconBackgroundColor="SlateGray"
IconImageSource="user.png"
ControlTemplate="{StaticResource CardViewControlTemplate}" />
</StackLayout>
</ContentPage>
В этом примере элементы управления шаблона CardViewControlTemplate
становятся частью визуального дерева каждого объекта CardView
. Так как корневой объект Frame шаблона элемента управления присваивает значение BindingContext шаблонному родительскому элементу, Frame и его дочерние элементы разрешают свои выражения привязки с учетом свойств каждого объекта CardView
.
На следующем снимка экрана показан примененный CardViewControlTemplate
к CardView
объектам:
Внимание
Момент времени, когда шаблон ControlTemplate применяется к экземпляру элемента управления, можно определить путем переопределения метода OnApplyTemplate в шаблонном пользовательском элементе управления или шаблонной странице. Дополнительные сведения см. в разделе Извлечение именованного элемента из шаблона.
Передача параметров с помощью TemplateBinding
Расширение разметки TemplateBinding
привязывает свойство элемента из шаблона ControlTemplate к общему свойству, которое определено шаблонным пользовательским элементом управления или шаблонной страницей. Благодаря использованию расширения TemplateBinding
свойства элемента управления могут действовать в качестве параметров шаблона. Таким образом, если присвоено свойство шаблонного пользовательского элемента управления или шаблонной страницы, значение передается в элемент с расширением разметки TemplateBinding
.
Внимание
Выражение разметки TemplateBinding
позволяет удалить привязку RelativeSource
из предыдущего шаблона элемента управления и заменяет выражения Binding
.
Расширение разметки TemplateBinding
определяет следующие свойства:
- Path с типом
string
— путь к свойству. - Mode с типом BindingMode — направление распространения изменений между источником и целью.
- Converter с типом IValueConverter — преобразователь значений привязки.
- ConverterParameter с типом
object
— параметр для преобразователя значений привязки. - StringFormat с типом
string
— формат строки для привязки.
Path — это свойство ContentProperty
для расширения разметки TemplateBinding
. Если путь является первым элементом в выражении TemplateBinding
, часть Path= расширения разметки можно опустить. Дополнительные сведения об использовании этих свойств в выражении привязки см. в разделе "Привязка данных".
Предупреждение
Расширение разметки TemplateBinding
следует использовать только в шаблоне ControlTemplate. Однако попытка использовать выражение TemplateBinding
за пределами шаблона ControlTemplate не приведет к ошибке сборки или возникновению исключения.
В следующем примере кода XAML показан шаблон ControlTemplate для объектов CardView
, использующих расширение разметки TemplateBinding
:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
...>
<ContentPage.Resources>
<ControlTemplate x:Key="CardViewControlTemplate">
<Frame BackgroundColor="{TemplateBinding CardColor}"
BorderColor="{TemplateBinding BorderColor}"
...>
<!-- UI objects that define the CardView visual structure -->
</Frame>
</ControlTemplate>
</ContentPage.Resources>
...
</ContentPage>
В этом примере расширение разметки TemplateBinding
разрешает выражения привязки для свойств каждого объекта CardView
. На следующем снимка экрана показан примененный CardViewControlTemplate
к CardView
объектам:
Внимание
Использование расширения разметки TemplateBinding
эквивалентно присвоению для свойства BindingContext корневого элемента шаблона значения шаблонного родительского элемента с помощью расширения разметки RelativeSource
и разрешению привязки дочерних объектов с помощью расширения разметки Binding
. На самом деле расширение разметки TemplateBinding
создает Binding
с Source
в качестве RelativeBindingSource.TemplatedParent
.
Применение шаблона ControlTemplate со стилем
Шаблоны элементов управления также можно применить со стилями. Для этого необходимо создать неявный или явный стиль, в котором используется ControlTemplate.
В следующем примере кода XAML показан неявный стиль, в котором используется CardViewControlTemplate
:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:ControlTemplateDemos.Controls"
...>
<ContentPage.Resources>
<ControlTemplate x:Key="CardViewControlTemplate">
...
</ControlTemplate>
<Style TargetType="controls:CardView">
<Setter Property="ControlTemplate"
Value="{StaticResource CardViewControlTemplate}" />
</Style>
</ContentPage.Resources>
<StackLayout Margin="30">
<controls:CardView BorderColor="DarkGray"
CardTitle="John Doe"
CardDescription="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla elit dolor, convallis non interdum."
IconBackgroundColor="SlateGray"
IconImageSource="user.png" />
...
</StackLayout>
</ContentPage>
В этом примере неявный Style автоматически применяется к каждому объекту CardView
и устанавливает для свойства ControlTemplate каждого объекта CardView
значение CardViewControlTemplate
.
Дополнительные сведения о стилях см. в статье Стили .
Переопределение пользовательского интерфейса элемента управления
Когда шаблон ControlTemplate создается и присваивается свойству ControlTemplate производного пользовательского элемента управления ContentView или производной страницы ContentPage, визуальная структура, определенная для пользовательского элемента управления или страницы, заменяется визуальной структурой, определенной в ControlTemplate.
Например, пользовательский элемент управления CardViewUI
определяет пользовательский интерфейс, используя следующий код XAML:
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="ControlTemplateDemos.Controls.CardViewUI"
x:Name="this">
<Frame BindingContext="{x:Reference this}"
BackgroundColor="{Binding CardColor}"
BorderColor="{Binding BorderColor}"
...>
<!-- UI objects that define the CardView visual structure -->
</Frame>
</ContentView>
Однако элементы управления, которые входят в пользовательский интерфейс, можно заменить. Для этого необходимо определить новую визуальную структуру в ControlTemplate и присвоить ее свойству ControlTemplate объекта CardViewUI
:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
...>
<ContentPage.Resources>
<ControlTemplate x:Key="CardViewCompressed">
<Grid RowDefinitions="100"
ColumnDefinitions="100, *">
<Image Source="{TemplateBinding IconImageSource}"
BackgroundColor="{TemplateBinding IconBackgroundColor}"
...>
<!-- Other UI objects that define the CardView visual structure -->
</Grid>
</ControlTemplate>
</ContentPage.Resources>
<StackLayout Margin="30">
<controls:CardViewUI BorderColor="DarkGray"
CardTitle="John Doe"
CardDescription="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla elit dolor, convallis non interdum."
IconBackgroundColor="SlateGray"
IconImageSource="user.png"
ControlTemplate="{StaticResource CardViewCompressed}" />
...
</StackLayout>
</ContentPage>
В этом примере визуальная структура объекта CardViewUI
переопределяется в шаблоне ControlTemplate, который предоставляет более компактную визуальную структуру, пригодную для сокращенного списка:
Преобразование содержимого в ContentPresenter
ContentPresenter можно поместить в шаблон элемента управления, чтобы указать, где будет отображаться содержимое шаблонного пользовательского элемента управления или шаблонной страницы. Затем пользовательский элемент управления или пользовательская страница, которые используют шаблон элемента управления, определяют содержимое, отображаемое объектом ContentPresenter. На следующей схеме показан шаблон ControlTemplate для страницы, содержащий несколько элементов управления, в том числе элемент ContentPresenter, обозначенный синим прямоугольником:
В следующем коде XAML показан шаблон элемента управления TealTemplate
, содержащий ContentPresenter в своей визуальной структуре:
<ControlTemplate x:Key="TealTemplate">
<Grid RowDefinitions="0.1*, 0.8*, 0.1*">
<BoxView Color="Teal" />
<Label Margin="20,0,0,0"
Text="{TemplateBinding HeaderText}"
... />
<ContentPresenter Grid.Row="1" />
<BoxView Grid.Row="2"
Color="Teal" />
<Label x:Name="changeThemeLabel"
Grid.Row="2"
Margin="20,0,0,0"
Text="Change Theme"
...>
<Label.GestureRecognizers>
<TapGestureRecognizer Tapped="OnChangeThemeLabelTapped" />
</Label.GestureRecognizers>
</Label>
<controls:HyperlinkLabel Grid.Row="2"
Margin="0,0,20,0"
Text="Help"
Url="https://learn.microsoft.com/dotnet/maui/"
... />
</Grid>
</ControlTemplate>
В следующем примере показан шаблон TealTemplate
, назначенный свойству ControlTemplate производной страницы ContentPage:
<controls:HeaderFooterPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:ControlTemplateDemos.Controls"
ControlTemplate="{StaticResource TealTemplate}"
HeaderText="MyApp"
...>
<StackLayout Margin="10">
<Entry Placeholder="Enter username" />
<Entry Placeholder="Enter password"
IsPassword="True" />
<Button Text="Login" />
</StackLayout>
</controls:HeaderFooterPage>
Когда во время выполнения шаблон TealTemplate
применяется к странице, ее содержимое подставляется в класс ContentPresenter, определенный в шаблоне элемента управления:
Извлечение именованного элемента из шаблона
Именованные элементы в шаблоне элемента управления можно извлечь из шаблонного пользовательского элемента управления или шаблонной страницы. Это можно сделать с помощью метода GetTemplateChild, который возвращает именованный элемент в созданном экземпляре визуального дерева ControlTemplate, если он найден. В противном случае возвращается значение null
.
Когда экземпляр шаблона элемента управления будет создан, вызовется метод шаблона OnApplyTemplate. Поэтому метод GetTemplateChild следует вызывать из переопределения OnApplyTemplate в шаблонном элементе управления или шаблонной странице.
Внимание
Метод GetTemplateChild должен вызываться только после вызова метода OnApplyTemplate.
В следующем коде XAML показан шаблон элемента управления TealTemplate
, который можно применить к производным страницам ContentPage:
<ControlTemplate x:Key="TealTemplate">
<Grid>
...
<Label x:Name="changeThemeLabel"
Text="Change Theme"
...>
<Label.GestureRecognizers>
<TapGestureRecognizer Tapped="OnChangeThemeLabelTapped" />
</Label.GestureRecognizers>
</Label>
...
</Grid>
</ControlTemplate>
В этом примере элемент Label именованный, поэтому его можно извлечь из кода шаблонной страницы. Для этого нужно вызвать метод GetTemplateChild из переопределения OnApplyTemplate для шаблонной страницы:
public partial class AccessTemplateElementPage : HeaderFooterPage
{
Label themeLabel;
public AccessTemplateElementPage()
{
InitializeComponent();
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
themeLabel = (Label)GetTemplateChild("changeThemeLabel");
themeLabel.Text = OriginalTemplate ? "Aqua Theme" : "Teal Theme";
}
}
В этом примере объект Label с именем changeThemeLabel
извлекается после создания экземпляра ControlTemplate. После этого объект changeThemeLabel
станет доступным для использования классом AccessTemplateElementPage
. На следующем снимку экрана показано, что текст, отображаемый Label измененным:
Привязка к ViewModel
ControlTemplate может выполнять привязку данных к ViewModel, даже если шаблон ControlTemplate привязывается к шаблонному родительскому элементу (экземпляру объекта среды выполнения, к которому применяется шаблон).
В следующем примере кода XAML показана страница, которая использует ViewModel с именем PeopleViewModel
:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:ControlTemplateDemos"
xmlns:controls="clr-namespace:ControlTemplateDemos.Controls"
...>
<ContentPage.BindingContext>
<local:PeopleViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources>
<DataTemplate x:Key="PersonTemplate">
<controls:CardView BorderColor="DarkGray"
CardTitle="{Binding Name}"
CardDescription="{Binding Description}"
ControlTemplate="{StaticResource CardViewControlTemplate}" />
</DataTemplate>
</ContentPage.Resources>
<StackLayout Margin="10"
BindableLayout.ItemsSource="{Binding People}"
BindableLayout.ItemTemplate="{StaticResource PersonTemplate}" />
</ContentPage>
В этом примере элемент BindingContext страницы содержит экземпляр PeopleViewModel
. ViewModel предоставляет коллекцию People
и ICommand с именем DeletePersonCommand
. Класс StackLayout на странице использует привязываемый макет, чтобы привязать данные к коллекции People
, а для ресурса PersonTemplate
шаблон ItemTemplate
привязываемого макета. Этот шаблон DataTemplate указывает, что каждый элемент коллекции People
будет отображаться с помощью объекта CardView
. Визуальная структура объекта CardView
определяется с помощью шаблона ControlTemplate с именем CardViewControlTemplate
:
<ControlTemplate x:Key="CardViewControlTemplate">
<Frame BindingContext="{Binding Source={RelativeSource TemplatedParent}}"
BackgroundColor="{Binding CardColor}"
BorderColor="{Binding BorderColor}"
...>
<!-- UI objects that define the CardView visual structure -->
</Frame>
</ControlTemplate>
В этом примере корневым элементом шаблона ControlTemplate является объект Frame. Объект Frame использует расширение разметки RelativeSource
, чтобы задать для BindingContext значение шаблонного родительского элемента. Выражения привязки объекта Frame и его дочерних элементов разрешаются для свойств CardView
из-за наследования BindingContext от корневого элемента Frame. На следующем снимка экрана показана страница, отображающая коллекцию People
:
Хотя объекты в шаблоне ControlTemplate привязываются к свойствам родительского элемента, Button в шаблоне элемента управления привязывается к шаблонному родительскому элементу и к DeletePersonCommand
в объекте ViewModel. Это связано с тем, что свойство Button.Command
переопределяет источник привязки как контекст привязки предка с типом PeopleViewModel
, то есть StackLayout. Затем часть Path
выражений привязки может разрешить свойство DeletePersonCommand
. Однако свойство Button.CommandParameter
не изменяет источник привязки, а наследует его от родительского элемента в ControlTemplate. Таким образом, свойство CommandParameter
привязывается к свойству CardTitle
CardView
.
Общим эффектом привязок Button является то, что при нажатии Button в классе PeopleViewModel
выполняется DeletePersonCommand
со значением свойства CardName
, которое передается в DeletePersonCommand
. Это приводит к удалению указанного объекта CardView
из привязываемого макета.
Дополнительные сведения об относительных привязках см. в разделе "Относительные привязки".