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,使用上會造成一些混亂。