ContentControl是最簡單的TemplatedControl,而且它在UWP出場頻率很高。ContentControl和Panel是VisualTree的基礎,可以說幾乎所有VisualTree上的UI元素的父節點中總有一個ContentControl或Panel。
因為ContentControl很簡單,如果只實現ContentControl最基本功能的話很適合用來做TemplatedControl的入門。這次的內容就是模仿ContentControl實現一個模板化控件MyContentControl,直接繼承自Control。
1. 定義屬性
/// <summary>
/// 獲取或設置Content的值
/// </summary>
public object Content
{
get { return (object)GetValue(ContentProperty); }
set { SetValue(ContentProperty, value); }
}
/// <summary>
/// 標識 Content 依賴屬性。
/// </summary>
public static readonly DependencyProperty ContentProperty =
DependencyProperty.Register("Content", typeof(object), typeof(MyContentControl), new PropertyMetadata(null, OnContentChanged));
private static void OnContentChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
MyContentControl target = obj as MyContentControl;
object oldValue = (object)args.OldValue;
object newValue = (object)args.NewValue;
if (oldValue != newValue)
target.OnContentChanged(oldValue, newValue);
}
protected virtual void OnContentChanged(object oldValue, object newValue)
{
}
/// <summary>
/// 獲取或設置ContentTemplate的值
/// </summary>
public DataTemplate ContentTemplate
{
get { return (DataTemplate)GetValue(ContentTemplateProperty); }
set { SetValue(ContentTemplateProperty, value); }
}
/// <summary>
/// 標識 ContentTemplate 依賴屬性。
/// </summary>
public static readonly DependencyProperty ContentTemplateProperty =
DependencyProperty.Register("ContentTemplate", typeof(DataTemplate), typeof(MyContentControl), new PropertyMetadata(null, OnContentTemplateChanged));
private static void OnContentTemplateChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
MyContentControl target = obj as MyContentControl;
DataTemplate oldValue = (DataTemplate)args.OldValue;
DataTemplate newValue = (DataTemplate)args.NewValue;
if (oldValue != newValue)
target.OnContentTemplateChanged(oldValue, newValue);
}
protected virtual void OnContentTemplateChanged(DataTemplate oldValue, DataTemplate newValue)
{
}
MyContentControl只實現ContentControl兩個最常用的屬性:Content和ContentTemplate。兩個都需要使用依賴屬性,這樣才可以使用Binding和下面會用到的TemplateBinding。
通常重要的屬性都會定義一個通知屬性值變更的virtual方法給派生類使用,如這里的protected virtual void OnContentChanged(object oldValue, object newValue)。為了可以定義virtual方法,要移除類的sealed關鍵字。
值得一提的是Content屬性的類型是Object,這樣Content中既可以放文字,也可以放圖片、Panel等元素。在UWP中如無特殊需求,Content、Header、Title等內容屬性最好都是Object類型,這樣更方便擴展,例如可以在Header放一個Checkbox,這是很常見的做法。
2. 實現外觀
2.1 DefaultStyle
<Style TargetType="local:MyContentControl">
<Setter Property="HorizontalContentAlignment"
Value="Left" />
<Setter Property="VerticalContentAlignment"
Value="Top" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:MyContentControl">
<ContentPresenter Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
將Themes/Generic.xaml中TargetType="local:MyContentControl"的Style改寫成上述XAML。
UWP通過ControlTemplate定義控件的外觀。在MyContentControl中,ControlTemplate只有一個元素ContentPresenter,它使用TemplateBinding綁定到自己所在的MyContentControl的公共屬性。對經常使用ControlTemplate的開發者來說ContentPresenter和TemplateBinding都不是陌生的概念。
2.2 ContentPresenter
ContentPresenter用於顯示內容,默認綁定到ContentControl的Content屬性。基本上所有ContentControl中都包含一個ContentPresenter。ContentPresenter直接從FrameworkElement派生。
2.3 TemplateBinding
用於單向綁定ControlTemplate所在控件的功能屬性,例如Margin="{TemplateBinding Padding}"幾乎等效於Margin="{Binding Margin,RelativeSource={RelativeSource Mode=TemplatedParent},Mode=OneWay}",相當於一種簡化的寫法。但它們之間有如下不同:
- TemplateBinding只能用在ControlTemplate中。
- TemplateBinding的源和目標屬性都必須是依賴屬性。
- TemplateBinding不能使用TypeConverter,所以源屬性和目標屬性必須為相同的數據類型。
通常在ContentPresenter上使用TemplateBinding的屬性不會太多,因為很大一部分Control的屬性都是可屬性值繼承的,即默認使用VisualTree上父節點所設置的屬性值,譬如字體屬性(如FontSize、FontFamily)、DataContext等。
除了可屬性值繼承的屬性,需要適當地將ControlTemplate中的元素屬性綁定到所屬控件的屬性,例如Margin="{TemplateBinding Padding}",這樣可以方便控件的使用者通過屬性調整UI。
2.4 通過Setter改變默認值
通常從父類繼承而來的屬性不會在構造函數中設置默認值,而是在DefaultStyle的Setter中設置默認值。MyContentControl為了將HorizontalContentAlignment改為Left而在Style添加了Property="HorizontalContentAlignment"的Setter。
2.5 ContentPropertyAttribute
<local:MyContentControl>
<local:MyContentControl.Content>
<Rectangle Height="100"
Width="100"
Fill="Red" />
</local:MyContentControl.Content>
</local:MyContentControl>
使用MyContentControl的XAML如上所示,但看起來和ContentControl不同,多了 local:MyContentControl.Content 這行。解決辦法是添加Windows.UI.Xaml.Markup.ContentPropertyAttribute到MyContentControl上。
[ContentProperty(Name = "Content")]
public class MyContentControl : Control
在MyContentControl使用這個Attribute,UWP在解釋XAML時,會將XAML的內容識別為MyContentControl的Content屬性。除了可以省略兩行XAML外,ContentPropertyAttribute還有指出類的主要屬性的作用。譬如Panel添加了[ContentProperty(Name = "Children")],TextBlock添加了[ContentProperty(Name = "Inlines")]。
添加ContentPropertyAttribute后,使用MyContentControl的XAML和ContentControl就基本一致了。
<local:MyContentControl>
<Rectangle Height="100"
Width="100"
Fill="Red" />
</local:MyContentControl>
