在WPF中,ControlTemplateDataTemplate是两种用于定义UI的模板。

  • DataTemplate:用于定义数据对象如何显示。比如在ListBoxComboBoxItemsControl等控件中,程序员会直接绑定数据——这里我们可以使用DataTemplate来定义每一项的数据如何显示。注意,DataTemplate目标是数据对象,它定义了数据如何在UI中呈现,比如一个字符串、一个自定义对象,或者一个集合的每一项。
  • ControlTemplate:用于定义控件的外观。例如,你可以为一个Button控件定义一个ControlTemplate来改变按钮的外观。注意,ControlTemplate的目标是控件本身,比如ButtonTextBox等。它定义了控件的可视化树结构。

数据模板

数据模板提供了一种至关重要的能力:即可以自由灵活地展示数据外观。在不使用数据模板的情况下,WPF默认展示对象时总是固定调用对象的.ToString()方法,我们当然还可以使用绑定路径、自定义控件等方法。但是问题是,这些方案都不够灵活。

数据模板(DataTemplate) 提供的解决方案是,为控件关连的对象自动应用相应的数据模板。

DataTemplate通常在绑定控件的ItemTemplateContentTemplate等属性中使用,用于定义数据项的显示方式。例如,定义一个ListBox中项的显示方式:

<ListBox ItemsSource="{Binding Items}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel>
                <TextBlock Text="{Binding Name}" FontWeight="Bold"/>
                <TextBlock Text="{Binding Description}" FontStyle="Italic"/>
            </StackPanel>
        </DataTemplate>
     </ListBox.ItemTemplate>
</ListBox>

数据模板可以定义在资源中,实现一定程度上的共享。数据模板中还可以包含数据触发器,以动态修改属性:

<Window.Resources>
    <DataTemplate DataType="{x:Type local:Employee}">

        <Border Padding="5" BorderThickness="1" BorderBrush="Gray" CornerRadius="5">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="80"/>
                    <ColumnDefinition/>
                </Grid.ColumnDefinitions>
                <TextBlock x:Name="txtName" VerticalAlignment="Center" Text="{Binding Name}" FontSize="18" FontWeight="Black"/>
                <StackPanel Grid.Column="1">
                    <TextBlock Text="{Binding Job}"/>
                    <TextBlock Text="{Binding Department}"/>
                    <TextBlock Text="{Binding Email}"/>
                </StackPanel>
            </Grid>
        </Border>

        <!-- 为数据模板定义数据触发器 -->
        <DataTemplate.Triggers>
            <DataTrigger Binding="{Binding Gender}" Value="Male">
                <Setter TargetName="txtName" Property="Foreground" Value="LightBlue"/>
            </DataTrigger>
            <DataTrigger Binding="{Binding Gender}" Value="Female">
                <Setter TargetName="txtName" Property="Foreground" Value="LightGreen"/>
            </DataTrigger>
        </DataTemplate.Triggers>
     </DataTemplate>
</Window.Resources>

更多内容可以参考:
https://learn.microsoft.com/en-us/dotnet/desktop/wpf/data/data-templating-overview?view=netframeworkdesktop-4.8

内容模板

和数据模板类似,内容模板是WPF中的一种模板技术。不过和DataTemplate不同,DataTemplateTargetType是普通数据,而ControlTemplateTargetType是一个个控件:

The TargetType of a ControlTemplate must be or inherit from a Control, a Page, or a PageFunctionBase.

如果指定的TargetType不是一个控件,就会抛出ArgumentException,关于这块的说明,可参见微软官方文档

例1: 通过控件模板为默认的按钮重写外观

默认的Button是一个矩形,我们可以把ControlTemplate将其改成椭圆形(这个示例参照的是dino623的文章):

WPF中使用空间模板为Button改写外观.PNG

代码如下:

<StackPanel>

    <Button Content="原生控件" Margin="20,20,20,20" />


    <Button Content="改写外观" Foreground="Salmon" Margin="20,20,20,20">
        <Button.Template>
            <ControlTemplate TargetType="Button">
                <Grid>
                    <Ellipse  Stroke="DarkOrange" StrokeThickness="3" Fill="Black"/>
                    <ContentPresenter Content="{TemplateBinding Content}"
                        Margin="10,20" HorizontalAlignment="Center" VerticalAlignment="Center" />
                </Grid>
            </ControlTemplate>
        </Button.Template>
    </Button>

