您的位置:首页 > 其它

[Silverlight入门系列]使用MVVM模式(9): 想在ViewModel中控制Storyboard动画?

2012-07-23 18:18 344 查看
原文 http://www.cnblogs.com/Mainz/archive/2011/08/25/2153828.html

[code] <Image x:Name="myImage"
Source="http://www.silverlightinaction.com/man.png">
<Image.Triggers>
<EventTrigger RoutedEvent="Image.Loaded">
<BeginStoryboard>
<Storyboard x:Name="myStoryboard">
<DoubleAnimation Duration="0:0:2"
Storyboard.TargetName="myImage"
Storyboard.TargetProperty="Opacity"
From="0" To="1" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Image.Triggers>
</Image>
[/code]

解决方法一:ViewModel中用事件Event通知View启动Storyboard动画

ViewModel是对界面逻辑、业务逻辑、和模型数据的封装和抽象,ViewModel不依赖于具体的View视图,所以ViewModel根本 不知道具体的某个Storyboard,怎么去启动这个动画呢?解决问题思路有好多:第一种方法就是很自然的想到在ViewModel中用事件Event通知View启动动画。具体做法是:在ViewModel中添加 一个事件Event,当业务操作好了保存Save成功了用这个事件通知View,这样View在Event的处理函数里面打开动画即可。

ViewModel代码:

[code] public class YourViewModel
{
public delegate void YourEventHandler(object sender, EventArgs e);
public event YourEventHandler YourEvent;
protected void OnYourEvent(EventArgs e){
if (YourEvent != null) YourEvent(this, e);
}
 
//当业务操作好了保存Save成功了触发这个事件
//OnYourEvent(new EventArgs(a));
}
[/code]在Xaml.cs写code behind代码:

[code] var vm = new YourViewModel();
vm.YourEvent += (s,e) =>
{
var story = Resources["YourTransition"] as Storyboard;
story.Begin();
};
this.DataContext = vm;
[/code]

解决方法二:ViewModel属性和View绑定并用Trigger

大家知道,ViewModel的属性可以和View绑定,当属性变化的时候用NotifyPropertyChanged自动通知View。按照这 个思路,我们只要在ViewModel加一个属性,当业务操作好了保存Save成功了就改变这个属性的值,然后就会自动通知View,在View中加个 Trigger,当绑定的值变化的时候就触发启动动画。

假设ViewModel属性为bool
PopupSideShow. 在视图中:


[code] xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
[/code]
[code] xmlns:ei=http://schemas.microsoft.com/expression/2010/interactions
[/code]Storyboard定义在Resource中:(此处Storyboard没实际意义仅为演示)

[code] <UserControl.Resources>
<Storyboard x:Name="popupSideShowStory">
<DoubleAnimation Storyboard.TargetName="PopUpDisplaySide" Storyboard.TargetProperty="Height" From="0" To="{Binding _PopupSideHeight}" Duration="0:0:1" />
<DoubleAnimation Storyboard.TargetName="PopUpDisplaySide" Storyboard.TargetProperty="Width" From="0" To="{Binding _PopupSideWidth}" Duration="0:0:1" />
</Storyboard>
<Storyboard x:Name="popupSideHideStory">
<DoubleAnimation Storyboard.TargetName="PopUpDisplaySide" Storyboard.TargetProperty="Height" From="{Binding _PopupSideHeight}" To="0" Duration="0:0:.8" />
<DoubleAnimation Storyboard.TargetName="PopUpDisplaySide" Storyboard.TargetProperty="Width" From="{Binding _PopupSideWidth}" To="0" Duration="0:0:.8" />
</Storyboard>
</UserControl.Resources>
[/code]在View视图中绑定ViewModel属性为bool
PopupSideShow,并用Trigger实现当绑定的值
PopupSideShow
变化的时候就触发启动动画:


[code] <Grid x:Name="PopUpDisplaySide"Background="White">
<i:Interaction.Triggers>
<ei:DataTrigger Binding="{Binding PopupSideShow}" Value="true">
<ei:ControlStoryboardAction Storyboard="{StaticResource popupSideShowStory}"/>
</ei:DataTrigger>
<ei:DataTrigger Binding="{Binding PopupSideShow}" Value="false">
<ei:ControlStoryboardAction Storyboard="{StaticResource popupSideHideStory}"/>
</ei:DataTrigger>
</i:Interaction.Triggers>
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding _PopupTitle}" FontSize="16" />
</StackPanel>
</Grid>
[/code]


