[UWP]了解模板化控件(5.1):TemplatePart vs. VisualState
1. TemplatePart vs. VisualState
在前面两篇文章中分别使用了TemplatePart及VisualState的方式实现了相同的功能,其中明显VisualState的方式更灵活一些。如果遇到这种情况通常我更倾向使用VisualState。不过在实际应用中这两种实现方式并不是互斥的,很多模板化控件都同时使用这两种方式,
使用VisualState有如下好处:
- 代码和UI分离。
- 可以更灵活地扩展控件。
- 可以使用Blend轻松实现动画。
并不是说VisualState好处这么多就一定要用VisualState实现所有功能,下面这些情况我会选择使用TemplatePart:
- 需要快速实现一个控件。
- 某个行为时固定的,不需要扩展。
- 需要在代码中操作UI,譬如Slider或ComboBox。
- 为了强调某个部件是控件必须的。
- 为了隐藏实现细节,限制派生类或ControlTemplate修改重要的逻辑。
其中,使用TemplatePart产生的扩展性问题是我谨慎使用这种方案的最大因素。
2. TemplatePart vs. TemplateBinding
除了VisualState,TemplatePart的功能也常常会被TemplateBinding代替。前面的例子展示了使用VisualState在UI上的优势,这次用另一个控件DateTimeSelector来讨论使用TemplatePart在扩展性上的其它问题。
2.1 使用TemplatePart
DateTimeSelector组合了CalendarDatePicker和TimePicker,用于选择日期和时间(SelectedDateTime)。它的XAML如下:
<Style TargetType="local:DateTimeSelector">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DateTimeSelector">
<StackPanel Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<CalendarDatePicker x:Name="DateElement"
Margin="0,0,0,5" />
<TimePicker x:Name="TimeElement" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
代码如下:
[TemplatePart(Name = DateElementPartName, Type = typeof(CalendarDatePicker))]
[TemplatePart(Name = TimeElementPartName, Type = typeof(TimePicker))]
public class DateTimeSelector : Control
{
public const string DateElementPartName = "DateElement";
public const string TimeElementPartName = "TimeElement";
/// <summary>
/// 标识 SelectedDateTime 依赖属性。
/// </summary>
public static readonly DependencyProperty SelectedDateTimeProperty =
DependencyProperty.Register("SelectedDateTime", typeof(DateTime), typeof(DateTimeSelector), new PropertyMetadata(DateTime.Now, OnSelectedDateTimeChanged));
private static void OnSelectedDateTimeChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
DateTimeSelector target = obj as DateTimeSelector;
DateTime oldValue = (DateTime)args.OldValue;
DateTime newValue = (DateTime)args.NewValue;
if (oldValue != newValue)
target.OnSelectedDateTimeChanged(oldValue, newValue);
}
public DateTimeSelector()
{
this.DefaultStyleKey = typeof(DateTimeSelector);
}
/// <summary>
/// 获取或设置SelectedDateTime的值
/// </summary>
public DateTime SelectedDateTime
{
get { return (DateTime)GetValue(SelectedDateTimeProperty); }
set { SetValue(SelectedDateTimeProperty, value); }
}
private CalendarDatePicker _dateElement;
private TimePicker _timeElement;
private bool _isUpdatingDateTime;
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (_dateElement != null)
_dateElement.DateChanged -= OnDateElementDateChanged;
_dateElement = GetTemplateChild(DateElementPartName) as CalendarDatePicker;
if (_dateElement != null)
_dateElement.DateChanged += OnDateElementDateChanged;
if (_timeElement != null)
_timeElement.TimeChanged -= OnTimeElementTimeChanged;
_timeElement = GetTemplateChild(TimeElementPartName) as TimePicker;
if (_timeElement != null)
_timeElement.TimeChanged += OnTimeElementTimeChanged;
UpdateElement();
}
protected virtual void OnSelectedDateTimeChanged(DateTime oldValue, DateTime newValue)
{
UpdateElement();
}
private void OnDateElementDateChanged(CalendarDatePicker sender, CalendarDatePickerDateChangedEventArgs args)
{
UpdateSelectDateTime();
}
private void OnTimeElementTimeChanged(object sender, TimePickerValueChangedEventArgs e)
{
UpdateSelectDateTime();
}
private void UpdateElement()
{
_isUpdatingDateTime = true;
try
{
if (_dateElement != null)
_dateElement.Date = SelectedDateTime.Date;
if (_timeElement != null)
_timeElement.Time = SelectedDateTime.TimeOfDay;
}
finally
{
_isUpdatingDateTime = false;
}
}
private void UpdateSelectDateTime()
{
if (_isUpdatingDateTime)
return;
DateTime dateTime = DateTime.Now;
if (_dateElement != null && _dateElement.Date.HasValue)
dateTime = _dateElement.Date.Value.Date;
if (_timeElement != null)
dateTime = dateTime.Add(_timeElement.Time);
SelectedDateTime = dateTime;
}
}
可以看出,DateTimeSelector通过监视CalendarDatePicker的DateChanged和TimePicker的TimeChanged来改变SelectedDateTime的值。
DateTimeSelector的代码很简单,控件也工作得很好,但如果某天需要将CalendarDatePicker 替换为DatePicker或某个第三方的日期选择控件,DateTimeSelector就无能为力了,既不能通过修改ControlTemplate,也不能通过继承来达到目的。
2.2. 使用TemplateBinding
通常在构建这类控件时应先考虑它的数据和行为,而不关心它的UI。DateTimeSelector最核心的功能是通过选择Date和Time得出组合起来的DateTime,那么就可以先写出如下的类:
public class DateTimeSelector2 : Control
{
/// <summary>
/// 标识 Date 依赖属性。
/// </summary>
public static readonly DependencyProperty DateProperty =
DependencyProperty.Register("Date", typeof(DateTime), typeof(DateTimeSelector2), new PropertyMetadata(DateTime.Now, OnDateChanged));
private static void OnDateChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
DateTimeSelector2 target = obj as DateTimeSelector2;
DateTime oldValue = (DateTime)args.OldValue;
DateTime newValue = (DateTime)args.NewValue;
if (oldValue != newValue)
target.OnDateChanged(oldValue, newValue);
}
/// <summary>
/// 标识 Time 依赖属性。
/// </summary>
public static readonly DependencyProperty TimeProperty =
DependencyProperty.Register("Time", typeof(TimeSpan), typeof(DateTimeSelector2), new PropertyMetadata(TimeSpan.Zero, OnTimeChanged));
private static void OnTimeChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
DateTimeSelector2 target = obj as DateTimeSelector2;
TimeSpan oldValue = (TimeSpan)args.OldValue;
TimeSpan newValue = (TimeSpan)args.NewValue;
if (oldValue != newValue)
target.OnTimeChanged(oldValue, newValue);
}
/// <summary>
/// 标识 DateTime 依赖属性。
/// </summary>
public static readonly DependencyProperty DateTimeProperty =
DependencyProperty.Register("DateTime", typeof(DateTime), typeof(DateTimeSelector2), new PropertyMetadata(DateTime.Now, OnDateTimeChanged));
private static void OnDateTimeChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
DateTimeSelector2 target = obj as DateTimeSelector2;
DateTime oldValue = (DateTime)args.OldValue;
DateTime newValue = (DateTime)args.NewValue;
if (oldValue != newValue)
target.OnDateTimeChanged(oldValue, newValue);
}
public DateTimeSelector2()
{
this.DefaultStyleKey = typeof(DateTimeSelector2);
}
/// <summary>
/// 获取或设置Date的值
/// </summary>
public DateTime Date
{
get { return (DateTime)GetValue(DateProperty); }
set { SetValue(DateProperty, value); }
}
/// <summary>
/// 获取或设置Time的值
/// </summary>
public TimeSpan Time
{
get { return (TimeSpan)GetValue(TimeProperty); }
set { SetValue(TimeProperty, value); }
}
/// <summary>
/// 获取或设置DateTime的值
/// </summary>
public DateTime DateTime
{
get { return (DateTime)GetValue(DateTimeProperty); }
set { SetValue(DateTimeProperty, value); }
}
private bool _isUpdatingDateTime;
protected virtual void OnDateChanged(DateTime oldValue, DateTime newValue)
{
UpdateDateTime();
}
protected virtual void OnTimeChanged(TimeSpan oldValue, TimeSpan newValue)
{
UpdateDateTime();
}
protected virtual void OnDateTimeChanged(DateTime oldValue, DateTime newValue)
{
_isUpdatingDateTime = true;
try
{
Date = newValue.Date;
Time = newValue.TimeOfDay;
}
finally
{
_isUpdatingDateTime = false;
}
}
private void UpdateDateTime()
{
if (_isUpdatingDateTime)
return;
DateTime = Date.Date.Add(Time);
}
}
控件的代码并不清楚ControlTemplate中包含什么控件,它只关心自己的数据。
XAML中通过绑定使用这些数据。
<Style TargetType="local:DateTimeSelector2">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DateTimeSelector2">
<StackPanel Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<CalendarDatePicker Margin="0,0,0,5"
Date="{Binding Date,RelativeSource={RelativeSource Mode=TemplatedParent},Mode=TwoWay,Converter={StaticResource DateTimeOffsetConverter}}" />
<TimePicker Time="{Binding Time,RelativeSource={RelativeSource Mode=TemplatedParent},Mode=TwoWay}" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="DateTimeSelector2CustomStyle"
TargetType="local:DateTimeSelector2">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DateTimeSelector2">
<StackPanel Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<DatePicker Margin="0,0,0,5"
Date="{Binding Date,RelativeSource={RelativeSource Mode=TemplatedParent},Mode=TwoWay,Converter={StaticResource DateTimeOffsetConverter}}" />
<TimePicker Time="{Binding Time,RelativeSource={RelativeSource Mode=TemplatedParent},Mode=TwoWay}" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

