前面Silverlight入门系列文章中穿插讲了一些MVVM模式系列文章,MVVM模式貌似简单,其实要把界面逻辑抽象出来还是很不容易,像《TreeView真正实现MVVM模式和Expanded发生时异步动态加载子节点(WCFRiaService)》就不是这么简单,有的童鞋像feiyang还要实现Treeview的展开状态持久化和自动恢复,配合MVVM实现不容易。所以,MVVM的核心概念理解不难,在具体使用上则问题多多。今天要讲的话题就是一个MVVM使用上的具体问题: Silverlight中的Storyboard动画是否可以在ViewModel中来控制?

 

为什么想在ViewModel中控制Storyboard?

假设我的业务逻辑在ViewModel中,业务操作好了保存Save成功了就需要启动一个动画:Stobyboard.begin()。而这个动画在视图中,怎么去控制它?这个需求很普遍吧。确实很普遍,但实现就不那么简单了,不像下面这样的Storyboard启动那么简单:

 
>
>
>
> 
> 
/>
>
>
>
>
>

 

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

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

ViewModel代码:

class YourViewModel 
   2: { 
object sender, EventArgs e); 
event YourEventHandler YourEvent; 
void OnYourEvent(EventArgs e) { 
this, e); 
   7:     } 
   8:     
//当业务操作好了保存Save成功了触发这个事件
//OnYourEvent(new EventArgs(a));
  11: } 

在Xaml.cs写code behind代码:

new YourViewModel();
   2: vm.YourEvent += (s,e) => 
   3: {
as Storyboard;
   5:    story.Begin();
   6: }; 
this.DataContext = vm;

 

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

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

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


http://schemas.microsoft.com/expression/2010/interactions

Storyboard定义在Resource中:(此处Storyboard没实际意义仅为演示)

>
>
/>
/>
>
>
/>
/>
>
>

在View视图中绑定ViewModel属性为bool PopupSideShow,并用Trigger实现当绑定的值PopupSideShow变化的时候就触发启动动画:

>
>
>
/>
>
>
/>
>
>
>
/>
>
>

 

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

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

我们首先加一个StoryboardManager:

using System;
using System.Windows;
using System.Windows.Media.Animation;
using System.Collections.Generic;
   5:  
namespace TestVMAnimation
   7: {
class StoryboardManager
   9:     {
static DependencyProperty IDProperty =
typeof(StoryboardManager),
null, IdChanged));
  13:         
string, Storyboard>();
  15:  
object state);
  17:  
/// <summary>
/// IDs the changed.
/// </summary>
void IdChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) 
  24:         { 
as Storyboard; 
null)            
return; 
  28:             
string; 
if (Storyboards.ContainsKey(key))            
  31:                 Storyboards[key] = sb; 
else            
  33:                 Storyboards.Add(key, sb); 
  34:         }
  35:  
/// <summary>
/// Plays the storyboard.
/// </summary>
object state) 
  43:         { 
if (!Storyboards.ContainsKey(id)) 
  45:             { 
  46:                 callback(state); 
return; 
  48:             } 
  49:             Storyboard sb = Storyboards[id]; 
null;
  51:             EventHandler handlertemp = handler;
delegate { sb.Completed -= handlertemp; callback(state); }; 
  53:             sb.Completed += handler; 
  54:             sb.Begin(); 
  55:         }
  56:  
/// <summary>
/// Sets the ID.
/// </summary>
string id) 
  63:         { 
  64:             obj.SetValue(IDProperty, id); 
  65:         }
  66:  
/// <summary>
/// Gets the ID.
/// </summary>
/// <returns></returns>
string GetID(DependencyObject obj) 
  73:         { 
string; 
  75:         }
  76:     }
  77:     
  78: }

有了DependencyProperty就可以在Xaml里面绑定了,注意下面的StoryboardManager.ID:

>
 
>
/>
>
>

在ViewModel里面控制Storyboard很简单,下面这个例子是通过Command调用的,你当然也可以不通过Command直接调用Storyboard,像本文的例子,可以在ViewModel的业务逻辑里面当业务操作好了保存Save成功了启动Storyboard动画。

class YourViewModel
   2: {
private set;  }
   4:     
public YourViewModel()
   6:     {
new DelegateCommand(
   8:             () => {
null);
  10:         });
  11:     }
  12:  
  13:     
  14: }

 

解决方法四:不要在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该负责的,是高内聚的,是不想被重用的,是不能被测试的,那你就搁在code behind好了。绝对应该避免业务逻辑在里面哦。某位大神说过,“解决问题的最好办法是think different,说不定问题本身就不是个问题”。是的,你想在ViewModel里面控制Storyboard,这本身是不是有问题? 想想我们的动画一般在什么时候发生?真的是业务逻辑完成了发生吗?真的和业务逻辑相关吗?不!动画其实是和VisualElement的VisualState相关。也就是说,我们往往是在某个panel显示/隐藏/打开/关闭的时候有个淡入淡出、推箱子、跳跃、或者x/y/z/3D旋转的效果(不要告诉我是显示/隐藏panel本身,这个可以和ViewModel的属性绑定的,不是动画)。说白了就是一个transition,从一个VisualState到另一个VisualState而已。好了,想清楚了,问题就没有了。也就是说,你无须在ViewModel里面控制Storyboard,只要在View里面定义好VisualState就可以了,封装在控件行为中,把VisualState动画写在控件的模板中,有关怎么封装Silverlight控件这儿就不多说了,下回有空再说。具体做法可以参考MSDN这个页面,里面就有button的VisualState切换动画,比如MouseOver等:

>
/>
/>
/>
/>
>
>
>
/>
/>
/>
/>
>
>
>
>
>
>
>
>
>
/>
>
>
/>
/>
/>
/>
>
>
>
>
/>
/>
/>
/>
/>
/>
>
>
>
>
/>
>
>
>
>
>
>
/>
>
>
/>
>
>
>
>
/>
>
>
>
/>
/>
/>
/>
>
>
>
>
>
ContentPresenter
/>
/>
/>
>
>
>
>
>

 

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

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

 

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

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

   1:  
true);
   3:  

在Xaml中把动画不要定义在Resource中,而是定义为几个VisualState: 


   2:  
>
>
/>
>
>
>
>
/>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
/>
>
>
/>
>
>
>
>
>
>
>
>
>
>
>
>
>

 

总结

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

相关文章:

  • 2021-08-17
  • 2021-07-13
  • 2022-12-23
  • 2021-09-27
  • 2021-12-25
  • 2021-11-22
猜你喜欢
  • 2022-12-23
  • 2021-08-23
  • 2021-06-30
  • 2021-07-21
  • 2021-11-26
  • 2022-12-23
相关资源
相似解决方案