解决方法三:加一个中间人管理Storyboard从而既实现ViewModel和View解耦,又能在ViewModel控制StoryboardViewModel属性和View

既然我们想在ViewModel里面控制Storyboard,而ViewModel又不能依赖具体的View,所以我们可以加个中间人把 Storyboard抽象出来,这样既能实现ViewModel和View解耦,又能在ViewModel通过中间人控制Storyboard。这个思路 我想也是很自然的。但怎么实现呢?首先这个中间人要和View发生联系必须要能在Xaml里面绑定,所以我们要实现 DependencyProperty。

我们首先加一个StoryboardManager:

[code] using System;
using System.Windows;
using System.Windows.Media.Animation;
using System.Collections.Generic;
 
namespace TestVMAnimation
{
public class StoryboardManager
{
public static DependencyProperty IDProperty =
 DependencyProperty.RegisterAttached("ID", typeof(string), typeof(StoryboardManager),
 new PropertyMetadata(null, IdChanged));
 
static readonly Dictionary<string, Storyboard> Storyboards = new Dictionary<string, Storyboard>();
 
public delegate void Callback(object state);
 
/// <summary>
/// IDs the changed.
/// </summary>
/// <param name="obj">The obj.</param>
/// <param name="e">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
private static void IdChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) 
{
 var sb = obj as Storyboard; 
 if (sb == null)
return; 
 
 var key = e.NewValue as string; 
 if (Storyboards.ContainsKey(key))
Storyboards[key] = sb; 
 else
Storyboards.Add(key, sb); 
}
 
/// <summary>
/// Plays the storyboard.
/// </summary>
/// <param name="id">The id.</param>
/// <param name="callback">The callback.</param>
/// <param name="state">The state.</param>
public static void PlayStoryboard(string id, Callback callback, object state) 
{
 if (!Storyboards.ContainsKey(id)) 
{
callback(state); 
return; 
}
 Storyboard sb = Storyboards[id]; 
 EventHandler handler = null;
 EventHandler handlertemp = handler;
 handler = delegate{sb.Completed -= handlertemp; callback(state);};
 sb.Completed += handler; 
 sb.Begin(); 
}
 
/// <summary>
/// Sets the ID.
/// </summary>
/// <param name="obj">The obj.</param>
/// <param name="id">The id.</param>
public static void SetID(DependencyObject obj, string id) 
{
 obj.SetValue(IDProperty, id); 
}
 
/// <summary>
/// Gets the ID.
/// </summary>
/// <param name="obj">The obj.</param>
/// <returns></returns>
public static string GetID(DependencyObject obj) 
{
 return obj.GetValue(IDProperty) as string; 
}
}
 
}
[/code]有了DependencyProperty就可以在Xaml里面绑定了,注意下面的StoryboardManager.ID:

[code] <UserControl.Resources>
<Storyboard x:Key="YourStoryboardResourceKey" 
StoryboardManager:StoryboardManager.ID="YourAnimation">
<DoubleAnimation By="360" Duration="0:0:1" Storyboard.TargetName="btn1"
 Storyboard.TargetProperty="Angle" />
</Storyboard>
</UserControl.Resources>
[/code]在ViewModel里面控制Storyboard很简单,下面这个例子是通过Command调用的,你当然也可以不通过Command直接调用 Storyboard,像本文的例子,可以在ViewModel的业务逻辑里面当业务操作好了保存Save成功了启动Storyboard动画。

[code] public class YourViewModel
{
public ICommand PlayStoryboardCommand{ get; private set;}
 
public YourViewModel()
{
PlayStoryboardCommand = new DelegateCommand(
 () =>{
StoryboardManager.PlayStoryboard("YourAnimation", (o) =>{}, null);
});
}
 
 
}
[/code]

解决方法四:不要在ViewModel里面控制Storyboard,把Transition封装在控件中

