[WPF自定义控件库] 自定义控件的代码如何与ControlTemplate交互 (2)

TemplatePart(部件)是指ControlTemplate中的命名元素(如上面XAML中的“HeaderElement”)。控件逻辑预期这些部分存在于ControlTemplate中,控件在加载ControlTemplate后会调用OnApplyTemplate,可以在这个函数中调用protected DependencyObject GetTemplateChild(String childName)获取模板中指定名字的部件。

[TemplatePart(Name =ContentPresenterName,Type =typeof(UIElement))] public class ExpanderUsingPart : MyExpander { private const string ContentPresenterName = "ContentPresenter"; protected UIElement ContentPresenter { get; private set; } public override void OnApplyTemplate() { base.OnApplyTemplate(); ContentPresenter = GetTemplateChild(ContentPresenterName) as UIElement; UpdateContentPresenter(); } protected override void OnIsExpandedChanged(bool oldValue, bool newValue) { base.OnIsExpandedChanged(oldValue, newValue); UpdateContentPresenter(); } private void UpdateContentPresenter() { if (ContentPresenter == null) return; ContentPresenter.Visibility = IsExpanded ? Visibility.Visible : Visibility.Collapsed; } }

上面的代码实现了获取HeaderElement并为它订阅鼠标点击事件。由于Template可能多次加载,或者不能正确获取TemplatePart,所以使用TemplatePart前应该先判断是否为空;如果要订阅事件,应该先取消订阅。

注意:不要在Loaded事件中尝试调用GetTemplateChild,因为Loaded的时候OnApplyTemplate不一定已经被调用,而且Loaded更容易被多次触发。

TemplatePartAttribute协定

有时,为了表明控件期待在ControlTemplate存在某个特定部件,防止编辑ControlTemplate的开发人员删除它,控件上会添加添加TemplatePartAttribute协定。上面代码中即包含这个协定:

[TemplatePart(Name =ContentPresenterName,Type =typeof(UIElement))]

这段代码的意思是期待在ControlTemplate中存在名称为 "ContentPresenterName",类型为UIElement的部件。

TemplatePartAttribute在UWP中的作用好像被弱化了,不止在UWP原生控件中见不到TemplatePartAttribute,甚至在Blend中“部件”窗口也消失了。可能UWP更加建议使用VisualState。

使用TemplatePart需要遵循以下原则:

尽可能减少TemplarePartAttribute协定。

在使用TemplatePart之前检查其是否为Null。

如果ControlTemplate没有遵循TemplatePartAttribute协定也不应该抛出异常,缺少部分功能可以接受,但要确保程序不会报错。

6. 使用VisualState

VisualState 指定控件处于特定状态时的外观。控件的代码使用VisualStateManager.GoToState(Control control, string stateName,bool useTransitions)指定控件处于何种VisualState,控件的ControlTemplate中根节点使用VisualStateManager.VisualStateGroups附加属性,并在其中确定各个VisualState的外观。

[TemplateVisualState(Name = StateExpanded, GroupName = GroupExpansion)] [TemplateVisualState(Name = StateCollapsed, GroupName = GroupExpansion)] public class ExpanderUsingState : MyExpander { public const string GroupExpansion = "ExpansionStates"; public const string StateExpanded = "Expanded"; public const string StateCollapsed = "Collapsed"; public ExpanderUsingState() { DefaultStyleKey = typeof(ExpanderUsingState); } protected override void OnIsExpandedChanged(bool oldValue, bool newValue) { base.OnIsExpandedChanged(oldValue, newValue); UpdateVisualStates(true); } public override void OnApplyTemplate() { base.OnApplyTemplate(); UpdateVisualStates(false); } protected virtual void UpdateVisualStates(bool useTransitions) { VisualStateManager.GoToState(this, IsExpanded ? StateExpanded : StateCollapsed, useTransitions); } } <ControlTemplate TargetType="{x:Type local:ExpanderUsingState}"> <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> <VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="ExpansionStates"> <VisualState x:Name="Expanded"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="ContentPresenter"> <DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Visible}" /> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> <VisualState x:Name="Collapsed" /> </VisualStateGroup> </VisualStateManager.VisualStateGroups> ...... </Border> </ControlTemplate>

上面的代码演示了如何通过控件的IsExpanded 属性进入不同的VisualState。ExpansionStates是VisualStateGroup,它包含Expanded和Collapsed两个互斥的状态,控件使用VisualStateManager.GoToState(Control control, string stateName,bool useTransitions)更新VisualState。useTransitions这个参数指示是否使用 VisualTransition 进行状态过渡,简单来说即是VisualState之间切换时用不用VisualTransition里面定义的动画。请注意我在OnApplyTemplate()中使用了 UpdateVisualStates(false),这是因为这时候控件还没在UI上呈现,这时候使用动画毫无意义。

使用VisualState的最佳实践

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpjgfw.html