循序渐进介绍基于CommunityToolkit.Mvvm 和HandyControl的WPF应用端开发(6) -- 窗口控件脏数据状态IsDirty的跟踪处理

简介: 循序渐进介绍基于CommunityToolkit.Mvvm 和HandyControl的WPF应用端开发(6) -- 窗口控件脏数据状态IsDirty的跟踪处理

在我们窗口新增、编辑状态下的时候,我们往往会根据是否修改过的痕迹-也就是脏数据状态进行跟踪,如果用户发生了数据修改,我们在用户退出窗口的时候,提供用户是否丢弃修改还是继续编辑,这样在一些重要录入时的时候,可以避免用户不小心关掉窗口,导致窗口的数据要重新录入的尴尬场景。本篇随笔介绍基于WPF开发中,窗口控件脏数据状态IsDirty的跟踪处理操作。

1、WPF的Page页面、Window窗口对象和视图模型

MVVM是Model-View-ViewModel的简写。类似于目前比较流行的MVC、MVP设计模式,主要目的是为了分离视图(View)和模型(Model)的耦合。

对于MVVM应用中,MVVM其中包括Model、View、ViewModel三者内容。其中Page或者Window对象,都是属于视图View的概念。由于目前我们程序框架大多数情况下采用IOC的控制反转方式来调用,因此对象和接口的注入是程序开始的重要工作。

.net 中 负责依赖注入和控制反转的核心组件有两个:IServiceCollection和IServiceProvider。其中,IServiceCollection负责注册,IServiceProvider负责提供实例。在注册接口和类时,IServiceCollection提供了三种注册方法,如下所示:

1、services.AddTransient<IDictDataService, DictDataService>();  // 瞬时生命周期

2、services.AddScoped<IDictDataService, DictDataService>();     // 域生命周期

3、services.AddSingleton<IDictDataService, DictDataService>();  // 全局单例生命周期

如果使用AddTransient方法注册,IServiceProvider每次都会通过GetService方法创建一个新的实例;

如果使用AddScoped方法注册, 在同一个域(Scope)内,IServiceProvider每次都会通过GetService方法调用同一个实例,可以理解为在局部实现了单例模式;

如果使用AddSingleton方法注册, 在整个应用程序生命周期内,IServiceProvider只会创建一个实例。

了解了这几个不同的注入方式,有助于我们了解WPF的整个注入对象的生命周期,对于页面来说,由于采用了导航的方式,我们在注入的时候,采用单例的方式,对于弹出的编辑、新增、导入、批量处理的这种常规视图,我们采用用完就丢弃的AddTransient方式,而视图模型为了方便,我们也采用单件的方式构建即可。

我们在WPF程序入口的程序代码App.xaml.cs中注入相关的对象信息。

登录窗口和主窗口,采用单件注入方式,如下代码所示。

// 程序导航主窗体及视图模型
services.AddSingleton<INavigationWindow, MainWindow>();
services.AddSingleton<ViewModels.MainWindowViewModel>();
//登录窗口
services.AddSingleton<LoginView, LoginView>();

而对于我们程序的视图或者视图模型对象来做,我们不可能一一按名字插入,应该通过一种动态的方式来批量处理,也就是根据各自的接口类型/继承基类来处理即可。

#region 加入视图页面和视图模型
//使用动态方式加入
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、弹出式窗口的几种注入的方式,通过接口判断和基类判断的方式,自动注入相关的对象。

2、对窗口控件的修改进行跟踪

了解了上面几种对象的注入方式,我们来进一步了解弹出式窗口对象Window的控件的修改状态跟踪。

由于WPF不能像Winform那种,通过父对象的Controls集合就可以遍历出来所有的对象,然后进行一一判断,而WPF对象没有这个属性,因此也就无法直接的对控件的修改状态进行跟踪。

那是不是没有办法对窗口下面的控件进行一一判断了呢?肯定不是,办法还是有的,就是通过内置辅助类LogicalTreeHelper,或者VisualTreeHelper的方式,由于前者是所有窗口或者页面的逻辑控件都会跟踪到,后者VisualTreeHelper只是对可视化的控件进行跟踪,因此我们这里选择LogicalTreeHelper来对控件进行遍历处理。

实现的效果,就是对应窗口编辑内容发生变化,如果用户退出,提示用户即可,如下界面效果所示。

由于窗口元素都是继承自Visual这个wpf的基类,而这个基类又是继承于DependencyObject,如下代码所示。

public abstract class Visual : DependencyObject

而辅助类,可以通过GetChildren的方法获取对应的控件列表,接口如下所示。

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;
                        }
                    }
                }
            }
        }

