XAML: 自定义控件中事件处理的最佳实践

在开发 XAML(WPF/UWP) 应用程序中,有时候,我们需要创建自定义控件 (Custom Control) 来满足实际需求。而在自定义控件中,我们一般会用到一些原生的控件(如 Button、TextBox 等)来辅助以完成自定义控件的功能。

自定义控件并不像用户控件 (User Control) 一样,使用 Code-Behind(UI 与逻辑在一起)技术。相反,它通过把 UI 与逻辑分离而将两者解耦。因此,创建一个自定义控件会产生两个文件,一个是 Generic.xaml,在它里面定义其模板与样式;另一个是 <ControlName>.cs,这里面存放其逻辑,如下图:

在这种情况下,要想在代码中获取到模板里定义的控件,就不像 Code-Behind 中那么容易,而要借助于 OnApplyTemplate 和 GetTemplateChild 这两个方法。它们的意义分别如下:

  • OnApplyTemplate: 在自定义控件中,通常要重写这个方法,当基类调用 ApplyTemplate() 方法以构造可视化树时,会调用它;
  • GetTemplateChild: 获取 ControlTemplate 中所定义的可视化树上指定名称的元素;

所以,如果我们在模板中定义了一个名为 PART_ViewButton 的按钮,那么,我们可以这样获取它,并为它注册响应事件:

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            Button btnView = GetTemplateChild("PART_ViewButton") as Button;
            if (btnView != null)
            {
                btnView.Click += BtnView_Click;
            }
        }

        private void BtnView_Click(object sender, RoutedEventArgs e)
        {
            // 这里写响应逻辑
        }

当我们(或者其他人)要用这个控件时,通过给它设置了模板(一般都是默认模板)后, OnApplyTemplate 方法就会被执行。这样做看起来没什么问题。不过,其实这里有可能会引起一个听起来很严重的问题:内存泄露 (Memory Leak)

何为内存泄露

内存泄露有多种类型,一般来说,它是指某种类型的资源不再使用,但却仍然占用内存。换句话说,它从受管理的内存区域中“泄漏”出去了。如果在程序中有多处内存泄露,将会占有很多内存,并最终导到内存被耗尽。

在 C# 中,常见的内存泄露有:

? 没有移除事件监听;
? 没有销毁非托管资源(如数据库、文件流等);

对于上面两种情况,它们的解决办法也非常简单,分别是:要反注册事件(即移除事件监听)与调用 Dispose 方法(如果没有,则要实现 IDisposable 接口,并在其中销毁非托管资源)。

对于第二种情况,比较好理解;而对于第一种情况,问题是,为什么没有移除事件监听,会导致内存泄露呢?这是因为事件源比事件监听者的生命周期更长。来看代码:

            ObjectA objA = new ObjectA();
            ObjectB objB = new ObjectB();
            objA.Event += objB.EventHanlder;

ObjectA 中定义了 Event 事件,我们为它注册了一个事件处理器(对象 objB 中的 EventHanlder 方法);因此,事件源 objA 对事件监听对象 objB 存在一个引用。

如果 objB 不再使用,我们要销毁它,但由于 objA 引用了它,所以它不会被销毁、回收;它要等到 objA 销毁时,才能被销毁。所以本来需要被销毁的对象,却因有其它对象对它的引用,结果造成了内存泄露。

再回到自定义控件的问题上,因为我们的自定义控件,可能会被重写样式或者重写模板,这会使 OnApplyTemplate 方法在这个自定义控件的生命周期内被执行多次。所以,我们需要为那些通过 GetTemplateChild 方法得到并且又添加了事件处理的控件(如上述代码中的 btnView 控件)进行事件反注册。因为这些都是前一个模板中的控件(元素),当反注册后,原来的控件与事件监听者(自定义控件)本身就不存在引用关系,从而避免了内存泄露的问题。

如何解决

