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如下:
代码如下:
[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"; ////// 标识 SelectedDateTime 依赖属性。 /// 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); } ////// 获取或设置SelectedDateTime的值 /// 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{ ////// 标识 Date 依赖属性。 /// 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); } ////// 标识 Time 依赖属性。 /// 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); } ////// 标识 DateTime 依赖属性。 /// 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); } ////// 获取或设置Date的值 /// public DateTime Date { get { return (DateTime)GetValue(DateProperty); } set { SetValue(DateProperty, value); } } ////// 获取或设置Time的值 /// public TimeSpan Time { get { return (TimeSpan)GetValue(TimeProperty); } set { SetValue(TimeProperty, value); } } ////// 获取或设置DateTime的值 /// 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,分别使用了CalendarDatePicker 和DatePicker ,通过TwoWay Binding访问DateTimeSelector2中的Date属性。如果你的TemplatedControl需要有良好的扩展能力,可以尝试使用这种方式。