这样我们如果需要获取父控件类下面所有的TextBox控件列表,只需要如下操作即可。

//文本控件
var texboxList = FindLogicalChildren<TextBoxBase>(depObj);
foreach (var textbox in texboxList)
{
    if (textbox != null && !textbox.IsReadOnly)
    {
        textbox.TextChanged += (s, e) => { MainModelHelper.SetIsDirty(true); };//文本变化触发
    }
}

其他控件也是类似的方式处理,例如对于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); };//选择变化触发
        toggle.Unchecked += (s, e) => { MainModelHelper.SetIsDirty(true); };//选择变化触发
    }
}

这样我们把它放到一个静态的辅助类里面方便使用,如下所示。

/// <summary>
/// 对WPF控件的相关处理,包括遍历查找等
/// </summary>
public static class ControlHelper
{
    /// <summary>
    /// 对界面的控件遍历,并监测状态变化
    /// </summary>
    /// <param name="depObj">父节点控件</param>
    public static void SetDirtyEvent(DependencyObject depObj)
    {
        //如果全局禁用,则不跟踪脏数据状态
        if (App.ViewModel!.DisableDirtyMessage)
            return;
        #region 对控件类型进行监控
        //文本控件
        var texboxList = FindLogicalChildren<TextBoxBase>(depObj);
        foreach (var textbox in texboxList)
        {
            if (textbox != null && !textbox.IsReadOnly)
            {
                textbox.TextChanged += (s, e) => { MainModelHelper.SetIsDirty(true); };//文本变化触发
            }
        }
        //下拉列表
        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选择触发
            {
                selector.SelectionChanged += (s, e) => { MainModelHelper.SetIsDirty(true); };//选择变化触发
            }
        }
        //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); };//选择变化触发
                toggle.Unchecked += (s, e) => { MainModelHelper.SetIsDirty(true); };//选择变化触发
            }
        } 
        #endregion
    }

因此,对于窗口的控件的编辑状态跟踪,我们可以在窗口的Loaded或者ContentRendered中实现跟踪即可,我这里实现覆盖OnContentRendered的方式,来对窗口控件的统一跟踪。

/// <summary>
    /// 该事件在loaded之后执行,也是在所有元素渲染结束之后执行
    /// </summary>
    protected override void OnContentRendered(EventArgs e)
    {
        base.OnContentRendered(e);//初始化后默认脏数据状态为false
        App.ViewModel!.IsDirty = false;
        //对控件变化进行跟踪, 遍历父级及子级节点
        ControlHelper.SetDirtyEvent(this);
        //如果修改内容,对退出窗口进行确认
        this.Closing += (s, e) =>
        {
            var isDisableDirty = App.ViewModel!.DisableDirtyMessage; //是否禁用了脏数据提示
            var isDirty = App.ViewModel!.IsDirty;
            if (!isDisableDirty && isDirty)
            {
                //数据已脏,提示确认
                if (MessageDxUtil.ShowYesNoAndWarning("界面控件数据已编辑过,是否确认丢弃并关闭窗口") != System.Windows.MessageBoxResult.Yes)
                {
                    e.Cancel = true;
                }
                else
                {
                    App.ViewModel!.IsDirty = false;//取消脏数据状态
                    GrowlUtil.ClearTips(); //清空提示
                }
            }
        };
    }

但是每个编辑窗口这样做,肯定是代码冗余的,我们优化一下,把逻辑抽取到一个独立的辅助类里面处理,这里改进后代码如下所示。

/// <summary>
/// 该事件在loaded之后执行,也是在所有元素渲染结束之后执行
/// </summary>
protected override void OnContentRendered(EventArgs e)
{
    base.OnContentRendered(e);
    //在窗口准备完成后,对控件的内容变化进行监控,如修改过则退出确认
    ControlHelper.SetDirtyWindow(this);
}

这样在页面关闭的时候,我们提示用户即可。

当然,我们在主窗口视图模型里面也是设置了一个总开关,不需要的时候,关闭它即可。

App.ViewModel!.DisableDirtyMessage

我们在上面的代码逻辑中也可以看到,我们如果确认丢弃修改内容,那么状态重置,并清空一些提示信息即可。

App.ViewModel!.IsDirty = false;//取消脏数据状态
GrowlUtil.ClearTips(); //清空提示

我们前面说到窗口对象的注入都是以Transient的方式,窗口的打开都是以每次构建新对象的方式,视图模型则是共享方式,因此,我们打开窗口的操作如下所示。

/// <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();
    //模态对话框打开
    page.ShowDialog();
}

通过GetService<T>的方式获取新的一个窗口对象,并赋值对应的视图模型即可,然后打开模态对话框界面。