用MVVM模式的出发点之一就是分离关注点(Separation of concerns).View负责什么?UI Layout, structure, appearance,animation,那View的CodeBehind(Xaml.cs)可以有什么?View的Code Behind可以有Initialize Component,可以有Xaml里面表示不了的视觉行为,比如复杂动画控制(带callback,completed事件那种)。还可以是视觉元素的控制。总之,只要这些 代码是View该负责的,是高内聚的,是不想被重用的,是不能被测试的,那你就搁在codebehind好了。绝对应该避免业务逻辑在里面哦。某位大神说过,“解决问题的最好办法是thinkdifferent,说不定问题本身就不是个问题”。是的,你想在ViewModel里面控制Storyboard,这本身是不是有问题?想想我们的动画一般在什么时候发生?真的是业务逻辑完成了发生吗?真的和业务逻辑相关吗?不!动画其实是和VisualElement的 VisualState相关。也就是说,我们往往是在某个panel显示/隐藏/打开/关闭的时候有个淡入淡出、推箱子、跳跃、或者x/y/z/3D旋转 的效果(不要告诉我是显示/隐藏panel本身,这个可以和ViewModel的属性绑定的,不是动画)。说白了就是一个transition,从一个 VisualState到另一个VisualState而已。好了,想清楚了,问题就没有了。也就是说,你无须在ViewModel里面控制 Storyboard,只要在View里面定义好VisualState就可以了,封装在控件行为中,把VisualState动画写在控件的模板中,有 关怎么封装Silverlight控件这儿就不多说了,下回有空再说。具体做法可以参考MSDN这个页面,里面就有button的VisualState切换动画,比如MouseOver等:

[code] <Style TargetType="Button">
<Setter Property="Background" Value="#FF1F3B53"/>
<Setter Property="Foreground" Value="#FF000000"/>
<Setter Property="Padding" Value="3"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush">
 <Setter.Value>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFA3AEB9" Offset="0"/>
<GradientStop Color="#FF8399A9" Offset="0.375"/>
<GradientStop Color="#FF718597" Offset="0.375"/>
<GradientStop Color="#FF617584" Offset="1"/>
</LinearGradientBrush>
 </Setter.Value>
</Setter>
<Setter Property="Template">
 <Setter.Value>
<ControlTemplate TargetType="Button">
<Grid>
 <vsm:VisualStateManager.VisualStateGroups>
<vsm:VisualStateGroup x:Name="CommonStates">
 <vsm:VisualState x:Name="Normal"/>
 <vsm:VisualState x:Name="MouseOver">
<Storyboard>
<DoubleAnimation Duration="0" Storyboard.TargetName="BackgroundAnimation" Storyboard.TargetProperty="Opacity" To="1"/>
<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)" To="#F2FFFFFF"/>
<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[2].(GradientStop.Color)" To="#CCFFFFFF"/>
<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[3].(GradientStop.Color)" To="#7FFFFFFF"/>
 </Storyboard>
 </vsm:VisualState>
 <vsm:VisualState x:Name="Pressed">
<Storyboard>
<ColorAnimation Duration="0" Storyboard.TargetName="Background" Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)" To="#FF6DBDD1"/>
<DoubleAnimation Duration="0" Storyboard.TargetName="BackgroundAnimation" Storyboard.TargetProperty="Opacity" To="1"/>
<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[0].(GradientStop.Color)" To="#D8FFFFFF"/>
<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)" To="#C6FFFFFF"/>
<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[2].(GradientStop.Color)" To="#8CFFFFFF"/>
<ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[3].(GradientStop.Color)" To="#3FFFFFFF"/>
 </Storyboard>
 </vsm:VisualState>
 <vsm:VisualState x:Name="Disabled">
<Storyboard>
<DoubleAnimation Duration="0" Storyboard.TargetName="DisabledVisualElement" Storyboard.TargetProperty="Opacity" To=".55"/>
 </Storyboard>
 </vsm:VisualState>
</vsm:VisualStateGroup>
<vsm:VisualStateGroup x:Name="FocusStates">
 <vsm:VisualState x:Name="Focused">
<Storyboard>
<DoubleAnimation Duration="0" Storyboard.TargetName="FocusVisualElement" Storyboard.TargetProperty="Opacity" To="1"/>
 </Storyboard>
 </vsm:VisualState>
 <vsm:VisualState x:Name="Unfocused" />
</vsm:VisualStateGroup>
 </vsm:VisualStateManager.VisualStateGroups>
 <Border x:Name="Background" CornerRadius="3" Background="White" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}">
<Grid Background="{TemplateBinding Background}"Margin="1">
 <Border Opacity="0"x:Name="BackgroundAnimation" Background="#FF448DCA" />
 <Rectangle x:Name="BackgroundGradient" >
<Rectangle.Fill>
<LinearGradientBrush StartPoint=".7,0" EndPoint=".7,1">
 <GradientStop Color="#FFFFFFFF" Offset="0" />
 <GradientStop Color="#F9FFFFFF" Offset="0.375" />
 <GradientStop Color="#E5FFFFFF" Offset="0.625" />
 <GradientStop Color="#C6FFFFFF" Offset="1" />
</LinearGradientBrush>
</Rectangle.Fill>
 </Rectangle>
</Grid>
 </Border>
 <ContentPresenter
 x:Name="contentPresenter"
 Content="{TemplateBinding Content}"
 ContentTemplate="{TemplateBinding ContentTemplate}"
 VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
 HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
 Margin="{TemplateBinding Padding}"/>
 <Rectangle x:Name="DisabledVisualElement" RadiusX="3" RadiusY="3" Fill="#FFFFFFFF" Opacity="0" IsHitTestVisible="false" />
 <Rectangle x:Name="FocusVisualElement" RadiusX="2" RadiusY="2" Margin="1" Stroke="#FF6DBDD1" StrokeThickness="1" Opacity="0" IsHitTestVisible="false" />
</Grid>
</ControlTemplate>
 </Setter.Value>
</Setter>
 </Style>
[/code]

解决方法五:把Storyboard作为ViewModel的一个属性给View来绑定(糟糕的主意)

也许有人会想到这个主意:在ViewModel中加个Storyboard类型的属性,给view绑定传进去,这样在ViewModel的业务逻辑 中当业务操作好了保存Save成功了就可以直接调用自己的Storyboard.begin(),岂不爽哉?我想说这是个糟糕的主意,为什么?不要把业务 逻辑无关的纯UI的元素混到viewModel里面,难道要抽象依赖于具体?

解决方法六:用VisualStateManager,在ViewModel用事件通知View(仅供参考)

用VisualStateManager的方法(Event同方法一的事件),在视图收到事件通知以后,调用StateManager启动动画而已。在xaml.cs中:

[code]
VisualStateManager.GoToState(this, "YourState1", true);
 [/code]在Xaml中把动画不要定义在Resource中,而是定义为几个VisualState:
[code] xmlns:ic="clr-namespace:Microsoft.Expression.Interactivity.Core;assembly=Microsoft.Expression.Interactions"
 
<Grid x:Name="LayoutRoot">
<VisualStateManager.CustomVisualStateManager>
 <ic:ExtendedVisualStateManager/>
</VisualStateManager.CustomVisualStateManager>
<VisualStateManager.VisualStateGroups>
 <VisualStateGroup x:Name="EditViewGroup" ic:ExtendedVisualStateManager.UseFluidLayout="True">
<VisualStateGroup.Transitions>
 <VisualTransition GeneratedDuration="00:00:00.2500000"/>
</VisualStateGroup.Transitions>
<VisualState x:Name="YourState1">
 <Storyboard>
<ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.0010000" Storyboard.TargetName="GridFlyout" Storyboard.TargetProperty="(FrameworkElement.Margin)">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
 <DiscreteObjectKeyFrame.Value>
<Thickness>0,0,0,-101</Thickness>
 </DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="YourState2">
 <Storyboard>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.0010000" Storyboard.TargetName="GridFlyout" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)">
<EasingDoubleKeyFrame KeyTime="00:00:00" Value="3"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.0010000" Storyboard.TargetName="GridFlyout" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.Y)">
<EasingDoubleKeyFrame KeyTime="00:00:00" Value="-101"/>
</DoubleAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.0010000" Storyboard.TargetName="GridFlyout" Storyboard.TargetProperty="(FrameworkElement.Margin)">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
 <DiscreteObjectKeyFrame.Value>
<Thickness>-4,0,0,-101</Thickness>
 </DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
 </VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
[/code]

总结

以上几种方法个人觉得第二种最好,第三种次之,第四种也不错但是比较费时间。我们遇到问题不仅仅是思考问题,解决问题,还要发散思维想想多重解决方案并选择最优最简单的方案;如果当初是赶时间,那后续就需要重构来寻求最优解决方案。这种重构是有意义的。就像我在前一篇中如何在Silverlight页面间传递复杂对象,也给出了5种解决方法,选择最优的一种,好的攻城师应当多钻研,多分享,多接受批评和自我批评,这样才能进步的快一些。

继续阅读 – Silverlight入门MVVM系列

[Silverlight入门系列]使用MVVM模式(1):MVVM核心概念
[Silverlight入门系列]使用MVVM模式(2):集合Model /ObservableCollection/ICollectionView
[Silverlight入门系列]使用MVVM模式(3):Model的INotifyPropertyChanged接口实现
[Silverlight入门系列]使用MVVM模式(4):Prism的NotificationObject自动实现INotifyPropertyChanged接口
[Silverlight入门系列]使用MVVM模式(5):异步Validation数据验证和INotifyDataErrorInfo接口
[Silverlight入门系列]使用MVVM模式(6):使用Behavior
[Silverlight入门系列]使用MVVM模式(7):ViewModel的INotifyPropertyChanged接口实现
[Silverlight入门系列]使用MVVM模式(8):ViewModel进一步分离出Services

继续阅读 - Silverlight入门系列

[Silverlight入门系列]用扩展方法解决Prism导航Navigation在页面间传递复杂对象的问题
[Silverlight入门系列]Prism中TreeView真正实现MVVM模式和Expanded发生时异步动态加载子节点(WCFRiaService)
[Silverlight入门系列]如何单元测试RequiresAuthentication的DomainService
[Silverlight入门系列]反射获取所有CustomAttribute的值和方法
[Silverlight入门系列]Prism如何减小模块Module的XAP文件大小提高性能
[Silverlight入门系列]用CustomAttributes简化Command的CanExecute权限检查逻辑
[Silverlight入门系列]Prism的ViewModel和Service实现IDisposable接口Unsubscribe事件订阅
[Silverlight入门系列]扩展WebContext.Current(Authentication,Roles,Profiles)
[Silverlight入门系列]用Trigger实现事件发生时控件属性改变
[Silverlight入门系列]ControlTemplate和DataTemplate的区别
[Silverlight入门系列]EntityList<T>/DomainCollectionView<T>/DomainCollectionViewLoader<T>
[Silverlight入门系列]对DomainDataSource控件的羡慕嫉妒恨
[Silverlight入门系列]Image图片资源Resource的BuildAction如何设置
[Silverlight入门系列]Prism的事件EventAggregator如何单元测试UnitTesting
[Silverlight入门系列]Prism+MEF+单元测试UnitTest
[Silverlight入门系列]如何自动隐藏分页控件DataPager
[Silverlight入门系列]如何自动复制*.xap到ClientBin目录
[Silverlight入门系列]Silverlight性能最佳实践
[Silverlight入门系列]用MVVM方式让DataPager分页控件实现服务器端分页
[Silverlight入门系列]DomainService/ObjectContext/EF/动态切换数据库
[Silverlight入门系列]使用WCF RIA Services Class Library类库(进阶)
[Silverlight入门系列]使用WCF RIA Services Class Library类库(最新)
[Silverlight入门系列]Silverlight应用程序的安全性考虑Security
[Silverlight入门系列]Validation:INotifyPropertyChanged和INotifyDataErrorInfo放在Model实现好还是ViewModel实现好?
[Silverlight入门系列]Prism用到的设计模式
[Silverlight入门系列]App.xaml用途是什么?怎么传递参数和使用?
[Silverlight入门系列]动态跨域调用WCF Ria Service(动态添加引用/设置服务地址)
[Silverlight入门系列]WCF Ria Service跨域调用错误: The client and service bindings may be mismatched.
[Silverlight入门系列]WCF Ria Service跨域调用错误: The remote server returned an error: NotFound.
[Silverlight入门系列]WCF Ria Service错误调试tracing
[Silverlight入门系列]独立部署WCF Ria Service(单独/跨域)
[Silverlight入门系列]WCF Ria Service Authentication用户角色认证
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