我们啥时候应该为组件实现带有 IContainer 参数的构造函数?

Posted

技术标签:

【中文标题】我们啥时候应该为组件实现带有 IContainer 参数的构造函数?【英文标题】:When should we implement a constructor with the IContainer parameter for a Component?我们什么时候应该为组件实现带有 IContainer 参数的构造函数? 【发布时间】:2021-05-15 03:01:42 【问题描述】:

假设我有一个 WinForms 组件。 可以是基于System.ComponentModel.Component或System.Windows.Forms.Control类的类(实际上Control继承Component)。

我的组件可能会使用我们应该正确处理的其他 .NET 类 - Pens、Brushes、ContextMenuStrips 等。我根据众所周知的IDisposable 模式在Dispose(bool disposing) 方法的实现中按预期处置它们。

我想知道,当我必须添加一个接受IConatiner 参数的特殊构造函数时,表单设计器将注册我的组件实例,以便在表单关闭期间自动处理资源:

private void InitializeComponent()

    this.components = new System.ComponentModel.Container();
    ...
    this.contextMenuStrip1 = new System.Windows.Forms.ContextMenuStrip(this.components);


protected override void Dispose(bool disposing)

    if (disposing && (components != null))
    
        components.Dispose();
    
    base.Dispose(disposing);

我分析了一些WinForms组件的Dispose(bool disposing)方法的源代码试图找到答案,但情况仍然不清楚。 例如,ContextMenuStrip 组件具有这样的构造函数,但 DataGridView 没有 - 尽管两者都有足够的对象在它们的 Dispose(bool disposing) 实现中处置。

有什么提示或想法吗?

【问题讨论】:

这是设计师的要求。您是否希望您的组件显示在设计器的托盘区域中,或者您希望将它作为父窗体? ContextMenuStrip(正如您所提到的)通常不是 Form 的父级,因此它作为组件处理并为此提供构造函数。当 Component 被释放时,重写的 Dispose(disposing) 方法被调用,该方法依次调用继承链(到 ToolStripDropDown -> ToolStrip -> Control)所有一次性对象的 Dispose() 方法。如果您将组件添加到项目中,所有这些都已预先构建。 如果您在代码中创建例如System.Windows.Forms.Timer 组件,您应该检查components 对象是否为null,如果是则创建它,然后使用构造函数构建定时器接受 IContainer 对象。如果您不这样做,那么您必须在表单关闭时显式处理 Timer。如果你不这样做,TimerNativeWindow 只会在它本身完成时调用KillTimer()。可能为时已晚;应用程序关闭时调用 Timer 的 Dispose(false) 方法:Timer 继续运行... @Jimi,即使一个Component没有实现IContainer构造函数,它仍然显示在Designer的Tray区域中。 是的,它将在托盘中可见。该构造函数只是为将组件添加到表单的组件集合的代码生成提供设计时支持。 这是设计者的要求:当您使用组件模板创建新组件时,所有(极少数)必需的部分都将添加到类中。然后设计器可以将组件添加到 IContainer 对象。如果 IContainer 对象尚不存在,则会创建它。这允许组件(显然未添加到任何 Controls 集合中)在处置表单时被处置。如果一个组件没有自动添加到 IContainer,它的 Dispose() 方法永远不会被调用。只有终结器会调用Dispose(false)。未释放非托管资源。 [...] 【参考方案1】:

如果您是组件作者,作为最佳实践,我的建议是实现接受IContainer 的构造函数重载,因为当组件具有这样的构造函数时,当开发人员将组件的实例放到设计表面时,设计人员生成一段代码,它关心组件的处置。

您的组件在 ToolBox 中可用吗?

对于可以通过工具箱访问的组件,如果用户将组件放在设计器上,那么他们不应该担心组件的处置。所有标准组件都遵循这种模式,用户从不关心他们通过设计器创建的控件和组件的处置,因此组件作者有责任关心设计时支持。

您的组件中有什么要处理的吗?

如果你没有要处理的东西,那么这个重载并不那么重要,但是如果你有任何东西要处理,那么你应该关心处理。如前一个要点所述,由于用户可能会将您的组件放在设计器上并依靠设计器来处理它们,因此您有责任提供该方法,以便设计器生成处理代码。否则,组件将不会被释放,从而导致内存/句柄泄漏。

作为组件用户

作为组件用户,如果您在代码中创建组件(而不是在设计时删除它),您有责任在不再需要组件时处理该组件。您可能会发现这篇文章很有用:Why should I insert a non-UI Windows.Forms component from the designer?

如果您从工具箱中删除了一个组件,那么您无需担心它的处置。所有标准组件都会生成一个代码来处理表单中的组件。

