专栏首页dino.c的专栏[UWP 自定义控件]了解模板化控件(5.1):TemplatePart vs. VisualState

[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需要有良好的扩展能力,可以尝试使用这种方式。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • [UWP 自定义控件]了解模板化控件(5.2):UserControl vs. TemplatedControl

    在UWP中自定义控件常常会遇到这个问题:使用UserControl还是TemplatedControl来自定义控件。

    dino.c
  • [WPF自定义控件库]了解WPF的布局过程,并利用Measure为Expander添加动画

    这篇文章介绍WPF UI元素的两步布局过程,并且通过Resizer控件介绍只使用Measure可以实现些什么内容。

    dino.c
  • [WPF]何如在Win7使用Aero2主题

    假设我在Windows10的环境新建一个4.6的WPF项目,添加一个ComboBox,并用Blend在这个ComboBox上右键“编辑模板”->“编辑副本”,B...

    dino.c
  • (33) Joda-Time / 计算机程序的思维逻辑

    Joda-Time 上节介绍了JDK API中的日期和时间类,我们提到了JDK API的一些不足,并提到,实践中有一个广泛使用的日期和时间类库,Joda-Tim...

    swiftma
  • ModernPHP读书笔记(三)——PHP的良好实践

    ModernPHP读书笔记(三)——PHP的良好实践 (原创内容,转载请注明来源,谢谢) 一、密码 1、密码不宜用明文存储,也不能用可以解密的...

    用户1327360
  • maven 打包忽略checkstyle

    mvn eclipse:eclipse -Dcheckstyle.skip=true

    shengjk1
  • 实现WordPress提交评论的时删除该页面的WP-Super-Cache缓存的方法

    这篇文章依然是围绕缓存问题展开的,lnmp 环境下的 WordPress 使用 WP-Super-Cache 插件,若要开启 mod_rewrite 缓存模式,...

    张戈
  • Hololens开发学习(八)——Hololens视频流输出方法

    之前分享过Hololens开发学习的心得,最近在做Hololens视频流输出的任务,我也尝试了各种方式,今天给大家分享一下。

    用户7498388
  • 格莱美幕后:人工智能和正在被悄然改变的音乐产业

    量子位 | 李林 问耕 发自 凹非寺 ? Hello, it’s me “向我们缓步走来的,是一位…呃…机器艺人” 这不是未来的场景,而是两天之前。一位通体土豪...

    量子位
  • 使用ConcurrentHashMap实现高效缓存框架

    上述代码中,Computable接口定义的是一类用于执行某种类型计算的策略族。ExpensiveFunction实现了Computable接口,该类...

    用户4143945

扫码关注云+社区

领取腾讯云代金券