1. 功能需求
使用TemplatePart實現上篇文章的兩個需求(Header爲空時隱藏HeaderContentPresenter,鼠標沒有放在控件上時HeaderContentPresent半透明),雖然功能已經實現,但這樣實現的話基本上也就別想擴展了。譬如開發者做不到通過繼承或修改ControlTemplate實現如下功能:
半透明時的Opacity不是0.7,而是0.5。
半透明和不透明之前切換時有漸變動畫。
當然也並不是不可以用代碼實現這些需求,只是會複雜很多。大部分的開發者都是對C#熟悉,對XAML陌生,很容易就選擇儘量使用C#實現全部功能,將所有功能集中在同一個地方並用熟悉的語言處理,當然也有這樣做的優點,不過既然在用XAML平臺,就應該儘可能利用XAML平臺UI和代碼分離的優點。
這篇文章用ContentView2示例講解VisualState如何實現上述的需求,ContentView2和上篇文章的ContentView一樣繼承自HeaderedContentControl。
2. VisualState
在實現需求前首先解釋VisualState的概念。
VisualState 指定控件處於特定狀態時的外觀。控件的代碼指定控件處於何種狀態,控件的ControlTemplate中根節點包含VisualStateManager.VisualStateGroups附加屬性,並在其中確定各個VisualState的外觀。
以CheckBox爲例,CheckBox基本上包含Unchecked、Checked、Indeterminate三種狀態,它通過IsChecked的值在這三種狀態中轉換。
這三種狀態的外觀如下所示:
實際上Checkbox的VisualState複雜很多,這裏是簡化的模型。
3. 確定VisualState
要使用VisualState,首先要明確控件中包含哪些VisualState。在ContentView2中有兩組VisualState:
CommonStates: 默認是“Normal”,當鼠標進入控件時是“PointerOver”。
HeaderStates: 默認是“NoHeader”,當Header屬性的值不爲空時是“HasHeader”。
其中“CommonStates”、“HeaderStates”稱爲VisualStateGroup,“Normal”、“PointerOver”等稱爲VisualState。在同一個VisualStateGroup中的VisualState是互斥的,控件始終只能處於每組狀態中的一種。例如,控件只能處於NoHeader狀態,或者HasHeader狀態。
模板化控件可以使用TemplateVisualStateAttribute協定聲明它的VisualState,用於通知控件的使用者有這些VisualState可用。TemplateVisualStateAttribute是可選的,而且就算控件聲明瞭這些VisualState,ControlTemplate也可以不包含它們中的任何一個,並且不會引發異常。
ContentView2的TemplateVisualStateAttribute如下:
[TemplateVisualState(Name = NormalState, GroupName = CommonStates)] [TemplateVisualState(Name = PointerOverState,GroupName =CommonStates)] [TemplateVisualState(Name = NoHeaderState, GroupName = HeaderStates)] [TemplateVisualState(Name = HasHeaderState, GroupName = HeaderStates)] public class ContentView2 : HeaderedContentControl { public const string CommonStates = "CommonStates"; public const string NormalState = "Normal"; public const string PointerOverState = "PointerOver"; public const string HeaderStates = "HeaderStates"; public const string NoHeaderState = "NoHeader"; public const string HasHeaderState = "HasHeader"; }
4. VisualStateManager
VisualStateManager用於管理VisualState並操作它們之間的轉換。
public ContentView2() { this.DefaultStyleKey = typeof(ContentView2); } private bool _isPointerEntered; protected override void OnApplyTemplate() { base.OnApplyTemplate(); UpdateVisualState(false); } protected override void OnPointerEntered(PointerRoutedEventArgs e) { base.OnPointerEntered(e); _isPointerEntered = true; UpdateVisualState(); } protected override void OnPointerExited(PointerRoutedEventArgs e) { base.OnPointerExited(e); _isPointerEntered = false; UpdateVisualState(); } protected override void OnHeaderChanged(object oldValue, object newValue) { base.OnHeaderChanged(oldValue, newValue); UpdateVisualState(); } internal virtual void UpdateVisualState(bool useTransitions = true) { if (_isPointerEntered) VisualStateManager.GoToState(this, PointerOverState, useTransitions); else VisualStateManager.GoToState(this, NormalState, useTransitions); if (Header == null) VisualStateManager.GoToState(this, NoHeaderState, useTransitions); else VisualStateManager.GoToState(this, HasHeaderState, useTransitions); }
ContentView2的其它代碼如上所示,在OnApplyTemplate、OnHeaderChanged及鼠標進入離開時使用VisualStateManager.GoToState(Control control, string stateName,bool useTransitions)
更新VisualState。useTransitions這個參數指示是否使用 VisualTransition 進行狀態過渡,簡單來說即是VisualState之間切換時用不用VisualTransition裏面定義的動畫。
注意OnApplyTemplate中的這句代碼:UpdateVisualState(false)。控件在加載ControlTemplate時就需要確定它的狀態,一般這時候都不會使用過渡動畫。
VisualStateManager.GoToState不會使控件重複進入某個狀態,譬如如果控件已處於PointerOverState,再次調用VisualStateManager.GoToState(this, PointerOverState, useTransitions)
不會觸發任何操作,也不會打斷正在執行的過渡動畫或重複觸發動畫。
到這裏爲止ContentView2.cs的工作已經完成,接下來就是XAML的責任了。
5. 使用Blend編輯ControlTemplate
使用Blend編輯ContentView2的空白ControlTemplate時,由於已經聲明瞭TemplateVisualStateAttribute,可以看到在“狀態”窗口已經默認就有定義好的狀態。
編輯後結果如下:
<VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="CommonStates"> <VisualStateGroup.Transitions> <VisualTransition GeneratedDuration="0:0:0.5"> <VisualTransition.GeneratedEasingFunction> <CubicEase EasingMode="EaseInOut" /> </VisualTransition.GeneratedEasingFunction> </VisualTransition> </VisualStateGroup.Transitions> <VisualState x:Name="Normal"> <VisualState.Setters> <Setter Target="HeaderContentPresenter.(UIElement.Opacity)" Value="0.5" /> </VisualState.Setters> </VisualState> <VisualState x:Name="PointerOver"> <VisualState.Setters> <Setter Target="HeaderContentPresenter.(UIElement.Opacity)" Value="1" /> </VisualState.Setters> </VisualState> </VisualStateGroup> <VisualStateGroup x:Name="HeaderStates"> <VisualState x:Name="NoHeader"> <VisualState.Setters> <Setter Target="HeaderContentPresenter.(UIElement.Visibility)" Value="Collapsed" /> </VisualState.Setters> </VisualState> <VisualState x:Name="HasHeader" /> </VisualStateGroup> </VisualStateManager.VisualStateGroups>
從XAML中可以看出VisualState子節點的Setter是關鍵所在,如PointerOver的VisualState通過Setter將HeaderContentPresenter的Opacity更改爲1,滿足了“當鼠標移動到控件控件上時,設置Header的Opacity=1”這個需求。
另外,VisualStateGroup.Transitions
節點定義了CommonStates在各個狀態之間切換時的過渡動畫。VisualStateManager.GoToState(this, PointerOverState, useTransitions)
中的參數useTransitions即是控制是否使用過渡動畫。示例中使用的過渡動畫爲CubicEase,過渡時間爲0.5秒。
需要注意的是不同VisualStateGroup之間儘量不要對同一個UI元素的同一個屬性進行操作,否則會引起衝突。
這個主題不會詳細講解使用Blend修改VisualState,因爲那會佔用很多篇幅。幸好Blend在這方面做得很容易上手,而且多年來基本操作都沒有變過,可以在網上找到很多這方面的文章。
6. 結論
很多時候VisualState方式並不會比TemplatePart方式少寫代碼,譬如ContentView2的代碼量就基本和ContentView一致,而XAML行數還更多。但VisualState的實現方式更靈活,更加符合UI與代碼分離原則及開放封閉原則。