使用TemplatePart實作上篇文章的兩個需求(Header為空時隐藏HeaderContentPresenter,滑鼠沒有放在控件上時HeaderContentPresent半透明),雖然功能已經實作,但這樣實作的話基本上也就别想擴充了。譬如開發者做不到通過繼承或修改ControlTemplate實作如下功能:
半透明時的Opacity不是0.7,而是0.5。
半透明和不透明之前切換時有漸變動畫。
當然也并不是不可以用代碼實作這些需求,隻是會複雜很多。大部分的開發者都是對C#熟悉,對XAML陌生,很容易就選擇盡量使用C#實作全部功能,将所有功能集中在同一個地方并用熟悉的語言處理,當然也有這樣做的優點,不過既然在用XAML平台,就應該盡可能利用XAML平台UI和代碼分離的優點。
這篇文章用ContentView2示例講解VisualState如何實作上述的需求,ContentView2和上篇文章的ContentView一樣繼承自HeaderedContentControl。
在實作需求前首先解釋VisualState的概念。
VisualState 指定控件處于特定狀态時的外觀。控件的代碼指定控件處于何種狀态,控件的ControlTemplate中根節點包含VisualStateManager.VisualStateGroups附加屬性,并在其中确定各個VisualState的外觀。
以CheckBox為例,CheckBox基本上包含Unchecked、Checked、Indeterminate三種狀态,它通過IsChecked的值在這三種狀态中轉換。

這三種狀态的外觀如下所示:
實際上Checkbox的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如下:
VisualStateManager用于管理VisualState并操作它們之間的轉換。
ContentView2的其它代碼如上所示,在OnApplyTemplate、OnHeaderChanged及滑鼠進入離開時使用<code>VisualStateManager.GoToState(Control control, string stateName,bool useTransitions)</code>更新VisualState。useTransitions這個參數訓示是否使用 VisualTransition 進行狀态過渡,簡單來說即是VisualState之間切換時用不用VisualTransition裡面定義的動畫。
注意OnApplyTemplate中的這句代碼:UpdateVisualState(false)。控件在加載ControlTemplate時就需要确定它的狀态,一般這時候都不會使用過渡動畫。
VisualStateManager.GoToState不會使控件重複進入某個狀态,譬如如果控件已處于PointerOverState,再次調用<code>VisualStateManager.GoToState(this, PointerOverState, useTransitions)</code>不會觸發任何操作,也不會打斷正在執行的過渡動畫或重複觸發動畫。
到這裡為止ContentView2.cs的工作已經完成,接下來就是XAML的責任了。
使用Blend編輯ContentView2的空白ControlTemplate時,由于已經聲明了TemplateVisualStateAttribute,可以看到在“狀态”視窗已經預設就有定義好的狀态。
編輯後結果如下:
從XAML中可以看出VisualState子節點的Setter是關鍵所在,如PointerOver的VisualState通過Setter将HeaderContentPresenter的Opacity更改為1,滿足了“當滑鼠移動到控件控件上時,設定Header的Opacity=1”這個需求。
另外,<code>VisualStateGroup.Transitions</code> 節點定義了CommonStates在各個狀态之間切換時的過渡動畫。<code>VisualStateManager.GoToState(this, PointerOverState, useTransitions)</code> 中的參數useTransitions即是控制是否使用過渡動畫。示例中使用的過渡動畫為CubicEase,過渡時間為0.5秒。
需要注意的是不同VisualStateGroup之間盡量不要對同一個UI元素的同一個屬性進行操作,否則會引起沖突。
這個主題不會詳細講解使用Blend修改VisualState,因為那會占用很多篇幅。幸好Blend在這方面做得很容易上手,而且多年來基本操作都沒有變過,可以在網上找到很多這方面的文章。
很多時候VisualState方式并不會比TemplatePart方式少寫代碼,譬如ContentView2的代碼量就基本和ContentView一緻,而XAML行數還更多。但VisualState的實作方式更靈活,更加符合UI與代碼分離原則及開放封閉原則。