在Silverlight中你如果想把UI封裝成單獨的一部分或者創建一個新的頁面,你可能會在Visual Studio中通過右擊 “項目-> 添加-> 添加新項->Silverlight用戶控件” 這樣來創建控件。如果你是這么做的,那么這篇文章非常適合你。它將適用於任何基於XAML技術:WPF、silverlight、Windows Phone 和Windows 8 Runtime。
盡管用戶控件很棒,它們能快速的拼在一起,或一次又一次的重復使用,這是它們的很大一個價值所在。但是如果我告訴你還有另一種控件類型,具有干凈的代碼、更強大性能更好,而且比用戶控件的方式更加靈活、重復的使用,那它將會是大量開發人員的最愛嗎?
其實這個你早就知道,因為你已經一直在使用他們:Button、ListBox、ItemsControls、Grid、StackPanel等。你可以查看Xaml Style徹底改變控件的外觀和體驗,而不觸及任何代碼。這是多么強大的想法,看看下面一個Silverlight ListBox 行星DEMO 。在左邊,你會看到一個綁定了行星名單的ListBox。在右邊,你能看到一個太陽系,但事實上,這也是一個ListBox。這里沒有涉及到額外的代碼,完全是由修改Template達到效果。你可以按上下鍵,它有正常ListBox的功能。
讓我重復一遍:做到這一點我沒有添加任何后台代碼到ListBox。事實上,該頁面后台代碼完全是空的。如果你不相信,這里有源碼下載
解剖用戶控件
首先,讓我們解剖一個典型的用戶控件看看,充分了解下它是怎么工作的這是關鍵。在下面我們控件中一部分XAML確定了布局,為了保持它是一個簡單的例子,里有只一個Grid和一個Button。
2 xmlns ="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3 xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml" >
4
5 < Grid x:Name ="LayoutRoot" Background ="White" >
6 < Button Content ="Click Me" Click ="Button_Click" Opacity =".5" />
7 </ Grid >
8 </ UserControl >
我們控件的后台代碼:
2 using System.Windows.Controls;
3 using System.Windows.Media;
4
5 namespace SolarSystemRetemplate
6 {
7 public partial class SilverlightControl1 : UserControl
8 {
9 public SilverlightControl1()
10 {
11 InitializeComponent();
12 }
13
14 private void Button_Click( object sender, RoutedEventArgs e)
15 {
16 LayoutRoot.Background = new SolidColorBrush(Colors.Red);
17 }
18 }
19 }
這里有兩個地方值得注意:”LayoutRoot”是在XAML中使用X:Name定義的,我們在后台代碼中通過這個名字自動獲取了這個變量。 而且Button的Click事件與后台代碼中的事件處理程序奇跡般的掛接了。實際上這是編譯程序和調用方法InitializeComponent處理了這一切--但是有趣的是這個方法在這里不存在。實際上為了表示這是一個局部類,Visual Studio為你私底下創建了一個小(秘密)文件。你可以右擊方法選擇“轉到定義“。下面是該文件的內容:
2
3 public partial class SilverlightControl1 : System.Windows.Controls.UserControl {
4
5 internal System.Windows.Controls.Grid LayoutRoot;
6
7 private bool _contentLoaded;
8
9 /// <summary>
10 /// InitializeComponent
11 /// </summary>
12 [System.Diagnostics.DebuggerNonUserCodeAttribute()]
13 public void InitializeComponent() {
14 if (_contentLoaded)
15 return;
16 _contentLoaded = true;
17 System.Windows.Application.LoadComponent( this,
18 new System.Uri( " /MyApp;component/SilverlightControl1.xaml ",
19 System.UriKind.Relative));
20 this.LayoutRoot = ((System.Windows.Controls.Grid)( this.FindName( " LayoutRoot ")));
21 }
22 }
23 }
你會注意到LayoutRoot在這里被定義成internal,並且它的賦值使用了“FindName”方法。
這就是使用用戶控件的好處之一:它會自動為你做很多工作,但自定義控件則需要你自己來完成這些工作(但是如果考慮到你的效率的話,這並不是那么糟糕)。這里說明下:用戶控件只是另一種自定義控件。
解剖自定義控件
自定義控件不像用戶控件會有一個xaml和一個后台代碼組成,換成除了一個默認的XAML Template以外其余的全部是代碼。你可以認為XAML Template和用戶控件的XAML文件作用一樣,但是這里要注意,XAML Template可以實現任何改變。這里要注意另外一件事件,因為Template不具有Visual Studio為您生成的隱藏代碼局部類,所以任何事件處理程序不能在Template中定義。那么我們怎樣重新創建上述用戶控件為一個自定義控件呢?
對於Silverlight這是很容易的,右鍵單擊您的項目,選擇 “添加 -> 新建項 –> Silverlight模板化控件”。WPF 和Windows Phone不伴隨此模板,所以你必須手工通過創建一個類和一個通用模板文件。你做到了這一點后你會發現兩個新文件:首先一個簡單的C#類,第二個是在\Themes\Generic.xaml下創建了一個新文件。第二個文件匯集了你所有控件的Template樣式。它的名字必須是Generic.xaml而且必須在該目錄下,這樣自定義控件才能使用所有的Template。
下面讓我們一起來看看Template是怎么寫的,和上面用戶控件一樣也是添加了一個Button和一個Grid。
首先第一,注意Border上TemplateBinding語句,它是控件中一個重要的功能。您可以直接在你的控件代碼中定義一個依賴項屬性綁定。由於自定義控件繼承Control,你將自動繼承Background、 BorderBrush、BorderThickness 和其他屬性。請注意 我這里我沒有給按鈕添加click事件。如果這里添加了,模板將會加載失敗。我們將在后台加上click處理程序,接下來,讓我們一起看代碼吧:
2 using System.Windows.Controls;
3 using System.Windows.Controls.Primitives;
4 using System.Windows.Media;
5
6 namespace MyApp
7 {
8 [TemplatePart(Name= " LayoutRoot ", Type= typeof(Control))]
9 [TemplatePart(Name = " ClickButton ", Type = typeof(ButtonBase))]
10 public class TemplatedControl1 : Control
11 {
12 Control layoutRoot;
13 ButtonBase button;
14 public TemplatedControl1()
15 {
16 this.DefaultStyleKey = typeof(TemplatedControl1);
17 }
18 public override void OnApplyTemplate()
19 {
20 if (button != null) // unhook from previous template part
21 {
22 button.Click -= new RoutedEventHandler(button_Click);
23 }
24 button = GetTemplateChild( " ClickButton ") as ButtonBase;
25 if (button != null)
26 {
27 button.Click += new RoutedEventHandler(button_Click);
28 }
29 layoutRoot = GetTemplateChild( " LayoutRoot ") as Panel;
30 base.OnApplyTemplate();
31 }
32
33 private void button_Click( object sender, RoutedEventArgs e)
34 {
35 layoutRoot.Background = new SolidColorBrush(Colors.Red);
36 }
37 } 38 }
首先在控件中聲明”TemplatePart”,它指定預期元素的名稱和和類型。在demo中 LayoutRoot的類型是Panel(Grid的類型是Control)、ClickButton的類型是ButtonBase。這些不是嚴格要求,但是當你調用寫好的自定義控件時,它們能幫助Expression Blend了解模板的要求。我總是控件層次結構申明需要的最小類型,使Template更加靈活。比如我用ButtonBase而不是Button,因為我只要用到定義ButtonBase基類的Click事件。同樣LayoutRoot也一樣,我只需要它的BackGround 屬性。
在構造函數中,我定義了”DefaultStyleKey”,它告訴Framework我在Themes\Generic.xaml中定義了默認Template。
最后,最重要的部分是”OnApplyTemplate”,此方法當Template加載完后被調用。這是我們早期的機會,搶先對Template中controls的引用,即控件中申明的TemplatePart。在這種情況下,我搶先引用在Template中定義ButtonBase,如果找到它,我將給它添加一個click事件處理程序。此外,如果一個新的Template被應用,一定要記住去除以前實例中的事情處理程序。同樣重要要注意的是Template部件總是可選的!所以你要檢查所有引用template的部件是否為null。
添加Visual States到控件
現在添加一些鼠標狀態到我們的控件,並控制動畫何時觸發。在后台代碼中我們定義的添加兩個TemplateVisualState屬性:
2 [TemplateVisualState(GroupName = " HoverStates ", Name = " Normal ")]
接下來給控件添加visual state的觸發:
2 protected override void OnMouseEnter(System.Windows.Input.MouseEventArgs e)
3 {
4 isMouseOver = true;
5 ChangeVisualState( true);
6 base.OnMouseEnter(e);
7 }
8 protected override void OnMouseLeave(System.Windows.Input.MouseEventArgs e)
9 {
10 isMouseOver = false;
11 ChangeVisualState( true);
12 base.OnMouseLeave(e);
13 }
14
15 private void ChangeVisualState( bool useTransitions)
16 {
17 if (isMouseOver)
18 {
19 GoToState(useTransitions, " MouseOver ");
20 }
21 else
22 {
23 GoToState(useTransitions, " Normal ");
24 }
25 }
26
27 private bool GoToState( bool useTransitions, string stateName)
28 {
29 return VisualStateManager.GoToState( this, stateName, useTransitions); 30 }
這正是我們需要的所有代碼。它非常簡單。如果鼠標停留,則觸發MouseOver狀態,否則則觸發正常狀態。請注意,實際上我們沒有真正定義什么是”MouseOver”,這是Template的工作。好接下來讓我們來定義:
2 < Border Background =" {TemplateBinding Background} "
3 BorderBrush =" {TemplateBinding BorderBrush} "
4 BorderThickness =" {TemplateBinding BorderThickness} " >
5 < VisualStateManager.VisualStateGroups >
6 < VisualStateGroup x:Name ="HoverStates" >
7 < VisualState x:Name ="MouseOver" >
8 < Storyboard >
9 < ColorAnimation
10 Storyboard.TargetName ="BackgroundElement"
11 Storyboard.TargetProperty ="(Rectangle.Fill).(SolidColorBrush.Color)"
12 To ="Yellow" Duration ="0:0:.5" />
13 </ Storyboard >
14 </ VisualState >
15 < VisualState x:Name ="Normal" >
16 < Storyboard >
17 < ColorAnimation
18 Storyboard.TargetName ="BackgroundElement"
19 Storyboard.TargetProperty ="(Rectangle.Fill).(SolidColorBrush.Color)"
20 To ="Transparent" Duration ="0:0:.5" />
21 </ Storyboard >
22 </ VisualState >
23 </ VisualStateGroup >
24 </ VisualStateManager.VisualStateGroups >
25 < Grid x:Name ="LayoutRoot" >
26 < Rectangle x:Name ="BackgroundElement" Fill ="Transparent" />
27 < Button x:Name ="ClickButton"
28 Content ="Click me!" Opacity =".5" />
29 </ Grid >
30 </ Border > 31 </ ControlTemplate >
好了,你現在有一個控件,當ButtonBase被點擊以及鼠標懸停或離開時,Panel的背景色會改變,這樣可以解決於很多控件,不用重寫代碼。