这里给出了两个Style,分别使用了CalendarDatePicker 和DatePicker ,通过TwoWay Binding访问DateTimeSelector2中的Date属性。如果你的TemplatedControl需要有良好的扩展能力,可以尝试使用这种方式。
[UWP]了解模板化控件(5.1):TemplatePart vs. VisualState的更多相关文章
- [UWP]了解模板化控件(4):TemplatePart
1. TemplatePart TemplatePart(部件)是指ControlTemplate中的命名元素.控件逻辑预期这些部分存在于ControlTemplate中,并且使用protected ...
- [UWP 自定义控件]了解模板化控件(5.1):TemplatePart vs. VisualState
1. TemplatePart vs. VisualState 在前面两篇文章中分别使用了TemplatePart及VisualState的方式实现了相同的功能,其中明显VisualState的方式更 ...
- [UWP]了解模板化控件(10):原则与技巧
1. 原则 推荐以符合以下原则的方式编写模板化控件: 选择合适的父类:选择合适的父类可以节省大量的工作,从UWP自带的控件中选择父类是最安全的做法,通常的选择是Control.ContentContr ...
- [UWP 自定义控件]了解模板化控件(4):TemplatePart
1. TemplatePart TemplatePart(部件)是指ControlTemplate中的命名元素.控件逻辑预期这些部分存在于ControlTemplate中,并且使用protected ...
- [UWP]了解模板化控件(5):VisualState
1. 功能需求 使用TemplatePart实现上篇文章的两个需求(Header为空时隐藏HeaderContentPresenter,鼠标没有放在控件上时HeaderContentPresent半透 ...
- [UWP]了解模板化控件(8):ItemsControl
1. 模仿ItemsControl 顾名思义,ItemsControl是展示一组数据的控件,它是UWP UI系统中最重要的控件之一,和展示单一数据的ContentControl构成了UWP UI的绝大 ...
- [UWP]了解模板化控件(9):UI指南
1. 使用TemplateSettings统一外观 TemplateSettings提供一组只读属性,用于在新建ControlTemplate时使用这些约定的属性. 譬如,修改HeaderedCont ...
- [UWP]了解模板化控件(1):基础知识
1.概述 UWP允许开发者通过两种方式创建自定义的控件:UserControl和TemplatedControl(模板化控件).这个主题主要讲述如何创建和理解模板化控件,目标是能理解模板化控件常见的知 ...
- [UWP]了解模板化控件(2):模仿ContentControl
ContentControl是最简单的TemplatedControl,而且它在UWP出场频率很高.ContentControl和Panel是VisualTree的基础,可以说几乎所有VisualTr ...
随机推荐
- eclipse 修改中英文显示
1.关闭eclipse 2.右键->选择属性--->打开文件位置 3.找到“eclipse.ini”文件,用EditPlus打开 4.在里面加 “-Duser.language=en”,即 ...
- CJOJ 2171 火车站开饭店(树型动态规划)
CJOJ 2171 火车站开饭店(树型动态规划) Description 政府邀请了你在火车站开饭店,但不允许同时在两个相连的火车站开.任意两个火车站有且只有一条路径,每个火车站最多有 50 个和它相 ...
- 计算BMI
用一个小程序计算BMI 代码如下: package Day06; public class BMI { private String name; private int age; private do ...
- docker~学习笔记索引
回到占占推荐博客索引 使用docker也有段时间了,写了不少文章与总结,下面把它整理个目录出来,方便大家去学习与检索! docker~学习笔记索引 docker~linux下的部署和基本命令(2017 ...
- Work 1(导游类)(2017.06.27)
- C#中的Infinity和NaN
C#中double和float类型有两个特殊值: Infinity(无穷大):5.0 / 0.0 = Infinity NaN(not a number):0.0 / 0.0 = NaN 计算表达式 ...
- (转)Linux 下 查看以及修改文件权限
场景:Linux环境下远程部署项目,发现因为文件权限问题,不能执行远端的可执行文件.问题还没解决,待议... 1 查看权限 在终端输入: ls -l xxx.xxx (xxx.xxx是文件名) 那么就 ...
- C语言基础 - 实现动态数组并增加内存管理
用C语言实现一个动态数组,并对外暴露出对数组的增.删.改.查函数 (可以存储任意类型的元素并实现内存管理) 这里我的编译器就是xcode 分析: 模拟存放 一个 People类 有2个属性 字符串类型 ...
- iOS字典转模型MJExtension使用
如果项目是纯OC的建议使用,MJExtension是一套字典和模型之间互相转换的超轻量级框架,可以轻松完成: 字典(JSON) --> 模型(Model) 模型(Model) --> 字典 ...
- 修改wampsever默认密码
wamp初始默认mysql账号:root 密码: 空 准备修改为,账号:root 密码:xys829475 1.在phpMyAdmin界面中点击[用户],将用户概况中的所有用户名为[root]的用户的 ...