Поделиться через


Шаблоны элементов управления

Просмотрите пример. Обзор примера

Шаблоны элементов управления многоплатформенных приложений .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 объектам:

Снимок экрана: два шаблонных объекта 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 объектам:

Снимок экрана: объекты 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, который предоставляет более компактную визуальную структуру, пригодную для сокращенного списка:

Снимок экрана: объекты CardViewUI шаблона.

Преобразование содержимого в ContentPresenter

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

Шаблон управления для ContentPage.

В следующем коде 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 :

Снимок экрана: три шаблонных объекта CardView, которые привязываются к модели представления.

Хотя объекты в шаблоне ControlTemplate привязываются к свойствам родительского элемента, Button в шаблоне элемента управления привязывается к шаблонному родительскому элементу и к DeletePersonCommand в объекте ViewModel. Это связано с тем, что свойство Button.Command переопределяет источник привязки как контекст привязки предка с типом PeopleViewModel, то есть StackLayout. Затем часть Path выражений привязки может разрешить свойство DeletePersonCommand. Однако свойство Button.CommandParameter не изменяет источник привязки, а наследует его от родительского элемента в ControlTemplate. Таким образом, свойство CommandParameter привязывается к свойству CardTitle CardView.

Общим эффектом привязок Button является то, что при нажатии Button в классе PeopleViewModel выполняется DeletePersonCommand со значением свойства CardName, которое передается в DeletePersonCommand. Это приводит к удалению указанного объекта CardView из привязываемого макета.

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