在我們使用WPF過程中,不可避免並且超級喜歡使用MVVM框架。 那麼,使用MVVM的出發點是視覺與業務邏輯分離,即UI與數據分離 諸如下麵的問題: 刪除操作,假如需要先執行一部分數據的處理,然後刪除界面列表中的子項,之後再執行其它數據的處理。請問此業務該放置於Xaml.cs文件,還是ViewMode ...
在我們使用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中對應的屬性
1 <UserControl.Style> 2 <Style TargetType="editing:CloudListView"> 3 <Setter Property="DeleteCoursewaresAnimation" Value="{Binding DeleteCoursewaresAnimation.DelegateProgress}" /> 4 <Setter Property="ShowDeleteWaringShow" Value="{Binding ShowDeleteWaring.DelegateProgress}" /> 5 </Style> 6 </UserControl.Style>
界面ListBox,列表子項ListBoxItemr的DataTemplate模板中,刪除按鈕綁定ViewModel中的DeleteCommand
1 <Button x:Name="DeleteButton" 2 Command="{Binding ElementName=TheCloudDocsList,Path=DataContext.DeleteCommand}" 3 CommandParameter="{Binding RelativeSource={RelativeSource TemplatedParent},Path=DataContext }" 4 Content="刪除" Style="{StaticResource Style.Button}" />
2. ViewModel
ViewModel調用UIDelegateOperation交互處理時,根據是否需要同步等待,調用不同的函數 Start(),StartAsync(),StartWithResult(),StartWithResultAsync();
刪除業務中,除了數據處理,還有倆個交互(刪除確認框,刪除元素動畫)。
通過在同步調用刪除確認框/刪除元素動畫後,再繼續往下執行業務。
屬性和欄位字義:
定義命令
自定義命令,可以詳細之前寫的博客:自定義Command
1 private DelegateCommand<CoursewareListItem> _deleteCommand = null; 2 /// <summary> 3 /// 刪除 4 /// </summary> 5 public DelegateCommand<CoursewareListItem> DeleteCommand 6 { 7 get 8 { 9 if (_deleteCommand == null) 10 { 11 _deleteCommand = new DelegateCommand<CoursewareListItem>(DeleteCourseware_OnExecute); 12 13 } 14 return _deleteCommand; 15 } 16 }
提示框確認交互/刪除動畫交互
1 /// <summary> 2 /// 彈出刪除確認視窗 3 /// </summary> 4 public IUIDelegateOperation<List<CoursewareListItem>, MessageResult> ShowDeleteWaring { get; set; } = new IUIDelegateOperation<List<CoursewareListItem>, MessageResult>(); 5 6 /// <summary> 7 /// 刪除動畫 8 /// </summary> 9 public IUIDelegateOperation<List<CoursewareListItem>> DeleteCoursewaresAnimation { get; set; } = new IUIDelegateOperation<List<CoursewareListItem>>();
刪除邏輯:
1 /// <summary> 2 /// 刪除 3 /// </summary> 4 /// <param name="item"></param> 5 /// <returns></returns> 6 private async void DeleteCourseware_OnExecute(CoursewareListItem item) 7 { 8 await DeleteCoursewares(new List<CoursewareListItem>() { item }); 9 } 10 private async Task DeleteCoursewares(List<CoursewareListItem> 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 /// <summary> 2 /// 刪除視窗 3 /// </summary> 4 public static readonly DependencyProperty ShowDeleteWaringShowProperty = DependencyProperty.Register( 5 "ShowDeleteWaringShow", typeof(UIDelegateProgress<List<CoursewareListItem>, MessageResult>), typeof(CloudListView), new PropertyMetadata(default(UIDelegateProgress<List<CoursewareListItem>, MessageResult>), 6 (d, e) => ((UIDelegateProgress<List<CoursewareListItem>, MessageResult>)e.NewValue)?.StartAsync(((CloudListView)d).ShowDeleteWaringShow))); 7 8 private async Task<MessageResult> ShowDeleteWaringShow(List<CoursewareListItem> items) 9 { 10 var cmd = await DeleteWaringShow(items); 11 return cmd.Result; 12 } 13 14 public static void SetShowDeleteWaringShow(DependencyObject element, UIDelegateProgress<List<CoursewareListItem>, MessageResult> value) 15 { 16 element.SetValue(ShowDeleteWaringShowProperty, value); 17 } 18 19 public static UIDelegateProgress<List<CoursewareListItem>, MessageResult> GetShowDeleteWaringShow(DependencyObject element) 20 { 21 return (UIDelegateProgress<List<CoursewareListItem>, MessageResult>)element.GetValue(ShowDeleteWaringShowProperty); 22 }
添加依賴屬性 - 刪除動畫
1 public static readonly DependencyProperty DeleteCoursewaresAnimationProperty = DependencyProperty.Register( 2 "DeleteCoursewaresAnimation", typeof(UIDelegateProgress<List<CoursewareListItem>>), typeof(CloudListView), new PropertyMetadata(default(UIDelegateProgress<List<CoursewareListItem>>), 3 (d, e) => ((UIDelegateProgress<List<CoursewareListItem>>)e.NewValue)?.StartAsync(((CloudListView)d).ExecuteDeleteCoursewaresAnimation))); 4 5 private async Task ExecuteDeleteCoursewaresAnimation(List<CoursewareListItem> coursewares) 6 { 7 List<Storyboard> storyboards = new List<Storyboard>(); 8 foreach (var courseware in coursewares) 9 { 10 var listBoxItem = DocumentsControl.ItemContainerGenerator.ContainerFromItem(courseware) as ListBoxItem; 11 var border = listBoxItem?.VisualDescendant<Border>(); 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<List<CoursewareListItem>> value) 25 { 26 element.SetValue(DeleteCoursewaresAnimationProperty, value); 27 } 28 29 public static UIDelegateProgress<List<CoursewareListItem>> GetDeleteCoursewaresAnimation(DependencyObject element) 30 { 31 return (UIDelegateProgress<List<CoursewareListItem>>)element.GetValue(DeleteCoursewaresAnimationProperty); 32 }
動畫的執行,怎麼轉為有同步等待呢?動畫完成只有通過觸發事件Completed才能確定。
如何將動畫轉化為同步,可參考之前寫的博客:C# 非同步轉同步
1 /// <summary> 2 /// 執行動畫 3 /// </summary> 4 /// <param name="storyboard"></param> 5 /// <returns></returns> 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 /// <summary> 2 /// UI交互處理-提供可調用UI交互的操作 3 /// </summary> 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 /// <summary> 19 /// 執行 20 /// </summary> 21 public void Execute() 22 { 23 var delegateProgress = new UIDelegateProgress(); 24 delegateProgress.ProgressCompleted += () => 25 { 26 _delegateProgress = null; 27 }; 28 DelegateProgress = delegateProgress; 29 } 30 31 /// <summary> 32 /// 非同步執行 33 /// 交互處理完成並回調 34 /// </summary> 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 /// <summary> 52 /// UI交互處理-提供可同步調用UI交互的操作 53 /// </summary> 54 /// <typeparam name="T">輸入/輸出類型</typeparam> 55 public class UIDelegateAction<T> : BindableObject, IUIDelegateAction<T> 56 { 57 private UIDelegateProgress<T> _delegateProgress; 58 59 public UIDelegateProgress<T> DelegateProgress 60 { 61 get => _delegateProgress; 62 private set 63 { 64 _delegateProgress = value; 65 OnPropertyChanged(); 66 } 67 } 68 /// <summary> 69 /// 執行 70 /// </summary> 71 public void Execute(T parameter) 72 { 73 var delegateProgress = new UIDelegateProgress<T>(parameter); 74 delegateProgress.ProgressCompleted += () => 75 { 76 _delegateProgress = null; 77 }; 78 DelegateProgress = delegateProgress; 79 } 80 /// <summary> 81 /// 非同步執行 82 /// 交互處理完成並回調 83 /// </summary> 84 public async Task ExecuteAsync(T parameter) 85 { 86 AutoResetEvent autoResetEvent = new AutoResetEvent(false); 87 88 var delegateProgress = new UIDelegateProgress<T>(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 /// <summary> 101 /// 非同步執行並返回結果 102 /// </summary> 103 public async Task<T> ExecuteWithResultAsync() 104 { 105 AutoResetEvent autoResetEvent = new AutoResetEvent(false); 106 107 var delegateProgress = new UIDelegateProgress<T>(); 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 /// <summary> 123 /// UI交互處理-提供可同步調用UI交互的操作 124 /// </summary> 125 /// <typeparam name="TInput">輸入類型</typeparam> 126 /// <typeparam name="TOut">輸出類型</typeparam> 127 public class UIDelegateAction<TInput, TOut> : BindableObject, IUIDelegateAction<TInput, TOut> 128 { 129 private UIDelegateProgress<TInput, TOut> _delegateProgress; 130 131 public UIDelegateProgress<TInput, TOut> DelegateProgress 132 { 133 get => _delegateProgress; 134 private set 135 { 136 _delegateProgress = value; 137 OnPropertyChanged(); 138 } 139 } 140 /// <summary> 141 /// 執行 142 /// </summary> 143 public void Execute(TInput parameter) 144 { 145 var delegateProgress = new UIDelegateProgress<TInput, TOut>(parameter); 146 delegateProgress.ProgressCompleted += () => 147 { 148 _delegateProgress = null; 149 }; 150 DelegateProgress = delegateProgress; 151 } 152 153 /// <summary> 154 /// 執行並返回結果 155 /// </summary> 156 public TOut ExecuteWithResult(TInput parameter) 157 { 158 var delegateProgress = new UIDelegateProgress<TInput, TOut>(parameter); 159 delegateProgress.ProgressCompleted += () => 160 { 161 _delegateProgress = null; 162 }; 163 DelegateProgress = delegateProgress; 164 return delegateProgress.Result; 165 } 166 167 /// <summary> 168 /// 非同步執行並返回結果 169 /// </summary> 170 public async Task<TOut> ExecuteWithResultAsync(TInput parameter) 171 { 172 var delegateProgress = new UIDelegateProgress<TInput, TOut>(parameter); 173 await SetDelegateProgress(delegateProgress); 174 return delegateProgress.Result; 175 } 176 private async Task SetDelegateProgress(UIDelegateProgress<TInput, TOut> 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 /// <summary> 191 /// UI交互處理介面 192 /// </summary> 193 public interface IUIDelegateAction 194 { 195 196 UIDelegateProgress DelegateProgress { get; } 197 198 /// <summary> 199 /// 執行 200 /// </summary> 201 void Execute(); 202 203 /// <summary> 204 /// 非同步執行 205 /// </summary> 206 Task ExecuteAsync(); 207 } 208 209 /// <summary> 210 /// UI交互處理介面 211 /// </summary> 212 /// <typeparam name="T">輸入/輸出類型</typeparam> 213 public interface IUIDelegateAction<T> 214 { 215 UIDelegateProgress<T> DelegateProgress { get; } 216 217 /// <summary> 218 /// 執行 219 /// </summary> 220 void Execute(T parameter); 221 222 /// <summary> 223 /// 非同步執行 224 /// </summary> 225 Task ExecuteAsync(T parameter); 226 227 /// <summary> 228 /// 非同步執行並返回結果 229 /// </summary> 230 Task<T> ExecuteWithResultAsync(); 231 } 232 233 /// <summary> 234 /// UI交互處理介面 235 /// </summary> 236 /// <typeparam name="TInput">輸入類型</typeparam> 237 /// <typeparam name="TOut">輸出類型</typeparam> 238 public interface IUIDelegateAction<TInput, TOut> 239 { 240 UIDelegateProgress<TInput, TOut> DelegateProgress { get; } 241 242 /// <summary> 243 /// 執行 244 /// </summary> 245 void Execute(TInput parameter); 246 247 /// <summary> 248 /// 執行並返回結果 249 /// </summary> 250 TOut ExecuteWithResult(TInput parameter); 251 252 /// <summary> 253 /// 非同步執行並返回結果 254 /// </summary> 255 Task<TOut> ExecuteWithResultAsync(TInput parameter); 256 }View Code
UIDelegateProgress:
1 /// <summary> 2 /// 委托進度 3 /// </summary> 4 public class UIDelegateProgress 5 { 6 public event Action ProgressCompleted; 7 8 /// <summary> 9 /// UI委托處理 10 /// </summary> 11 /// <param name="uiTask"></param> 12 public async void StartAsync(Func<Task> uiTask) 13 { 14 try 15 { 16 await uiTask.Invoke(); 17 } 18 catch (InvalidOperationException e) 19 { 20 Log.Error(