设计器生成的代码如何处理组件的处置?

对于具有接受IContainer 的构造函数的组件,当您将它们放在设计图面上时,设计器会生成代码来创建components 集合并将组件添加到该集合中,然后释放该集合(包括它的所有组件)当表单处理时。

查看以下由设计器生成的代码:

private System.ComponentModel.IContainer components = null;

protected override void Dispose(bool disposing)

   if (disposing && (components != null))
   
       components.Dispose();
   
   base.Dispose(disposing);


InitializeComponent()

    this.components = new System.ComponentModel.Container();
    ...
    ...
    this.myComponent = new MyComponent(this.components);
    ...
    ...

还有Dispose method of Container类:

protected virtual void Dispose(bool disposing) 
    if (disposing) 
        lock (syncObj) 
            while (siteCount > 0) 
                ISite site = sites[--siteCount];
                site.Component.Site = null;
                site.Component.Dispose();
            
            sites = null;
            components = null;
        
    

您会看到,当您的表单被释放时,它会释放在容器中找到的所有组件。

【讨论】:

考虑以下情况。开发者将实现Dispose(bool disposing) 的组件放置在Designer 的Tray 区域中,并且该实例未在表单的组件集合中注册。当用户关闭表单时,这个 Component 对象会发生什么?会不会导致严重的资源泄露问题,尤其是用户在应用生命周期中会反复打开这个表单? 是的,它会导致句柄/内存泄漏。如果组件需要处理,但没有接受IContainer 的构造函数,那么当用户将其放在设计表面时,它不会被添加到组件集合中,因此不会在处理时处理形式。 如果您允许组件的用户将组件放在设计器上,则他们不应关心组件的处置。他们希望您处理处置。但是对于工具箱中没有的类,用户有责任关心 dispose。 据我了解,所有组件实例最终都会被 GC 处理。但是,它们会阻止宿主表单被正确销毁,并且所有这些表单对象都将保留在内存中,直到 GC 操作,对吧? 我把答案改写了一点,希望它能让事情更清楚。简短的回答是:对于工具箱中可用并且有一些东西要处理的组件,作为最佳实践,您应该有一个接受IContainer 的构造函数重载。它是一种设计时支持,允许设计人员生成一段代码来处理组件的处理。否则,用户应该关心自己处置组件,这不会发生,因为他们从不处置通过设计器添加的任何东西。所以这是你的责任。【参考方案2】:

不需要使用 IContainer 创建额外的构造函数。如果你省略了这个,那么你必须自己将 Control 添加到 IContainer 中。

public void MyForm()

    InitializeComponents();

    this.components.Add(this.MyUserControl);

如果您不将此添加到 this.components,则必须显式 Dispose MyUserControl。 额外问题:如果没有人使用 this.components,那么 this.components 仍然为 null。

总而言之,每当您创建一个必须被处置的控件时,添加额外的构造函数的工作量就会减少。毕竟是单线的。

public MyUserControl()

    InitializeComponents();

    ...


public MyUserControl(IContainer container) : this()

    container?.Add(this);

好消息是,Visual Studio 设计师识别出这个构造函数并将使用它。它甚至会为你创建容器。

【讨论】:

IContainer 对象添加控件(从控件继承的可视组件)是没有意义的:当调用Dispose() 时,控件已经在其Controls 集合中释放了所有子控件。这是一个级联效应。另一方面,组件不是控件,因此它们不是此集合的一部分,这就是为什么将它们添加到在处置父容器时处置的不同容器的原因。其他不需要 parenting 且没有视觉效果(作为 ContextMenuStrip)的控件被添加到 Designer 的托盘并作为组件处理。 你是对的。根据描述Control.Dispose(bool)“释放Control及其子控件使用的非托管资源,并可选择释放托管资源。”唉,没有说子控件的托管资源是否会被释放,但是如果我看reference source,Dispose()的每个子控件都会被调用 没错。当一个控件被释放时,它会在所有属于其Controls 集合的控件和子控件上调用Dispose(),因此,对它们的Controls 集合执行相同的操作。但是组件不是这个集合的一部分,这就是components 集合发挥作用的地方,具有类似(实际上相同)的效果。

以上是关于我们啥时候应该为组件实现带有 IContainer 参数的构造函数?的主要内容,如果未能解决你的问题,请参考以下文章

应该是啥组件?

我啥时候应该使用导航控制器?

在带有 Pub 的 Dart 中,我啥时候应该对依赖项使用“任何”版本约束?

Spring 啥时候使用服务或组件? [复制]

我们啥时候应该使用 SNOWPIPE?

我们啥时候应该使用互斥锁,啥时候应该使用信号量