WPF有一个灵活的UI框架,用户可以轻松地使用代码控制控件的外观。例设我需要一个控件在鼠标进入的时候背景变成蓝色,我可以用下面这段代码实现:
protected override void OnMouseEnter(MouseEventArgs e) { base.OnMouseEnter(e); Background = new SolidColorBrush(Colors.Blue); }但一般没人会这么做,因为这样做代码和UI过于耦合,难以扩展。正确的做法应该是使用代码告诉ControlTemplate去改变外观,或者控制ControlTemplate中可用的元素进入某个状态。
这篇文章介绍自定义控件的代码如何和ControlTemplate交互,涉及的知识包括RelativeSource、Trigger、TemplatePart和VisualState。
2. 简单的Expander本文使用一个简单的Expander介绍UI和ControlTemplate交互的几种技术,它的代码如下:
public class MyExpander : HeaderedContentControl { public MyExpander() { DefaultStyleKey = typeof(MyExpander); } public bool IsExpanded { get => (bool)GetValue(IsExpandedProperty); set => SetValue(IsExpandedProperty, value); } public static readonly DependencyProperty IsExpandedProperty = DependencyProperty.Register(nameof(IsExpanded), typeof(bool), typeof(MyExpander), new PropertyMetadata(default(bool), OnIsExpandedChanged)); private static void OnIsExpandedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) { var oldValue = (bool)args.OldValue; var newValue = (bool)args.NewValue; if (oldValue == newValue) return; var target = obj as MyExpander; target?.OnIsExpandedChanged(oldValue, newValue); } protected virtual void OnIsExpandedChanged(bool oldValue, bool newValue) { if (newValue) OnExpanded(); else OnCollapsed(); } protected virtual void OnCollapsed() { } protected virtual void OnExpanded() { } } <Style TargetType="{x:Type local:MyExpander}"> <Setter Property="HorizontalContentAlignment" Value="Stretch" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:MyExpander}"> <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> <StackPanel> <ToggleButton x:Name="ExpanderToggleButton" Content="{TemplateBinding Header}" IsChecked="{Binding IsExpanded,RelativeSource={RelativeSource Mode=TemplatedParent},Mode=TwoWay}" /> <ContentPresenter Grid.Row="1" x:Name="ContentPresenter" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Visibility="Collapsed" /> </StackPanel> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style>MyExpander是一个HeaderedContentControl,它包含一个IsExpanded用于指示当前是展开还是折叠。ControlTemplate中包含ExpanderToggleButton及ContentPresenter两个元素。
3. 使用RelativeSource之前已经介绍过TemplateBinding,通常ControlTemplate中元素都通过TemplateBinding获取控件的属性值。但需要双向绑定的话,就是RelativeSource出场的时候了。
RelativeSource有几种模式,分别是:
FindAncestor,引用数据绑定元素的父链中的上级。 这可用于绑定到特定类型的上级或其子类。
PreviousData,允许在当前显示的数据项列表中绑定上一个数据项(不是包含数据项的控件)。
Self,引用正在其上设置绑定的元素,并允许你将该元素的一个属性绑定到同一元素的其他属性上。
TemplatedParent,引用应用了模板的元素,其中此模板中存在数据绑定元素。。
ControlTemplate中主要使用RelativeSource Mode=TemplatedParent的Binding,它相当于TemplateBinding的双向绑定版本。,主要是为了可以和控件本身进行双向绑定。ExpanderToggleButton.IsChecked使用这种绑定与Expander的IsExpanded关联,当Expander.IsChecked为True时ExpanderToggleButton处于选中的状态。
IsChecked="{Binding IsExpanded,RelativeSource={RelativeSource Mode=TemplatedParent},Mode=TwoWay}"接下来分别用几种技术实现Expander.IsChecked为True时显示ContentPresenter。
4. 使用Trigger <ControlTemplate TargetType="{x:Type local:ExpanderUsingTrigger}"> <Border Background="{TemplateBinding Background}"> ...... </Border> <ControlTemplate.Triggers> <Trigger Property="IsExpanded" Value="True"> <Setter Property="Visibility" TargetName="ContentPresenter" Value="Visible" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate>可以为ControlTemplate添加,内容为Trigger或EventTrigger的集合,Triggers通过响应属性值变更或事件更改控件的外观。
大部分情况下Trigger简单好用,但滥用或错误使用将使ControlTemplate的各个状态之间变得很混乱。例如当可以影响外观的属性超过一定数量,并且这些属性可以组成不同的组合,Trigger将要处理无数种情况。
5. 使用TemplatePart