在我們窗口新增、編輯狀態(tài)下的時候,我們往往會根據(jù)是否修改過的痕跡-也就是臟數(shù)據(jù)狀態(tài)進行跟蹤,如果用戶發(fā)生了數(shù)據(jù)修改,我們在用戶退出窗口的時候,提供用戶是否丟棄修改還是繼續(xù)編輯,這樣在一些重要錄入時的時候,可以避免用戶不小心關(guān)掉窗口,導(dǎo)致窗口的數(shù)據(jù)要重新錄入的尷尬場景。本篇隨筆介紹基于WPF開發(fā)中,窗口控件臟數(shù)據(jù)狀態(tài)IsDirty的跟蹤處理操作。
?1、WPF的Page頁面、Window窗口對象和視圖模型
MVVM是Model-View-ViewModel的簡寫。類似于目前比較流行的MVC、MVP設(shè)計模式,主要目的是為了分離視圖(View)和模型(Model)的耦合。
對于MVVM應(yīng)用中,MVVM其中包括Model、View、ViewModel三者內(nèi)容。其中Page或者Window對象,都是屬于視圖View的概念。由于目前我們程序框架大多數(shù)情況下采用IOC的控制反轉(zhuǎn)方式來調(diào)用,因此對象和接口的注入是程序開始的重要工作。
.net 中 負責(zé)依賴注入和控制反轉(zhuǎn)的核心組件有兩個:IServiceCollection和IServiceProvider。其中,IServiceCollection負責(zé)注冊,IServiceProvider負責(zé)提供實例。在注冊接口和類時,IServiceCollection
提供了三種注冊方法,如下所示:
1、services.AddTransient<IDictDataService, DictDataService>(); // 瞬時生命周期 2、services.AddScoped<IDictDataService, DictDataService>(); // 域生命周期 3、services.AddSingleton<IDictDataService, DictDataService>(); // 全局單例生命周期
如果使用AddTransient
方法注冊,IServiceProvider
每次都會通過GetService
方法創(chuàng)建一個新的實例;
如果使用AddScoped
方法注冊, 在同一個域(Scope
)內(nèi),IServiceProvider
每次都會通過GetService
方法調(diào)用同一個實例,可以理解為在局部實現(xiàn)了單例模式;
如果使用AddSingleton
方法注冊, 在整個應(yīng)用程序生命周期內(nèi),IServiceProvider
只會創(chuàng)建一個實例。
了解了這幾個不同的注入方式,有助于我們了解WPF的整個注入對象的生命周期,對于頁面來說,由于采用了導(dǎo)航的方式,我們在注入的時候,采用單例的方式,對于彈出的編輯、新增、導(dǎo)入、批量處理的這種常規(guī)視圖,我們采用用完就丟棄的AddTransient方式,而視圖模型為了方便,我們也采用單件的方式構(gòu)建即可。
我們在WPF程序入口的程序代碼App.xaml.cs中注入相關(guān)的對象信息。
登錄窗口和主窗口,采用單件注入方式,如下代碼所示。
// 程序?qū)Ш街鞔绑w及視圖模型 services.AddSingleton<INavigationWindow, MainWindow>(); services.AddSingleton<ViewModels.MainWindowViewModel>(); //登錄窗口 services.AddSingleton<LoginView, LoginView>();
而對于我們程序的視圖或者視圖模型對象來做,我們不可能一一按名字插入,應(yīng)該通過一種動態(tài)的方式來批量處理,也就是根據(jù)各自的接口類型/繼承基類來處理即可。
#region 加入視圖頁面和視圖模型 //使用動態(tài)方式加入 var types = System.Reflection.Assembly.GetExecutingAssembly().DefinedTypes.Select(type => type.AsType()); //視圖頁面對象 typeof(Page),使用單件模式,每次請求都是一樣的頁面 var viewPageBaseType = typeof(Page); var pageClasses = types.Where(x => x != viewPageBaseType && viewPageBaseType.IsAssignableFrom(x)) .Where(x => x.IsClass && !x.IsAbstract).ToList(); foreach (var page in pageClasses) { services.AddSingleton(page); } //視圖模型對象 typeof(ObservableObject) var viewModelBaseType = typeof(ObservableObject); var viewModels = types.Where(x => x != viewModelBaseType && viewModelBaseType.IsAssignableFrom(x)) .Where(x => x.IsClass && !x.IsAbstract).ToList(); foreach (var view in viewModels) { services.AddSingleton(view); } //窗口對象,使用 Transient 模式,每次請求都是一個新的窗體 var windowBaseType = typeof(Window); var windowClasses = types.Where(x => x != windowBaseType && windowBaseType.IsAssignableFrom(x)) .Where(x => x.IsClass && !x.IsAbstract).ToList(); foreach (var window in windowClasses) { services.AddTransient(window); } #endregion
以上就是普通列表頁面Page類型、視圖模型ViewModel、彈出式窗口的幾種注入的方式,通過接口判斷和基類判斷的方式,自動注入相關(guān)的對象。
2、對窗口控件的修改進行跟蹤
了解了上面幾種對象的注入方式,我們來進一步了解彈出式窗口對象Window的控件的修改狀態(tài)跟蹤。
由于WPF不能像Winform那種,通過父對象的Controls集合就可以遍歷出來所有的對象,然后進行一一判斷,而WPF對象沒有這個屬性,因此也就無法直接的對控件的修改狀態(tài)進行跟蹤。
那是不是沒有辦法對窗口下面的控件進行一一判斷了呢?肯定不是,辦法還是有的,就是通過內(nèi)置輔助類LogicalTreeHelper,或者VisualTreeHelper的方式,由于前者是所有窗口或者頁面的邏輯控件都會跟蹤到,后者VisualTreeHelper只是對可視化的控件進行跟蹤,因此我們這里選擇LogicalTreeHelper來對控件進行遍歷處理。
實現(xiàn)的效果,就是對應(yīng)窗口編輯內(nèi)容發(fā)生變化,如果用戶退出,提示用戶即可,如下界面效果所示。
?由于窗口元素都是繼承自Visual這個wpf的基類,而這個基類又是繼承于DependencyObject,如下代碼所示。
public abstract class Visual : DependencyObject
而輔助類,可以通過GetChildren的方法獲取對應(yīng)的控件列表,接口如下所示。
IEnumerable GetChildren(DependencyObject current)
稍微封裝下對控件的遞歸遍歷處理,代碼如下所示。
/// <summary> /// 使用輔助類對窗口控件進行遍歷處理 /// </summary> /// <typeparam name="T">控件類型</typeparam> /// <param name="depObj">父對象</param> /// <returns></returns> public static IEnumerable<T> FindLogicalChildren<T>(DependencyObject depObj) where T : DependencyObject { if (depObj != null) { foreach (object rawChild in LogicalTreeHelper.GetChildren(depObj)) { if (rawChild is DependencyObject) { DependencyObject child = (DependencyObject)rawChild; if (child is T) { yield return (T)child; } foreach (T childOfChild in FindLogicalChildren<T>(child)) { yield return childOfChild; } } } } }
這樣我們?nèi)绻枰@取父控件類下面所有的TextBox控件列表,只需要如下操作即可。
//文本控件 var texboxList = FindLogicalChildren<TextBoxBase>(depObj); foreach (var textbox in texboxList) { if (textbox != null && !textbox.IsReadOnly) { textbox.TextChanged += (s, e) => { MainModelHelper.SetIsDirty(true); };//文本變化觸發(fā) } }
其他控件也是類似的方式處理,例如對于CheckBox和RadioButton,可以對它們共同的基類進行一并處理,如下所示。
//ToggleButton,包含CheckBox、RadioButton var buttonList = FindLogicalChildren<ToggleButton>(depObj); foreach (var toggle in buttonList) { if (toggle != null && toggle.IsEnabled) { toggle.Checked += (s, e) => { MainModelHelper.SetIsDirty(true); };//選擇變化觸發(fā) toggle.Unchecked += (s, e) => { MainModelHelper.SetIsDirty(true); };//選擇變化觸發(fā) } }
這樣我們把它放到一個靜態(tài)的輔助類里面方便使用,如下所示。
/// <summary> /// 對WPF控件的相關(guān)處理,包括遍歷查找等 /// </summary> public static class ControlHelper { /// <summary> /// 對界面的控件遍歷,并監(jiān)測狀態(tài)變化 /// </summary> /// <param name="depObj">父節(jié)點控件</param> public static void SetDirtyEvent(DependencyObject depObj) { //如果全局禁用,則不跟蹤臟數(shù)據(jù)狀態(tài) if (App.ViewModel!.DisableDirtyMessage) return; #region 對控件類型進行監(jiān)控 //文本控件 var texboxList = FindLogicalChildren<TextBoxBase>(depObj); foreach (var textbox in texboxList) { if (textbox != null && !textbox.IsReadOnly) { textbox.TextChanged += (s, e) => { MainModelHelper.SetIsDirty(true); };//文本變化觸發(fā) } } //下拉列表 var selectorList = FindLogicalChildren<Selector>(depObj); foreach (var selector in selectorList) { var selectorType = selector.GetType(); if (selector != null && selector.IsEnabled && selectorType != typeof(TabControl) && selectorType != typeof(ListBox)) //排除TabControl和ListBox選擇觸發(fā) { selector.SelectionChanged += (s, e) => { MainModelHelper.SetIsDirty(true); };//選擇變化觸發(fā) } } //ToggleButton,包含CheckBox、RadioButton var buttonList = FindLogicalChildren<ToggleButton>(depObj); foreach (var toggle in buttonList) { if (toggle != null && toggle.IsEnabled) { toggle.Checked += (s, e) => { MainModelHelper.SetIsDirty(true); };//選擇變化觸發(fā) toggle.Unchecked += (s, e) => { MainModelHelper.SetIsDirty(true); };//選擇變化觸發(fā) } } #endregion }
因此,對于窗口的控件的編輯狀態(tài)跟蹤,我們可以在窗口的Loaded或者ContentRendered中實現(xiàn)跟蹤即可,我這里實現(xiàn)覆蓋OnContentRendered的方式,來對窗口控件的統(tǒng)一跟蹤。
/// <summary> /// 該事件在loaded之后執(zhí)行,也是在所有元素渲染結(jié)束之后執(zhí)行 /// </summary> protected override void OnContentRendered(EventArgs e) { base.OnContentRendered(e);//初始化后默認臟數(shù)據(jù)狀態(tài)為false App.ViewModel!.IsDirty = false; //對控件變化進行跟蹤, 遍歷父級及子級節(jié)點 ControlHelper.SetDirtyEvent(this); //如果修改內(nèi)容,對退出窗口進行確認 this.Closing += (s, e) => { var isDisableDirty = App.ViewModel!.DisableDirtyMessage; //是否禁用了臟數(shù)據(jù)提示 var isDirty = App.ViewModel!.IsDirty; if (!isDisableDirty && isDirty) { //數(shù)據(jù)已臟,提示確認 if (MessageDxUtil.ShowYesNoAndWarning("界面控件數(shù)據(jù)已編輯過,是否確認丟棄并關(guān)閉窗口") != System.Windows.MessageBoxResult.Yes) { e.Cancel = true; } else { App.ViewModel!.IsDirty = false;//取消臟數(shù)據(jù)狀態(tài) GrowlUtil.ClearTips(); //清空提示 } } }; }
但是每個編輯窗口這樣做,肯定是代碼冗余的,我們優(yōu)化一下,把邏輯抽取到一個獨立的輔助類里面處理,這里改進后代碼如下所示。
/// <summary> /// 該事件在loaded之后執(zhí)行,也是在所有元素渲染結(jié)束之后執(zhí)行 /// </summary> protected override void OnContentRendered(EventArgs e) { base.OnContentRendered(e); //在窗口準(zhǔn)備完成后,對控件的內(nèi)容變化進行監(jiān)控,如修改過則退出確認 ControlHelper.SetDirtyWindow(this); }
這樣在頁面關(guān)閉的時候,我們提示用戶即可。
當(dāng)然,我們在主窗口視圖模型里面也是設(shè)置了一個總開關(guān),不需要的時候,關(guān)閉它即可。
App.ViewModel!.DisableDirtyMessage
?我們在上面的代碼邏輯中也可以看到,我們?nèi)绻_認丟棄修改內(nèi)容,那么狀態(tài)重置,并清空一些提示信息即可。
App.ViewModel!.IsDirty = false;//取消臟數(shù)據(jù)狀態(tài) GrowlUtil.ClearTips(); //清空提示
我們前面說到窗口對象的注入都是以Transient的方式,窗口的打開都是以每次構(gòu)建新對象的方式,視圖模型則是共享方式,因此,我們打開窗口的操作如下所示。
/// <summary> /// 批量添加頁面處理 /// </summary> [RelayCommand] private void BatchAdd() { if (this.ViewModel.SelectDictType == null) { GrowlUtil.ShowInfo("請選擇大類再添加項目"); return; } //獲取新增、編輯頁面接口 var page = App.GetService<BatchAddDictDataPage>(); page!.ViewModel.DictType = this.ViewModel.SelectDictType; page!.ViewModel.Item = new BatchAddDictDataDto(); //模態(tài)對話框打開 page.ShowDialog(); }
通過GetService<T>的方式獲取新的一個窗口對象,并賦值對應(yīng)的視圖模型即可,然后打開模態(tài)對話框界面。
如果用戶關(guān)閉,則會丟棄該對象,下次請求就是一個新的窗口實例了。
另外,我們窗口還可以配置快捷鍵ESC來關(guān)閉窗口,等同于按下關(guān)閉按鈕的處理。我們在頁面的Xaml文件中增加按鍵的綁定事件即可,如下代碼所示。
<Window.InputBindings> <KeyBinding Key="Esc" Command="{Binding BackCommand}" Modifiers="" /> </Window.InputBindings>
其中的BackCommand就是我們預(yù)設(shè)頁面關(guān)閉的方法。
/// <summary> /// 關(guān)閉窗體 /// </summary> [RelayCommand] private void Back() { this.Close(); }
對于通用的導(dǎo)入窗口,我們也是一樣的處理方式,我們通過一些事件的定義,把一些實現(xiàn)邏輯放在調(diào)用類上實現(xiàn)也可以的。
/// <summary> /// 導(dǎo)出內(nèi)容到Excel /// </summary> [RelayCommand] private void ImportExcel() { var page = App.GetService<ImportExcelData>(); page!.ViewModel.Items?.Clear(); page!.ViewModel.TemplateFile = $"系統(tǒng)用戶信息-模板.xls"; page!.OnDataSave -= ExcelData_OnDataSave; page!.OnDataSave += ExcelData_OnDataSave; //模態(tài)對話框打開 page.ShowDialog(); }
例如上面紅色部分的事件,我們就是放在調(diào)用類中差異化處理。
這樣就可以差異化不同的內(nèi)容,同時保留通用模塊的靈活性,導(dǎo)入界面如下所示。
文章來源:http://www.zghlxwxcb.cn/news/detail-712143.html
?文章來源地址http://www.zghlxwxcb.cn/news/detail-712143.html
到了這里,關(guān)于循序漸進介紹基于CommunityToolkit.Mvvm 和HandyControl的WPF應(yīng)用端開發(fā)(6) -- 窗口控件臟數(shù)據(jù)狀態(tài)IsDirty的跟蹤處理的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!