在我们使用WPF过程中,不可避免并且超级喜欢使用MVVM框架。
那么,使用MVVM的出发点是视觉与业务逻辑分离,即UI与数据分离
诸如下面的问题:
删除操作,假如需要先执行一部分数据的处理,然后删除界面列表中的子项,之后再执行其它数据的处理。请问此业务该放置于Xaml.cs文件,还是ViewModel中呢?
再如弹窗,提示框,设置列表的滚动等等。
此上一些操作,我们不应该把业务代码直接挪到cs文件中,因为删除操作绝大部分的代码都是数据的处理。所以,数据的部分放置在ViewModel中,一些交互放在cs文件中,就是很合理及有必要了。
单元测试,UI与交互的那部分mock模拟有点难度,也没必要去模拟。那么,我们是应该把数据与交互拆开,减少之间的耦合性。这样添加单元测试则更容易。
交互与数据分离 - 描述
首先MVVM,通过View与ViewModel的绑定,我们实现了UI与业务逻辑的分离。通俗一点,我们熟悉的Xaml与ViewModel文件中,代码之间的隔离。在此不详述~
而MVVM,不只是界面与逻辑,其实逻辑还可以拆分成交互与数据
即:Xaml 》Xaml.cs 》ViewModel
是的,按照上面的结构图,我们分成三部分:
- 界面 用于界面呈现 ---- 如页面/控件/样式/模板等其它资源的初始化,动画的触发等。
- 交互 用于与用户确认的交互或者界面复杂逻辑的处理 ---- 如弹窗/提示框/复杂动画的处理/设置列表的滚动等其它界面元素的视觉处理。
- 数据 只是数据的处理 ---- 增删改查导入导出保存等只针对数据的操作,界面状态属性的保存与触发更改(绑定)。
交互与数据分离是怎样的?比如删除:
1. 界面删除按钮,绑定ViewModel中的DeleteCommand,当我们点击删除时,触发DeleteCommand.Execute
2. ViewModel中,先执行数据状态的判断,然后执行交互通知ShowDeleteWaringAction,调用xaml.cs文件中的确认提示框
3. 在Xaml.cs中添加依赖属性ShowDeleteWaring,绑定ViewModel中的ShowDeleteWaringAction.Progress。在属性更改中,处理提示框确认逻辑。
4. ViewModel中,等待ShowDeleteWaring弹框完成后,继续执行下面的业务。
5. 还有类似上面步骤的删除动画。。。
交互与数据分离 - 实现
使用场景:在WPF框架下开发时,一种基于MVVM的UI分离方案
解决方案:在业务逻辑处理过程中,新建一个交互处理线程,通知界面完成交互处理,同时后台逻辑保持同步等待。界面完成交互处理后,回调并执行后续的业务逻辑。
实现方案:
- View中的依赖属性DependencyProperty,绑定ViewModel中属性“UIDelegateOperation”中的交互处理进度“UIDelegateProress”
- 每次在ViewModel执行业务逻辑需要调用交互处理时,由UIDelegateOperation创建一个新的交互进度“UIDelegateProress”,触发属性变更,并设置“UIDelegateOperation”同步等待。
- 当View中的属性变更事件执行完成后,回调并唤醒”UIDelegateOperation“,继续完成后面的业务逻辑。
1. 界面
在Xaml中添加附加属性,删除动画DeleteCoursewaresAnimation,删除确认框ShowDeleteWaring。并绑定ViewModel中对应的属性
12 6
界面ListBox,列表子项ListBoxItemr的DataTemplate模板中,删除按钮绑定ViewModel中的DeleteCommand
1
2. ViewModel
ViewModel调用UIDelegateOperation交互处理时,根据是否需要同步等待,调用不同的函数 Start(),StartAsync(),StartWithResult(),StartWithResultAsync();
删除业务中,除了数据处理,还有俩个交互(删除确认框,删除元素动画)。
通过在同步调用删除确认框/删除元素动画后,再继续往下执行业务。
属性和字段字义:
定义命令
自定义命令,可以详细之前写的博客:
1 private DelegateCommand_deleteCommand = null; 2 /// 3 /// 删除 4 /// 5 public DelegateCommandDeleteCommand 6 { 7 get 8 { 9 if (_deleteCommand == null)10 {11 _deleteCommand = new DelegateCommand (DeleteCourseware_OnExecute);12 13 }14 return _deleteCommand;15 } 16 }
提示框确认交互/删除动画交互
1 ///2 /// 弹出删除确认窗口 3 /// 4 public IUIDelegateOperation
, MessageResult> ShowDeleteWaring { get; set; } = new IUIDelegateOperation
, MessageResult>();5 6 /// 7 /// 删除动画 8 /// 9 public IUIDelegateOperation
> DeleteCoursewaresAnimation { get; set; } = new IUIDelegateOperation
>();
删除逻辑:
1 ///2 /// 删除 3 /// 4 /// 5 ///6 private async void DeleteCourseware_OnExecute(CoursewareListItem item) 7 { 8 await DeleteCoursewares(new List () { item }); 9 }10 private async Task DeleteCoursewares(List items)11 {12 if (items.Count == 0)13 {14 return;15 }16 17 //弹出删除确认窗口18 var messageResult = await ShowDeleteWaringShow.ExecuteWithResultAsync(items);19 if (messageResult == MessageResult.Positive)20 {21 //删除服务器数据 22 Response deleteResponse = await WebService.DeleteItemAsync(items);23 24 //删除失败25 if (!deleteResponse.Success)26 {27 Notification.ShowInfo(deleteResponse.Message);28 return;29 }30 //删除动画31 await DeleteCoursewaresAnimation.ExecuteAsync(items);32 33 //界面删除子项34 items.ForEach(item => ItemsSource.Remove(item));35 36 //退出编辑模式37 if (DocListState == EditStatus.Editing)38 {39 DocListState = EditStatus.Normal;40 }41 }42 }
3. Xaml.cs后台
- 添加依赖属性后,通过属性变更触发,来完成弹出提示框/删除动画等交互。
- 执行交互时,需要同步等待时,应将动画执行等转化为同步逻辑。
添加依赖属性 - 删除窗口
属性变更触发方法,应该是一个异步方法,里面的逻辑应该为同步执行。这样ViewModel中才能同步等待交互的完成,并执行之后的逻辑。
1 ///2 /// 删除窗口 3 /// 4 public static readonly DependencyProperty ShowDeleteWaringShowProperty = DependencyProperty.Register( 5 "ShowDeleteWaringShow", typeof(UIDelegateProgress
, MessageResult>), typeof(CloudListView), new PropertyMetadata(default(UIDelegateProgress
, MessageResult>), 6 (d, e) => ((UIDelegateProgress
, MessageResult>)e.NewValue)?.StartAsync(((CloudListView)d).ShowDeleteWaringShow))); 7 8 private async Task ShowDeleteWaringShow(List items) 9 {10 var cmd = await DeleteWaringShow(items);11 return cmd.Result;12 }13 14 public static void SetShowDeleteWaringShow(DependencyObject element, UIDelegateProgress
, MessageResult> value)15 {16 element.SetValue(ShowDeleteWaringShowProperty, value);17 }18 19 public static UIDelegateProgress
, MessageResult> GetShowDeleteWaringShow(DependencyObject element)20 {21 return (UIDelegateProgress
, MessageResult>)element.GetValue(ShowDeleteWaringShowProperty);22 }
添加依赖属性 - 删除动画
1 public static readonly DependencyProperty DeleteCoursewaresAnimationProperty = DependencyProperty.Register( 2 "DeleteCoursewaresAnimation", typeof(UIDelegateProgress
>), typeof(CloudListView), new PropertyMetadata(default(UIDelegateProgress
>), 3 (d, e) => ((UIDelegateProgress
>)e.NewValue)?.StartAsync(((CloudListView)d).ExecuteDeleteCoursewaresAnimation))); 4 5 private async Task ExecuteDeleteCoursewaresAnimation(List coursewares) 6 { 7 List storyboards = new List (); 8 foreach (var courseware in coursewares) 9 {10 var listBoxItem = DocumentsControl.ItemContainerGenerator.ContainerFromItem(courseware) as ListBoxItem;11 var border = listBoxItem?.VisualDescendant ();12 var storyboard = (Storyboard)border?.Resources["ItemRemovedStoryboard"];13 if (storyboard == null)14 {15 //如果找不到storyBoard,则中断动画的执行。因为删除多个Item,只执行一半的动画,界面会闪现俩次。16 return;17 }18 storyboards.Add(storyboard);19 }20 //删除界面课件21 await AsynchronousTransferHelper.ExecuteStoryboradAsync(storyboards);22 }23 24 public static void SetDeleteCoursewaresAnimation(DependencyObject element, UIDelegateProgress
> value)25 {26 element.SetValue(DeleteCoursewaresAnimationProperty, value);27 }28 29 public static UIDelegateProgress
> GetDeleteCoursewaresAnimation(DependencyObject element)30 {31 return (UIDelegateProgress
>)element.GetValue(DeleteCoursewaresAnimationProperty);32 }
动画的执行,怎么转为有同步等待呢?动画完成只有通过触发事件Completed才能确定。
如何将动画转化为同步,可参考之前写的博客:
1 ///2 /// 执行动画 3 /// 4 /// 5 ///6 public static async Task ExecuteStoryboradAsync([NotNull] Storyboard storyboard) 7 { 8 if (storyboard == null) throw new ArgumentNullException(nameof(storyboard)); 9 10 AutoResetEvent autoResetEvent = new AutoResetEvent(false);11 12 storyboard.Completed += OnStoryboardCompleted;13 storyboard.Begin();14 15 void OnStoryboardCompleted(object sender, EventArgs e)16 {17 storyboard.Completed -= OnStoryboardCompleted;18 autoResetEvent.Set();19 }20 21 await Task.Run(() => { autoResetEvent.WaitOne(); });22 }
4. 交互处理辅助类 UIDelegateOperation
在UIDelegateOperation内部,每次调用时,都会新建一个UIDelegateProgress(委托进度)。委托进度,是界面交互的处理~
UIDelegateOperation:
1 ///2 /// UI交互处理-提供可调用UI交互的操作 3 /// 4 public class UIDelegateOperation : BindableObject, IUIDelegateAction 5 { 6 private UIDelegateProgress _delegateProgress; 7 8 public UIDelegateProgress DelegateProgress 9 { 10 get => _delegateProgress; 11 private set 12 { 13 _delegateProgress = value; 14 OnPropertyChanged(); 15 } 16 } 17 18 ///19 /// 执行 20 /// 21 public void Execute() 22 { 23 var delegateProgress = new UIDelegateProgress(); 24 delegateProgress.ProgressCompleted += () => 25 { 26 _delegateProgress = null; 27 }; 28 DelegateProgress = delegateProgress; 29 } 30 31 ///32 /// 异步执行 33 /// 交互处理完成并回调 34 /// 35 public async Task ExecuteAsync() 36 { 37 AutoResetEvent autoResetEvent = new AutoResetEvent(false); 38 39 var delegateProgress = new UIDelegateProgress(); 40 delegateProgress.ProgressCompleted += () => 41 { 42 _delegateProgress = null; 43 44 autoResetEvent.Set(); 45 }; 46 DelegateProgress = delegateProgress; 47 await Task.Run(() => { autoResetEvent.WaitOne(); }); 48 } 49 } 50 51 ///52 /// UI交互处理-提供可同步调用UI交互的操作 53 /// 54 ///输入/输出类型 55 public class UIDelegateAction: BindableObject, IUIDelegateAction 56 { 57 private UIDelegateProgress _delegateProgress; 58 59 public UIDelegateProgress DelegateProgress 60 { 61 get => _delegateProgress; 62 private set 63 { 64 _delegateProgress = value; 65 OnPropertyChanged(); 66 } 67 } 68 /// 69 /// 执行 70 /// 71 public void Execute(T parameter) 72 { 73 var delegateProgress = new UIDelegateProgress(parameter); 74 delegateProgress.ProgressCompleted += () => 75 { 76 _delegateProgress = null; 77 }; 78 DelegateProgress = delegateProgress; 79 } 80 /// 81 /// 异步执行 82 /// 交互处理完成并回调 83 /// 84 public async Task ExecuteAsync(T parameter) 85 { 86 AutoResetEvent autoResetEvent = new AutoResetEvent(false); 87 88 var delegateProgress = new UIDelegateProgress(parameter); 89 delegateProgress.ProgressCompleted += () => 90 { 91 _delegateProgress = null; 92 93 autoResetEvent.Set(); 94 }; 95 DelegateProgress = delegateProgress; 96 97 await Task.Run(() => { autoResetEvent.WaitOne(); }); 98 } 99 100 /// 101 /// 异步执行并返回结果102 /// 103 public async TaskExecuteWithResultAsync()104 {105 AutoResetEvent autoResetEvent = new AutoResetEvent(false);106 107 var delegateProgress = new UIDelegateProgress ();108 delegateProgress.ProgressCompleted += () =>109 {110 _delegateProgress = null;111 112 autoResetEvent.Set();113 };114 DelegateProgress = delegateProgress;115 116 await Task.Run(() => { autoResetEvent.WaitOne(); });117 118 return delegateProgress.Result;119 }120 }121 122 /// 123 /// UI交互处理-提供可同步调用UI交互的操作124 /// 125 ///输入类型 126 ///输出类型 127 public class UIDelegateAction: BindableObject, IUIDelegateAction 128 {129 private UIDelegateProgress _delegateProgress;130 131 public UIDelegateProgress DelegateProgress132 {133 get => _delegateProgress;134 private set135 {136 _delegateProgress = value;137 OnPropertyChanged();138 }139 }140 /// 141 /// 执行142 /// 143 public void Execute(TInput parameter)144 {145 var delegateProgress = new UIDelegateProgress(parameter);146 delegateProgress.ProgressCompleted += () =>147 {148 _delegateProgress = null;149 };150 DelegateProgress = delegateProgress;151 }152 153 /// 154 /// 执行并返回结果155 /// 156 public TOut ExecuteWithResult(TInput parameter)157 {158 var delegateProgress = new UIDelegateProgress(parameter);159 delegateProgress.ProgressCompleted += () =>160 {161 _delegateProgress = null;162 };163 DelegateProgress = delegateProgress;164 return delegateProgress.Result;165 }166 167 /// 168 /// 异步执行并返回结果169 /// 170 public async TaskExecuteWithResultAsync(TInput parameter)171 {172 var delegateProgress = new UIDelegateProgress (parameter);173 await SetDelegateProgress(delegateProgress);174 return delegateProgress.Result;175 }176 private async Task SetDelegateProgress(UIDelegateProgress delegateProgress)177 {178 AutoResetEvent autoResetEvent = new AutoResetEvent(false);179 180 delegateProgress.ProgressCompleted += () =>181 {182 _delegateProgress = null;183 autoResetEvent.Set();184 };185 DelegateProgress = delegateProgress;186 await Task.Run(() => { autoResetEvent.WaitOne(); });187 }188 }189 190 /// 191 /// UI交互处理接口192 /// 193 public interface IUIDelegateAction194 {195 196 UIDelegateProgress DelegateProgress { get; }197 198 ///199 /// 执行200 /// 201 void Execute();202 203 ///204 /// 异步执行205 /// 206 Task ExecuteAsync();207 }208 209 ///210 /// UI交互处理接口211 /// 212 ///输入/输出类型 213 public interface IUIDelegateAction214 {215 UIDelegateProgress DelegateProgress { get; }216 217 /// 218 /// 执行219 /// 220 void Execute(T parameter);221 222 ///223 /// 异步执行224 /// 225 Task ExecuteAsync(T parameter);226 227 ///228 /// 异步执行并返回结果229 /// 230 TaskExecuteWithResultAsync();231 }232 233 /// 234 /// UI交互处理接口235 /// 236 ///输入类型 237 ///输出类型 238 public interface IUIDelegateAction239 {240 UIDelegateProgress DelegateProgress { get; }241 242 /// 243 /// 执行244 /// 245 void Execute(TInput parameter);246 247 ///248 /// 执行并返回结果249 /// 250 TOut ExecuteWithResult(TInput parameter);251 252 ///253 /// 异步执行并返回结果254 /// 255 TaskExecuteWithResultAsync(TInput parameter);256 }
UIDelegateProgress:
1 ///2 /// 委托进度 3 /// 4 public class UIDelegateProgress 5 { 6 public event Action ProgressCompleted; 7 8 ///9 /// UI委托处理 10 /// 11 /// 12 public async void StartAsync(FuncuiTask) 13 { 14 try 15 { 16 await uiTask.Invoke(); 17 } 18 catch (InvalidOperationException e) 19 { 20 Log.Error("UI交互处理,产生异常!", e); 21 } 22 finally 23 { 24 ProgressCompleted?.Invoke(); 25 } 26 } 27 28 /// 29 /// UI委托处理 30 /// 31 /// 32 public void Start(Action uiTask) 33 { 34 try 35 { 36 uiTask.Invoke(); 37 } 38 catch (InvalidOperationException e) 39 { 40 Log.Error("UI交互处理,产生异常!", e); 41 } 42 finally 43 { 44 ProgressCompleted?.Invoke(); 45 } 46 } 47 } 48 49 ///50 /// 委托进度 51 /// 52 public class UIDelegateProgress53 { 54 public event Action ProgressCompleted; 55 56 /// 57 /// 输入参数 58 /// 59 public T Parameter { get; set; } 60 61 ///62 /// 输出参数 63 /// 64 public T Result { get; set; } 65 66 public UIDelegateProgress() 67 { 68 69 } 70 public UIDelegateProgress(T parameter) 71 { 72 Parameter = parameter; 73 } 74 75 ///76 /// UI委托处理 77 /// 78 /// 79 public void Start(ActionuiTask) 80 { 81 try 82 { 83 uiTask.Invoke(Parameter); 84 } 85 catch (InvalidOperationException e) 86 { 87 Log.Error("UI交互处理,产生异常!", e); 88 } 89 finally 90 { 91 ProgressCompleted?.Invoke(); 92 } 93 } 94 95 /// 96 /// UI委托处理 97 /// 98 /// 99 public async void StartAsync(FuncuiTask)100 {101 try102 {103 await uiTask.Invoke(Parameter);104 }105 catch (InvalidOperationException e)106 {107 Log.Error("UI交互处理,产生异常!", e);108 }109 finally110 {111 ProgressCompleted?.Invoke();112 }113 }114 115 /// 116 /// UI委托处理117 /// 118 /// 119 public void Start(FuncuiTask)120 {121 try122 {123 Result = uiTask.Invoke();124 }125 catch (InvalidOperationException e)126 {127 Log.Error("UI交互处理,产生异常!", e);128 }129 finally130 {131 ProgressCompleted?.Invoke();132 }133 }134 135 /// 136 /// UI委托处理137 /// 138 /// 139 public async void StartAsync(Func> uiTask)140 {141 try142 {143 Result = await uiTask.Invoke();144 }145 catch (InvalidOperationException e)146 {147 Log.Error("UI交互处理,产生异常!", e);148 }149 finally150 {151 ProgressCompleted?.Invoke();152 }153 }154 }155 156 /// 157 /// 委托进度158 /// 159 public class UIDelegateProgress160 {161 public event Action ProgressCompleted;162 163 /// 164 /// 输入参数165 /// 166 public TInput Parameter { get; set; }167 168 ///169 /// 输出参数170 /// 171 public TOut Result { get; set; }172 173 public UIDelegateProgress(TInput parameter)174 {175 Parameter = parameter;176 }177 178 ///179 /// UI委托处理180 /// 181 /// 182 public async void StartAsync(Func> uiTask)183 {184 try185 {186 Result = await uiTask.Invoke(Parameter);187 }188 catch (InvalidOperationException e)189 {190 Log.Error("UI交互处理,产生异常!", e);191 }192 finally193 {194 ProgressCompleted?.Invoke();195 }196 }197 198 /// 199 /// UI委托处理200 /// 201 /// 202 public void Start(FuncuiTask)203 {204 try205 {206 uiTask.Invoke();207 }208 catch (InvalidOperationException e)209 {210 Log.Error("UI交互处理,产生异常!", e);211 }212 finally213 {214 ProgressCompleted?.Invoke();215 }216 }217 218 /// 219 /// UI委托处理220 /// 221 /// 222 public void Start(FuncuiTask)223 {224 try225 {226 Result = uiTask.Invoke(Parameter);227 }228 catch (InvalidOperationException e)229 {230 Log.Error("UI交互处理,产生异常!", e);231 }232 finally233 {234 ProgressCompleted?.Invoke();235 }236 }237 }
关键字:UI分离,交互与数据分离,动画同步,单元测试