根据我们的解决思路,对之前的代码重构如下:

        private Button btnView = null;
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            // 先反注册事件
            if (btnView != null)
            {
                btnView.Click -= BtnView_Click;
            }

            btnView = GetTemplateChild("PART_ViewButton") as Button;

            if (btnView != null)
            {
                btnView.Click += BtnView_Click;
            }
        }

        private void BtnView_Click(object sender, RoutedEventArgs e)
        {
            // 这里写响应逻辑
        }

这样,就解决了本文开头所说的问题。不过,接下来,我们还需要做一点调整。

试想,如果我们的自定义控件中,有多个类似像前述 btnView 这样的控件,我们就要将上面的代码在 OnApplyTemplate 方法中复制若干次,从而导致 OnApplyTemplate 方法的复杂度增加,以及代码的可读性变差 。

为了改善这一点,我们将每个控件以及它的事件注册与反注册封装一下,重构后的代码如下:

        protected const string PART_ViewButton = nameof(PART_ViewButton);

        private Button btnView = null;

        public Button ViewButton
        {
            get
            {
                return btnView;
            }
            set
            {
                // 先反注册事件
                if (btnView != null)
                {
                    btnView.Click -= BtnView_Click;
                }

                btnView = value;

                if (btnView != null)
                {
                    btnView.Click += BtnView_Click;
                }
            }
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            ViewButton = GetTemplateChild(PART_ViewButton) as Button;
        }

        private void BtnView_Click(object sender, RoutedEventArgs e)
        {
            // 这里写响应逻辑
        }

针对最终的代码,这里再提几点:

1. 在 OnApplyTemplate 方法中,建议一开始要先调用 base.OnApplyTemplate();
2. 无论在为控件反注册事件,还是注册事件时,都要对控件是否为空进行判断,这是因为有可能用户重写模板时没有遵循 TemplatePart 属性中所指定的控件名称;
3. 将控件的名称声明为常量,可以避免字符串拼写错误;

总结

本文讨论了在 WPF 或 UWP 中创建自定义控件时,可能会遇到内存泄露的问题;这主要是由于模板中的控件事件没有反注册导致的。我们不仅分析了其中的原因,也给出了针对这种情况的最佳实践。

虽然在一般情况下,这一问题并不会造成较大的影响,但是,如果我们能够在这些细节上注意,这样不仅能够提高我们的代码质量与程序的性能,也能够给我们在设计或处理类似的问题时,提供必要的思路与经验。

原文地址:https://www.cnblogs.com/wpinfo/p/xaml_control_event_bp.html

时间: 2024-10-19 17:07:05

XAML: 自定义控件中事件处理的最佳实践的相关文章

.NetCore 2.1中的HttpClientFactory最佳实践

原文:.NetCore 2.1中的HttpClientFactory最佳实践 .NET Core 2.1中的HttpClientFactory最佳实践 ASP.NET Core 2.1中出现一个新的HttpClientFactory功能, 它有助于解决开发人员在使用HttpClient实例从其应用程序发出外部Web请求时可能遇到的一些常见问题. 介绍 在.NETCore平台的2.1新增了HttpClientFactory,虽然HttpClient这个类实现了disposable,但使用它的时候用

.NET Core 2.1中的HttpClientFactory最佳实践

ASP.NET Core 2.1中出现一个新的HttpClientFactory功能, 它有助于解决开发人员在使用HttpClient实例从其应用程序发出外部Web请求时可能遇到的一些常见问题. 介绍 在.NETCore平台的2.1新增了HttpClientFactory,虽然HttpClient这个类实现了disposable,但使用它的时候用声明using包装块的方式通常不是最好的选择.处理HttpClient,底层socket套接字不会立即释放.该HttpClient类是专为多个请求重复使

Window下使用Xshell连接VirtualBox中CentOS SSH最佳实践

