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

C#

浏览数:380

2019-5-16

UWP自定义控件的入门文章

1. UserControl vs. TemplatedControl

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

1.1 使用UserControl自定义控件

  • 继承自UserControl。
  • 由复数控件组合而成。
  • 包含XAML及CodeBehind。
  • 优点:
    • 上手简单。
    • 可以在CodeBehind直接访问UI元素。
    • 开发速度很快。
  • 缺点:
    • 不能使用ControlTemplate进行定制。
    • 通常很难继承及扩展。
  • 使用场景:
    • 需要快速实现一个只有简单功能的控件,而且无需扩展性。
    • 不需要可以改变UI。
    • 不需要在不同项目中共享控件。
  • 使用UserControl的控件:
    • Page及DropShadowPanel都是UserControl。

1.2 使用CustomControl自定义控件

  • 继承自Control或其派生类。
  • 代码和XAML分离,可以没有XAML。
  • 可以使用ControlTemplate。
  • 控件库中的控件通常都是CustomControl。
  • 优点:
    • 更加灵活,容易扩展。
    • UI和代码分离。
  • 缺点:
    • 较高的上手难度。
  • 使用场景:
    • 需要一个可以扩展功能的灵活的控件。
    • 需要定制UI。
    • 需要在不同的项目中使用。
  • 使用CustomControl的控件:
    • 控件库中提供的元素,除了直接继承自FrameworkElement的Panel、Shape、TextBlock等少数元素,其它大部分都是CustomControl。

2. 实践:使用UserControl实现DateTimeSelector

上一篇的DateTimeSelector例子很适合讨这个问题。这个控件没有复杂的逻辑,用UserControl的方式实现很简单,代码如下:

public sealed partial class DateTimeSelector3 : UserControl
{
    /// <summary>
    /// 标识 DateTime 依赖属性。
    /// </summary>
    public static readonly DependencyProperty DateTimeProperty =
        DependencyProperty.Register("DateTime", typeof(DateTime?), typeof(DateTimeSelector3), new PropertyMetadata(null, OnDateTimeChanged));

    private static void OnDateTimeChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    {
        DateTimeSelector3 target = obj as DateTimeSelector3;
        DateTime? oldValue = (DateTime?)args.OldValue;
        DateTime? newValue = (DateTime?)args.NewValue;
        if (oldValue != newValue)
            target.OnDateTimeChanged(oldValue, newValue);
    }

    public DateTimeSelector3()
    {
        this.InitializeComponent();
        DateTime = System.DateTime.Now;
        TimeElement.TimeChanged += OnTimeChanged;
        DateElement.DateChanged += OnDateChanged;
    }


    /// <summary>
    /// 获取或设置DateTime的值
    /// </summary>  
    public DateTime? DateTime
    {
        get { return (DateTime?)GetValue(DateTimeProperty); }
        set { SetValue(DateTimeProperty, value); }
    }

    private bool _isUpdatingDateTime;

    private void OnDateTimeChanged(DateTime? oldValue, DateTime? newValue)
    {
        _isUpdatingDateTime = true;
        try
        {
            if (DateElement != null && DateTime != null)
                DateElement.Date = DateTime.Value;

            if (TimeElement != null && DateTime != null)
                TimeElement.Time = DateTime.Value.TimeOfDay;
        }
        finally
        {
            _isUpdatingDateTime = false;
        }
    }

    private void OnDateChanged(object sender, DatePickerValueChangedEventArgs e)
    {
        UpdateDateTime();
    }

    private void OnTimeChanged(object sender, TimePickerValueChangedEventArgs e)
    {
        UpdateDateTime();
    }

    private void UpdateDateTime()
    {
        if (_isUpdatingDateTime)
            return;

        DateTime = DateElement.Date.Date.Add(TimeElement.Time);
    }
}

XAML:

<StackPanel>
    <DatePicker x:Name="DateElement" />
    <TimePicker x:Name="TimeElement"
                Margin="0,5,0,0" />
</StackPanel>

代码真的很简单,不需要GetTemplateChild,不需要DefaultStyleKey,不需要Blend,熟练的话大概5分钟就能写好一个。

使用UserControl有这些好处:

  • 快速。
  • 可以直接查看设计视图,不需要用Blend。
  • 可以直接访问XAML中的元素。

当然坏处也不少:

  • 不可以通过ControlTemplate修改UI。
  • 难以继承并修改。
  • UI和代码高度耦合。

如果控件只是内部使用,不是放在类库中向第三者公开,也没有修改的必要,使用UserControl也是合适的,毕竟它符合80/20原则:使用20%的时间完成了80%的功能。

3. 混合方案

如果需要快速实现控件,又需要适当的扩展能力,可以实现一个继承UserControl的基类,再通过UserControl的方式派生这个基类。

public class DateTimeSelectorBase : UserControl

创建一个名为DateTimeSelectorBase的类,继承自UserControl,其它代码基本上照抄上一篇文章中的DatetimeSelector2,只不过删除了构造函数中的代码,因为不需要DefaultStyle。

然后用普通的方式新建一个UserControl,在XAML和CodeBehind中将基类改成DateTimeSelectorBase,如下所示:

<local:DateTimeSelectorBase x:Class="TemplatedControlSample.DateTimeSelector4"
                            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                            xmlns:local="using:TemplatedControlSample"
                            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                            mc:Ignorable="d"
                            x:Name="DateTimeSelector"
                            d:DesignHeight="300"
                            d:DesignWidth="400">
    <local:DateTimeSelectorBase.Resources>
        <local:DateTimeOffsetConverter x:Key="DateTimeOffsetConverter" />
        <local:TimeSpanConverter x:Key="TimeSpanConverter" />

    </local:DateTimeSelectorBase.Resources>
    <StackPanel>
        <DatePicker Margin="0,0,0,5"
                    Date="{Binding Date,ElementName=DateTimeSelector,Mode=TwoWay,Converter={StaticResource DateTimeOffsetConverter}}" />
        <TimePicker Time="{Binding Time,ElementName=DateTimeSelector,Mode=TwoWay}" />

    </StackPanel>
</local:DateTimeSelectorBase>
public sealed partial class DateTimeSelector4 : DateTimeSelectorBase
{
    public DateTimeSelector4()
    {
        this.InitializeComponent();
    }
}

这样既可以在不同的派生类使用不同的UI,也可以使用设计视图,结合了UserControl和TemplatedControl的优点。缺点是不可以使用ControlTemplate,而且不清楚这个控件的开发者会直观地以为这是TemplatedControl,使用上会造成一些混乱。

作者:dino.c