如果用户关闭,则会丢弃该对象,下次请求就是一个新的窗口实例了。

另外,我们窗口还可以配置快捷键ESC来关闭窗口,等同于按下关闭按钮的处理。我们在页面的Xaml文件中增加按键的绑定事件即可,如下代码所示。

<Window.InputBindings>
    <KeyBinding
        Key="Esc"
        Command="{Binding BackCommand}"
        Modifiers="" />
</Window.InputBindings>

其中的BackCommand就是我们预设页面关闭的方法。

/// <summary>
        /// 关闭窗体
        /// </summary>
        [RelayCommand]
        private void Back()
        {
            this.Close();
        }

对于通用的导入窗口,我们也是一样的处理方式,我们通过一些事件的定义,把一些实现逻辑放在调用类上实现也可以的。

/// <summary>
    /// 导出内容到Excel
    /// </summary>
    [RelayCommand]
    private void ImportExcel()
    {
        var page = App.GetService<ImportExcelData>();
        page!.ViewModel.Items?.Clear();
        page!.ViewModel.TemplateFile = $"系统用户信息-模板.xls";
        page!.OnDataSave -= ExcelData_OnDataSave;
        page!.OnDataSave += ExcelData_OnDataSave;
        //模态对话框打开
        page.ShowDialog();
    }

例如上面红色部分的事件,我们就是放在调用类中差异化处理。

这样就可以差异化不同的内容,同时保留通用模块的灵活性,导入界面如下所示。

 

链接附注

如对我们的代码生成工具有兴趣,可以到官网下载使用《代码生成工具Database2Sharp》。

如需了解我们官网对《SqlSugar开发框架》的介绍,可以参考《SqlSugar开发框架》了解。

如需阅读我们对于《SqlSugar开发框架》文章介绍,可以参考博客园的随笔标签《SqlSugar随笔 , WPF随笔》学习了解。

 

专注于代码生成工具、.Net/.NetCore 框架架构及软件开发,以及各种Vue.js的前端技术应用。著有Winform开发框架/混合式开发框架、微信开发框架、Bootstrap开发框架、ABP开发框架、SqlSugar开发框架等框架产品。
 转载请注明出处:撰写人:伍华聪  http://www.iqidi.com

