[翻译]Part&States Model With VisualStateManager(3/4)
[原文地址]
这是系列教程的第三篇.
上一次,你学会了如何对现有的控件使用VisualStateManager来改变皮肤外观.在这一篇中,你会看到如何创建基于"部件"与"状态"的自定义控件.同时我们还会探索创建一些更精密复杂的视觉过渡效果.
视觉状态管理器VisualStageManger
在上一篇中我们曾简述过,但现在我们正式的介绍VisualStageManager视觉状态管理器
![]()
VisualStateManager是一个类,用来管理控件的视觉状态."Visual"是关键字(用来管理视觉,非逻辑)-控件的逻辑仍然只负责逻辑状态.
VSM暴露PME的二个片段:
- 一个VisualStageGroups attached property(附加属性)
- 这个属性放在控件模板的RootVisual并包含所有与外观有关的视觉状态与过渡效果
- 一个静态GoToStage()方法
- 这个方法的产生原因是因为VisualStageManager需要来控制控件的视觉由一个视觉状态过渡到其它状态
上一次,我们专注于XAML中的VisualStageGroups属性.今天,我们深入到控件的代码中的GoToStage()方法.
WeatherControl
我们今天会来看一个简单的自定义控件 WeatherControl. 控件的部分代码可以在下面看到.(注意:为了可读性,我折叠了一些代码片段,你可以从这里找到完整的代码.)
public class WeatherControl : Control{public WeatherControl(){DefaultStyleKey = typeof(WeatherControl);}// OnApplyTemplate()public override void OnApplyTemplate(){base.OnApplyTemplate();}// Temperature DPpublic static readonly DependencyProperty TemperatureProperty = DependencyProperty.Register("Condition", typeof(Condition), typeof(WeatherControl), null);public string Temperature{get { /*…*/ }set { /*…*/ }}// Condition DP public static readonly DependencyProperty ConditionProperty = DependencyProperty.Register("Condition", typeof(Condition), typeof(WeatherControl), new PropertyMetadata(new PropertyChangedCallback(WeatherControl.OnConditionPropertyChanged)));public Condition Condition{get { /*…*/ }set { /*…*/ }}// ConditionDescription DP public static readonly DependencyProperty ConditionDescriptionProperty = DependencyProperty.Register("ConditionDescription", typeof(string), typeof(WeatherControl), null);public string ConditionDescription{get { /*…*/ }set { /*…*/}}// Property change notification private static void OnConditionPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e){WeatherControl weather = d as WeatherControl;//… weather.OnWeatherChange(null);}// OnWeatherChange virtual protected virtual void OnWeatherChange(RoutedEventArgs e){ }}
你可以看看我们的WeatherControl…
- 是一个自定义控件,继承自Control.
- 定义内置Style,并由DefaultStyleKey来标识.
- 有三个公共的依赖属性dependency properties:
- Temperature
- Condition
- ConditionDescription
如果让我们的WeatherControl能够使用VSM来换皮肤,我们需要:
- 定义一个控件约定
- 找到和操作parts部件
- 使用VisualStageManager来接管合适的状态
Here we go!
定义控件约定
控件的代码负责描述控件的约定.意味着必须声明任意和所有的期望出现的部件(Parts)与状态(Stages).这是使用metadata完成的在类中的声明:
[TemplatePart(Name="Core", Type=typeof(FrameworkElement))][TemplateVisualState(Name="Normal", GroupName="CommonStates")][TemplateVisualState(Name="MouseOver", GroupName="CommonStates")][TemplateVisualState(Name="Pressed", GroupName="CommonStates")][TemplateVisualState(Name="Sunny", GroupName="WeatherStates")][TemplateVisualState(Name="PartlyCloudy", GroupName="WeatherStates")][TemplateVisualState(Name="Cloudy", GroupName="WeatherStates")][TemplateVisualState(Name="Rainy", GroupName="WeatherStates")]public class WeatherControl : Control {… }
在上面的片段中,有二个attribute类:
- TemplatePartAttribute
- 指定部件名称与期望的类型
- TemplateVisualStateAttribute
- 指定状态和他所属的状态组的名称
这些metadata并不是实时调用的.但他能被Expression Blend工具识别(以用于可视化编辑的支持).
这些在WeatherControl上的attributes给了控件下列的内容:
![]()
现在,我们来看看如何编写部件的操作代码.
找到部件
被命名的部件需要在控件代码中手动编写代码来找到相应的部件.这个操作在OnApplyTemplate()这个虚方法中执行当一个template被应用时.
// OnApplyTemplate public override void OnApplyTemplate() { base.OnApplyTemplate(); CorePart = (FrameworkElement)GetTemplateChild("Core"); } // private CorePart property private FrameworkElement CorePart { get { return corePart; } set { FrameworkElement oldCorePart = corePart; if (oldCorePart != null) { oldCorePart.MouseEnter -= new MouseEventHandler(corePart_MouseEnter); oldCorePart.MouseLeave -= new MouseEventHandler(corePart_MouseLeave); oldCorePart.MouseLeftButtonDown -= new MouseButtonEventHandler(corePart_MouseLeftButtonDown); oldCorePart.MouseLeftButtonUp -= new MouseButtonEventHandler(corePart_MouseLeftButtonUp); } corePart = value; if (corePart != null) { corePart.MouseEnter += new MouseEventHandler(corePart_MouseEnter); corePart.MouseLeave += new MouseEventHandler(corePart_MouseLeave); corePart.MouseLeftButtonDown += new MouseButtonEventHandler(corePart_MouseLeftButtonDown); corePart.MouseLeftButtonUp += new MouseButtonEventHandler(corePart_MouseLeftButtonUp); } } }
你需要使用GetTemplateChild() 这个helper方法来找到模板中的被命名元素.
在上面的例子中,我们找到了"Core"部件,这是一个我们会用于确定当控件引发MouseOver或Pressed状态时的部件.值得注意的是setter访问器的逻辑声明并不是在template中声明的.这很重要,因为控件需要足够健全以便于当某个部件在template中没有时也能够正常工作.
初始化状态改变
控件代码负责向VisualStateManager告之什么时候状态改变.因此,代码必须维护着logical state machine与visual state machine的状态.
所有Silverlight 2的内置控件都创建了简单的helper方法去辅助状态变化.我们推荐你使用这个简单的模式:
// GoToState() helper private void GoToState(bool useTransitions) { // Go to states in NormalStates state group if (isPressed) { VisualStateManager.GoToState(this, "Pressed", useTransitions); } else if (isMouseOver) { VisualStateManager.GoToState(this, "MouseOver", useTransitions); } else { VisualStateManager.GoToState(this, "Normal", useTransitions); } // Go to states in WeatherStates state group if (Condition == Condition.PartlyCloudy) { VisualStateManager.GoToState(this, "PartlyCloudy", useTransitions); } else if (Condition == Condition.Sunny) { VisualStateManager.GoToState(this, "Sunny", useTransitions); } else if (Condition == Condition.Cloudy) { VisualStateManager.GoToState(this, "Cloudy", useTransitions); } else { VisualStateManager.GoToState(this, "Rainy", useTransitions); } }
GoToStage helper方法包含几个用于确定当前视觉状态的语法.他告诉VisualStateManager去初始化合适的状态变化.然后调用静态方法
public static bool VisualStateManager.GoToState(Control control, string stateName, bool useTransitions)
就像你所看到的,这个方法中…
- 有三个参数:
- control: 控件的实例
- stateName: 要去的视觉状态的名称
- usetTransitions: 确定正在进行过渡效果的一个标记
- 返回一个 bool 型
- 如果状态名找到就返回true,返之则false.
- 无法满足if条件的情况…
- 控件在处于发生过的visual state下
- visual state无法找到
大部分控件作者会在三个情况下调用GoToStage() helper
- OnApplyTemplate() 没有任何过渡效果
- 当控件第一次呈现,我们会呈现在合适的状态下,并没有任何过渡效果加于上面.
- 在确定的属性通知Handler中
- 在确定的事件Handler中
在我们的WeatherControl中,我们添加以下调用:
// OnApplyTemplate public override void OnApplyTemplate() { base.OnApplyTemplate(); CorePart = (FrameworkElement)GetTemplateChild("Core"); GoToState(false); } // Property Change Notifications protected virtual void OnWeatherChange(RoutedEventArgs e) { GoToState(true); } // Event Handlers void corePart_MouseEnter(object sender, MouseEventArgs e) { isMouseOver = true; GoToState(true); } void corePart_MouseLeave(object sender, MouseEventArgs e) { isMouseOver = false; GoToState(true); } void corePart_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { isPressed = true; GoToState(true);} void corePart_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { isPressed = false;GoToState(true); }
我们在下面情况时,需要初始化状态变化:
- 模板被初次应用
- 条件属性变化
- 鼠标事件在由Core部件激发
添加内置Style
现在我们看看我们的控件逻辑!
我使我们的ControlTemplate变得非常有趣,有趣意味着模板会变得冗长..不管怎样,看看编辑后的版本:
:
你可以看到ControlTemplate中,我做了:
- 定义了7个VisualStates:
- 定义了storyboard资源来声明状态的storyboard
- 提供一个默认的VisualTransition,用于CommonStages和WeatherStates
- 指定了VisualTransitions,用于CommonStages的certain stage changes
我们来运行一下!

添加专门的过渡效果Transitions
默认的过渡效果还可以.但,为了做得更好,我们加一些更多的自定义的视觉过渡效果.
下面是我们在不同weather状态下的不同外观:
![]()
当我们的控件从Sunny变为PartlyCloudy时,我们不想让云层效果慢慢动画过来,替代方法是,让他从左边进来.
![]()
为了让自定义的过渡效果像这个一样,你可以声明一个详细的过渡故事板:
现在,当VisualStateManager为Sunny到PartlyCloudy状态变化生成动画时,不会花很长时间产生BottomCloud的透明度动画.会马上运行我们定义的这个详细的故事板动画.
为了更好的理解生成动画与详细故事板之间的作用关系,我们看下面的示例:
![]()
这里,我们有二个视觉状态:Foo & Bar.每一个动画有一个不同的属性.
这些动画是如何创建的?
- VSM会生成属性A,C和D之间的过渡动画
- A,C和D会在一个或二个状态间发生动画,并且不会执行VisualTransition.Storyboard下定义的详细故事板
- VSM会根据详细故事板运行B,E和G之间的动画
- B,E和G会根据VisualTransition.Storyboard来动画.VSM不会自动为这些属性生成动画.
- VSM不会为属性F生成动画.
- F会由Foo & Bar状态中的ObjectAnimation来引发动画.他不会被VSM去生成程序化的动画.因此,属性F会简单的根据Bar的值来发生动画
返回到WeatherControl,我们同样加了明确的过渡效果为以下几个状态Sunny->PartlyCloudy, Sunny->Cloudy, and PartlyCloudy->Cloudy.

下一次
这就是我们学到的关于自定义控件中如何使用VSM,希望你能获得自定义详细过渡效果的乐趣.
下一次,在这系列教程的最后一篇,我们会给出一些使用"部件"与"状态"模式的推荐方式,你还会了解更我关于此模式在未来Silverlight及WPF中的规划
下面再看看CheckStates:


