[
原文地址]
这是系列教程的第三篇.
上一次,你学会了如何对现有的控件使用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 DP
public 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 型
- 无法满足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中的规划