相关文章
|
14天前
|
安全 C# 数据安全/隐私保护
WPF安全加固全攻略:从数据绑定到网络通信,多维度防范让你的应用固若金汤,抵御各类攻击
【8月更文挑战第31天】安全性是WPF应用程序开发中不可或缺的一部分。本文从技术角度探讨了WPF应用面临的多种安全威胁及防护措施。通过严格验证绑定数据、限制资源加载来源、实施基于角色的权限管理和使用加密技术保障网络通信安全,可有效提升应用安全性,增强用户信任。例如,使用HTML编码防止XSS攻击、检查资源签名确保其可信度、定义安全策略限制文件访问权限,以及采用HTTPS和加密算法保护数据传输。这些措施有助于全面保障WPF应用的安全性。
23 0
|
14天前
|
C# 开发者 Windows
全面指南:WPF无障碍设计从入门到精通——让每一个用户都能无障碍地享受你的应用,从自动化属性到焦点导航的最佳实践
【8月更文挑战第31天】为了确保Windows Presentation Foundation (WPF) 应用程序对所有用户都具备无障碍性,开发者需关注无障碍设计原则。这不仅是法律要求,更是社会责任,旨在让技术更人性化,惠及包括视障、听障及行动受限等用户群体。
37 0
|
14天前
|
C# UED 开发者
WPF与性能优化:掌握这些核心技巧,让你的应用从卡顿到丝滑,彻底告别延迟,实现响应速度质的飞跃——从布局到动画全面剖析与实例演示
【8月更文挑战第31天】本文通过对比优化前后的方法,详细探讨了提升WPF应用响应速度的策略。文章首先分析了常见的性能瓶颈,如复杂的XAML布局、耗时的事件处理、不当的数据绑定及繁重的动画效果。接着,通过具体示例展示了如何简化XAML结构、使用后台线程处理事件、调整数据绑定设置以及利用DirectX优化动画,从而有效提升应用性能。通过这些优化措施,WPF应用将更加流畅,用户体验也将得到显著改善。
42 0
|
14天前
|
测试技术 C# 开发者
“代码守护者:详解WPF开发中的单元测试策略与实践——从选择测试框架到编写模拟对象,全方位保障你的应用程序质量”
【8月更文挑战第31天】单元测试是确保软件质量的关键实践,尤其在复杂的WPF应用中更为重要。通过为每个小模块编写独立测试用例,可以验证代码的功能正确性并在早期发现错误。本文将介绍如何在WPF项目中引入单元测试,并通过具体示例演示其实施过程。首先选择合适的测试框架如NUnit或xUnit.net,并利用Moq模拟框架隔离外部依赖。接着,通过一个简单的WPF应用程序示例,展示如何模拟`IUserRepository`接口并验证`MainViewModel`加载用户数据的正确性。这有助于确保代码质量和未来的重构与扩展。
25 0
|
14天前
|
前端开发 C# 设计模式
“深度剖析WPF开发中的设计模式应用:以MVVM为核心,手把手教你重构代码结构,实现软件工程的最佳实践与高效协作”
【8月更文挑战第31天】设计模式是在软件工程中解决常见问题的成熟方案。在WPF开发中,合理应用如MVC、MVVM及工厂模式等能显著提升代码质量和可维护性。本文通过具体案例,详细解析了这些模式的实际应用,特别是MVVM模式如何通过分离UI逻辑与业务逻辑,实现视图与模型的松耦合,从而优化代码结构并提高开发效率。通过示例代码展示了从模型定义、视图模型管理到视图展示的全过程,帮助读者更好地理解并应用这些模式。
30 0
|
14天前
|
容器 C# Docker
WPF与容器技术的碰撞:手把手教你Docker化WPF应用,实现跨环境一致性的开发与部署
【8月更文挑战第31天】容器技术简化了软件开发、测试和部署流程,尤其对Windows Presentation Foundation(WPF)应用程序而言,利用Docker能显著提升其可移植性和可维护性。本文通过具体示例代码,详细介绍了如何将WPF应用Docker化的过程,包括创建Dockerfile及构建和运行Docker镜像的步骤。借助容器技术,WPF应用能在任何支持Docker的环境下一致运行,极大地提升了开发效率和部署灵活性。
28 0
|
14天前
|
存储 C# 关系型数据库
“云端融合:WPF应用无缝对接Azure与AWS——从Blob存储到RDS数据库,全面解析跨平台云服务集成的最佳实践”
【8月更文挑战第31天】本文探讨了如何将Windows Presentation Foundation(WPF)应用与Microsoft Azure和Amazon Web Services(AWS)两大主流云平台无缝集成。通过具体示例代码展示了如何利用Azure Blob Storage存储非结构化数据、Azure Cosmos DB进行分布式数据库操作;同时介绍了如何借助Amazon S3实现大规模数据存储及通过Amazon RDS简化数据库管理。这不仅提升了WPF应用的可扩展性和可用性,还降低了基础设施成本。
34 0
|
14天前
|
vr&ar C# 图形学
WPF与AR/VR的激情碰撞:解锁Windows Presentation Foundation应用新维度,探索增强现实与虚拟现实技术在现代UI设计中的无限可能与实战应用详解
【8月更文挑战第31天】增强现实(AR)与虚拟现实(VR)技术正迅速改变生活和工作方式,在游戏、教育及工业等领域展现出广泛应用前景。本文探讨如何在Windows Presentation Foundation(WPF)环境中实现AR/VR功能,通过具体示例代码展示整合过程。尽管WPF本身不直接支持AR/VR,但借助第三方库如Unity、Vuforia或OpenVR,可实现沉浸式体验。例如,通过Unity和Vuforia在WPF中创建AR应用,或利用OpenVR在WPF中集成VR功能,从而提升用户体验并拓展应用功能边界。
28 0
|
14天前
|
区块链 C# 存储
链动未来:WPF与区块链的创新融合——从智能合约到去中心化应用,全方位解析开发安全可靠DApp的最佳路径
【8月更文挑战第31天】本文以问答形式详细介绍了区块链技术的特点及其在Windows Presentation Foundation(WPF)中的集成方法。通过示例代码展示了如何选择合适的区块链平台、创建智能合约,并在WPF应用中与其交互,实现安全可靠的消息存储和检索功能。希望这能为WPF开发者提供区块链技术应用的参考与灵感。
30 0
|
14天前
|
C# 机器学习/深度学习 搜索推荐
WPF与机器学习的完美邂逅:手把手教你打造一个具有智能推荐功能的现代桌面应用——从理论到实践的全方位指南,让你的应用瞬间变得高大上且智能无比
【8月更文挑战第31天】本文详细介绍如何在Windows Presentation Foundation(WPF)应用中集成机器学习功能,以开发具备智能化特性的桌面应用。通过使用Microsoft的ML.NET框架,本文演示了从安装NuGet包、准备数据集、训练推荐系统模型到最终将模型集成到WPF应用中的全过程。具体示例代码展示了如何基于用户行为数据训练模型,并实现实时推荐功能。这为WPF开发者提供了宝贵的实践指导。
23 0