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與代碼分離原則及開放封閉原則。