网上已经有非常多讲怎样连接VMware的文章.可是针对一些可能遇到的细节没有讲全. 这里会有一个非常 实际的样例,附带全部软件的链接,保证成功. 最佳实践什么的都是骗人的. 1.安装VirtualBox 其实VMware会有更全面的虚拟化支持,比方cuda,这里选择VirtualBox更easy上手一些,对一个开发环境来说够用. VirtualBox直接去官网下载最新版,然后记得下载VirtualBox Extension Pack.当前版本号的VirtualBox相应的VirtualBox E

.NET中异常处理的最佳实践(转)

原文地址:http://www.cnblogs.com/xiaozhi_5638/p/4259115.html 目录 介绍 做最坏的打算 提前检查 不要信任外部数据 可信任的设备:摄像头.鼠标以及键盘 “写操作”同样可能失效 安全编程 不要抛出“new Exception()” 不要将重要的异常信息存储在Message属性中 每个线程要包含一个try/catch块 捕获异常后要记录下来 不要只记录Exception.Message的值,还需要记录Exception.ToString() 要捕获具

.NET中异常处理的最佳实践(译)

本文翻译自CodeProject上的一篇文章,原文地址. 目录 介绍 做最坏的打算 提前检查 不要信任外部数据 可信任的设备:摄像头.鼠标以及键盘 "写操作"同样可能失效 安全编程 不要抛出"new Exception()" 不要将重要的异常信息存储在Message属性中 每个线程要包含一个try/catch块 捕获异常后要记录下来 不要只记录Exception.Message的值,还需要记录Exception.ToString() 要捕获具体的异常 不要中止异常上

【转】Java中关于异常处理的十个最佳实践

原文地址:http://www.searchsoa.com.cn/showcontent_71960.htm 导读:异常处理是书写强健Java应用的一个重要部分,Java许你创建新的异常,并通过使用 throw 和 throws关键字抛出它们. 异常处理是书写强健Java应用的一个重要部分,它是关乎每个应用的一个非功能性需求,是为了优雅的处理任何错误状况,比如资源不可访问,非法输入,空输入等等.Java提供了几个异常处理特性,以try,catch和 finally 关键字的形式内建于语言自身之中

Java 编程中关于异常处理的 10 个最佳实践

异常处理是书写 强健 Java应用的一个重要部分.它是关乎每个应用的一个非功能性需求,是为了优雅的处理任何错误状况,比如资源不可访问,非法输入,空输入等等.Java提供了几个异常处理特性,以try,catch和finally 关键字的形式内建于语言自身之中.Java编程语言也允许你创建新的异常,并通过使用  throw 和 throws关键字抛出它们.事实上,异常处理不仅仅是知道语法.书写一个强健的代码更多的是一门艺术而不仅仅是一门科学,这里我们将讨论一些关于异常处理的Java最佳实践.这些 J

编程中关于异常处理的10个最佳实践

在实践中,异常处理不单单是知道语法这么简单.编写健壮的代码是更像是一门艺术,在本文中,将讨论java异常处理最佳实践.这些Java最佳实践遵循标准的JDK库,和几个处理错误和异常的开源代码.这还是一个提供java程序员编写健壮代码的便利手册. Java 编程中异常处理的最佳实践 这里是我通过在国内著名的IT培训平台扣丁学堂在线学习收集的10个java编程中进行异常处理的10最佳实践.在Java编程中对于检查异常有褒有贬,强制处理异常是一门语言的功能.在本文中,我们将尽量减少使用检查型异常,同时学

《.NET最佳实践》与Ext JS/Touch的团队开发

概述 有不少开发人员都问过我,Ext JS/Touch是否支持团队开发?对于这个问题,我可以毫不犹豫的回答:支持.原因是在Sencha官网博客中客户示例中,有不少项目都是基于团队模式开发的. 那为什么会出现这个问题?我觉得问题的关键在于不知道如何去进行模块独立调试或做最终的整合.对于这个问题,我觉得<.NET最佳实践>这本书(下 文中简称为实践一书)或许会给大家带来一点启示.虽然这本书是针对.NET而写的,但我觉得,这对于Ext JS/Touch,甚至于其他开发语言的开发,还是有不错的借鉴意义