</StackPanel>

这里<ContentPresenter/>用于显示控件的内容content),默认绑定到Content属性上。而属性TemplateBinding 用于模板内的依赖属性绑定。

例2:通过控件模板为无头控件编写外观

编写无头控件的代码

我们还是参照dino.c的文章,定义一个简单的无头控件 MyContentControl,可以根据使用者传入ContentTemplate展示内容。此控件代码非常简单,只是包含一个自定义的ContentContentTemplate依赖属性:

public class MyContentControl : Control
{
    static MyContentControl()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(MyContentControl), new FrameworkPropertyMetadata(typeof(MyContentControl)));
    }


    #region Content DP
    public object Content
    {
        get { return (object)GetValue(ContentProperty); }
        set { SetValue(ContentProperty, value); }
    }

    // Using a DependencyProperty as the backing store for ContentProperty.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(nameof(Content), typeof(object), typeof(MyContentControl), new PropertyMetadata(null));
    #endregion


    #region ContentTemplate DP
    public DataTemplate ContentTemplate
    {
        get { return (DataTemplate)GetValue(ContentTemplateProperty); }
        set { SetValue(ContentTemplateProperty, value); }
    }

    // Using a DependencyProperty as the backing store for ContentTemplateProperty.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ContentTemplateProperty = DependencyProperty.Register(nameof(ContentTemplate), typeof(DataTemplate), typeof(MyContentControl), new PropertyMetadata(null));
    #endregion
}

其中,我们定义的Content是任意类型;而ContentTemplateDataTemplate类型。

注意,上面的代码中,并未定义如何展示我们自定义的Content和如何使用ContentTemplate属性。这部分可以留待使用者实现。而这个控件的编写者,往往会提供默认的样式,并在其中设置控件的默认的Template属性(也就是ControlTemplate):

<Style TargetType="{x:Type local:MyContentControl}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:MyContentControl}">
                <ContentPresenter 
                    Content="{TemplateBinding Content}"
                    ContentTemplate="{TemplateBinding ContentTemplate}" />
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

注意:

  • 这里改写了控件Template属性(ControlTemplate
  • 这里依旧使用了 <ContentPresenter> 来展现内容,注意 ContentPresenter.ContentContentPresenter.ContentTemplate 这两个属性的绑定。

使用无头控件

如你所见,我们之前定义Content属性的时候,使用的是object类型。这意味着使用的时候,我们可以传入任意对象。比如这里我们可以新建一个ViewModel,其类型为:

internal class CourseViewModel
{
    public string Name { get; set; }
    public int Score { get; set; }
}

然后直接将其赋给Content属性:

<local:MyContentControl>
   
    <!-- 指定Content -->
    <local:MyContentControl.Content>
        <viewmodels:CourseViewModel>
            <viewmodels:CourseViewModel.Name>Math</viewmodels:CourseViewModel.Name>
            <viewmodels:CourseViewModel.Score>100</viewmodels:CourseViewModel.Score>
        </viewmodels:CourseViewModel>
    </local:MyContentControl.Content>
    
    <!-- 指定ContentTemplate -->
    <local:MyContentControl.ContentTemplate>
        <DataTemplate DataType="{x:Type viewmodels:CourseViewModel}">
            <Border Background="Green">
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding Name}"
                        Foreground="White" FontSize="20" Margin="20"  HorizontalAlignment="Center" />
                    <TextBlock Text="{Binding Score}"
                        Foreground="White" FontSize="20" Margin="20"  HorizontalAlignment="Center" />
                </StackPanel>
            </Border>
        </DataTemplate>
    </local:MyContentControl.ContentTemplate>
</local:MyContentControl>

注意,这里外部传入了两个属性:

  • Content属性:是object类型,代表外部传入的“数据”。在大多数情况下,这是“ViewModel”。
  • ContentTemplate属性:是DataTemplate类型,代表外部传入的“数据模板”。
    最终效果为:

MyContent的内容动态绑定.PNG

标签: WPF, DataTemplate, ControlTemplate

添加新评论