C#中带参数的'UserControl'构造函数

110

有人可能认为我很疯狂,但我是那种喜欢带参数的构造函数的人(如果需要的话),而不是没有参数的构造函数后面再设置属性。我的思路是:如果这些属性在实际构建对象时是必需的,它们应该放在构造函数中。这样我可以获得两个优点:

  1. 我知道当对象被构建时(没有错误/异常),我的对象是好的。
  2. 它有助于避免忘记设置某个属性。

这种思维方式对于表单/用户控件开发开始对我造成麻烦。想象一下这个UserControl

public partial class MyUserControl : UserControl
{
  public MyUserControl(int parm1, string parm2)
  {
    // We'll do something with the parms, I promise
    InitializeComponent();
  }
}
在设计时,如果我将这个 UserControl 拖放到一个窗体上,就会出现一个 Exception

无法创建组件 'MyUserControl'...
System.MissingMethodException - 未为此对象定义无参构造函数。

在我看来,似乎唯一的解决方法是添加默认构造函数(除非有人知道其他方法)。
public partial class MyUserControl : UserControl
{
  public MyUserControl()
  {
    InitializeComponent();
  }

  public MyUserControl(int parm1, string parm2)
  {
    // We'll do something with the parms, I promise
    InitializeComponent();
  }
}
不包含无参构造函数的整个原因就在于避免使用它。而我甚至不能使用DesignMode属性来做类似以下的事情:
public partial class MyUserControl : UserControl
{
  public MyUserControl()
  {
    if (this.DesignMode)
    {
      InitializeComponent();
      return;
    }

    throw new Exception("Use constructor with parameters");
  }
}

这个也不起作用:

if (LicenseManager.UsageMode == LicenseUsageMode.Designtime)

好的,继续往下...

我有我的无参构造函数,我可以将其拖放到表单上,表单的InitializeComponent看起来会像这样:

private void InitializeComponent()
{
  this.myControl1 = new MyControl();

  // blah, blah
}

相信我,因为我已经这样做了(是的,我忽略了Visual Studio生成的注释),我尝试进行调整并向InitializeComponent传递参数以便将它们传递给MyControl的构造函数。

这就导致了下面的问题:

public MyForm()
{
  InitializeComponent(); // Constructed once with no parameters

  // Constructed a second time, what I really want
  this.myControl1 = new MyControl(anInt, aString);  
}

我使用带有参数的UserControl构造函数,是否需要添加一个不需要的第二个构造函数?并且需要实例化控件两次吗?

我感觉我一定做错了什么。有什么想法?意见?保证(希望如此)?

11个回答

71
关于Windows Forms的设计决策,更多或少地排除了窗体组件的参数化构造函数。你可以使用它们,但当你这样做时,你正在超出通常批准的机制范围。相反,Windows Forms更喜欢通过属性初始化值。如果没有广泛使用,这是一种有效的设计技术。
尽管如此,这也有一些好处:
1.对客户端的易用性。客户端代码不需要跟踪一堆数据,它可以立即创建某些内容并查看到明智(但无趣)的结果。 2.对设计人员的易用性。设计人员代码通常更清晰、更容易解析。 3.避免单个组件内的不寻常数据依赖。 (尽管Microsoft在SplitContainer中犯了这个错误)
在这种技术中,窗体有很多支持与设计人员正常工作的东西。像DefaultValueAttributeDesignerSerializationVisibilityAttributeBrowsableAttribute这样的东西,为您提供了提供丰富客户端体验的机会,而不需要太多的努力。
(这不是为了客户体验而在Windows Forms中做出的唯一妥协。抽象基类组件也可能变得棘手。)
我建议使用无参数构造函数,并遵循Windows Forms设计原则。如果您的UserControl必须执行真实的前提条件,请将其封装在另一个类中,然后通过属性将该类的实例分配给您的控件。这样做也会更好地分离关注点。

36

设计类有两种竞争的范式:

  1. 使用无参数构造函数,然后在之后设置一堆属性
  2. 使用带参数的构造函数在构造函数中设置属性

Visual Studio Windows Forms Designer要求控件提供无参数构造函数才能正常工作。实际上,设计控件只需要无参数构造函数来实例化控件,而不是设计它们(设计器实际上会解析InitializeComponent方法,同时设计控件)。这意味着您可以使用设计器设计一个表单或用户控件,而不需要无参数构造函数,但您不能设计另一个控件来使用该控件,因为设计器将无法实例化它。

如果您不打算以编程方式实例化您的控件(即手动构建您的用户界面),那么不必担心创建带参数的构造函数,因为它们不会被使用。即使您将以编程方式实例化控件,您也可能希望提供无参数构造函数,这样如果需要,在设计器中仍然可以使用它们。

不管使用哪种范式,通常最好将复杂初始化代码放在OnLoad()方法中,特别是因为DesignMode属性会在加载时起作用,但不会在构造函数中起作用。


10

我建议

public partial class MyUserControl : UserControl
{
    private int _parm1;
    private string _parm2;

    private MyUserControl()
    {
        InitializeComponent();
    }

    public MyUserControl(int parm1, string parm2) : this()
    {
        _parm1 = parm1;
        _parm2 = parm2;
    }
}

这种方式可以确保首先调用基础构造函数并且任何组件的引用都是有效的。

如果需要,您可以重载公共构造函数,确保控件始终使用正确的值进行实例化。

无论哪种方式,您都可以确保不会调用无参数构造函数。

我没有测试过,如果出现问题,我道歉!


如果我的记忆没有出错的话,由于私有的“无参”构造函数,我仍然无法在设计模式下使用它。但是,还是点个赞。 - JustLooking

5

这是一个设计问题,不仅在控制空间中会频繁出现。

通常情况下,即使没有参数化的构造函数也需要它。例如,很多值类型本应该没有参数化的构造函数,但是却无法创建这样的构造函数。

在这种情况下,您只能尽可能地设计好控件/组件。使用合理且最常见的默认参数可以大大帮助,因为您至少可以(希望)用良好的值初始化组件。

另外,尽量以一种可以在组件生成后更改这些属性的方式设计组件。对于Windows表单组件来说,这通常是没问题的,因为你可以在加载时间之前安全地进行任何操作。

再次强调,我同意 - 这并不理想,但这是我们必须应对和解决的问题。


4

只需执行以下操作:

public partial class MyUserControl : UserControl
{
    public MyUserControl() : this(-1, string.Empty)
    {
    }

    public MyUserControl(int parm1, string parm2)
    {
        // We'll do something with the parms, I promise
        if (parm1 == -1) { ... }
        InitializeComponent();
    }
}

然后,“真正的”构造函数可以相应地执行。

1
我承认,我不确定这给我带来了什么好处(或者它如何回答我的问题)。 - JustLooking
1
我注意到的是重复的InitializeComponent()调用。此外,一个带有UserControl的私有无参构造函数可以在VS设计器中正常工作。 - Bob Nadler
1
你可以在设计师中显示类似于“需要为运行时设置参数foo”的消息。为了指示需要设置通常使用构造函数传入的参数,可能需要处理一些控件设置来设置参数,这通常是在构造函数中完成的。 - PeteT

4
为设计者提供一个无参数的构造函数并将其设置为私有 - 如果您真的必须这样做... :-)
编辑:当然,这对于用户控件是行不通的。我显然没有想清楚。设计者需要执行InitializeComponent()中的代码,如果构造函数是私有的,则无法工作。对此感到抱歉。但是,这确实适用于窗体。

1
如果它是私有的,我怀疑设计师能否使用它。 - Fredrik Mörk
1
用户控件不起作用 - 如果您正在使用多个程序集,这也会带来重大问题。 - Reed Copsey
1
你确定它们是“private”的吗?如果你将构造函数设为私有,那么只有类内部的代码可以使用它。如果你将它们设为“internal”,则同一程序集中的所有代码都可以使用它们,但程序集外部的代码则不能(除非你使用“InternalsVisibleTo”属性)。为了让VS设计器能够使用它们,它们需要是“public”的。 - Fredrik Mörk
1
是的,可能是这样。反射不会被成员可访问性等细节所阻止... - Fredrik Mörk
1
私有构造函数用于模态对话框(表单)是一个非常好的方法,可以确保对话框的使用者不会忘记处理它(甚至创建它)。在表单上使用公共静态方法,享受编程之乐! - Dan Byström
显示剩余3条评论

4

简而言之,设计师喜欢没有参数的构造函数。据我所知,如果你真的想使用基于参数的构造函数,你可能需要通过某种方式绕过它。


1
这位设计师是那种...很厉害的家伙(有趣)!看起来我得利用一些变通的方法。我只是想确认一下是否有什么聪明的做法,或者是否存在某些“已知规则”,例如:“你永远不会像我使用我的用户控件那样做”。 - JustLooking

2

这个问题问了很久,但也许我的方法对某人有帮助。

我个人更喜欢使用参数化的构造函数来避免忘记设置某个属性。

所以,我不使用实际的构造函数,而是简单地定义一个 public void PostConstructor 方法,在其中放置所有通常放置在构造函数中的东西。因此,UserControl 的实际构造函数始终只包含 InitializeComponent()。 这样,您就不必调整自己喜欢的编程范例以适应 Visual Studio 运行设计器的需要。为了使这种编程模式能够正常工作,必须从底层开始遵循它。

在实践中,这种 PostConstructionalizm 看起来会像这样: 让我们从 UserControl 调用层次结构的底部开始。

public partial class ChildControl : UserControl
{
  public ChildControl()
  {
    InitializeComponent();
  }

  public void PostConstructor(YourParameters[])
  {
      //setting parameters/fillingdata into form
  }
}

所以包含ChildControl的UserControl看起来会像这样:
public partial class FatherControl : UserControl
{
  public FatherControl()
  {
    InitializeComponent();
  }

  public void PostConstructor(YourParameters[])
  {
      ChildControl.PostConstructor(YourParameters[])
      //setting parameters/fillingdata into form
  }
}

最后,一个调用用户控件的表单只需在InitializeComponent之后放置PostConstructor。
public partial class UI : Form
{
  public UI(yourParameters[])
  {
    InitializeComponent();
    FatherControl.PostConstructor(yourParameters[]);
  }
}

1

我有一种解决方法。

  1. 在表单上创建一个没有参数的控件A。
  2. 在表单构造函数中使用带参数的构造函数创建控件B。
  3. 将A的位置和大小复制到B。
  4. 让A不可见。
  5. 将B添加到A的父级。

希望这可以帮到你。我遇到了相同的问题,尝试并测试了这种方法。

演示代码:

public Form1()
{
    InitializeComponent();
    var holder = PositionHolderAlgorithmComboBox;
    holder.Visible = false;
    fixedKAlgorithmComboBox = new MiCluster.UI.Controls.AlgorithmComboBox(c => c.CanFixK);
    fixedKAlgorithmComboBox.Name = "fixedKAlgorithmComboBox";
    fixedKAlgorithmComboBox.Location = holder.Location;
    fixedKAlgorithmComboBox.Size = new System.Drawing.Size(holder.Width, holder.Height);
    holder.Parent.Controls.Add(fixedKAlgorithmComboBox);
}

holder是控件A,fixedKAlgorithmComboBox是控件B。

更好、更完整的解决方案是使用反射(reflect)逐个复制A到B的属性。目前我很忙,没有时间去完成这个任务。也许将来会回来分享代码。不过这并不难,我相信你可以自己完成。


0
我曾经遇到过类似的问题,试图将在主Windows表单中创建的对象传递给自定义UserControl表单。对我有用的方法是在中添加一个具有默认值的属性,并在主表单中的InitializeComponent()调用后进行更新。具有默认值可以防止WinForms设计器抛出“对象引用未设置为对象实例”的错误。
例如:
// MainForm.cs
public partial class MainForm : Form
   public MainForm() 
   {
     /* code for parsing configuration parameters which producs in <myObj> myConfig */
     InitializeComponent();
     myUserControl1.config = myConfig; // set the config property to myConfig object
   }

//myUserControl.Designer.cs
partial class myUserControl
{
    /// <summary> 
    /// Required designer variable.
    /// </summary>
    private System.ComponentModel.IContainer components = null;

    /// <summary> 
    /// Clean up any resources being used.
    /// </summary>
    /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
    protected override void Dispose(bool disposing)
    {
        if (disposing && (components != null))
        {
            components.Dispose();
        }
        base.Dispose(disposing);
    }

    // define the public property to hold the config and give it a default value
    private myObj _config = new myObj(param1, param2, ...);      
    public myObj config
    {
        get
        {
            return _config ;
        }
        set
        {
            _config = value;
        }
    }

    #region Component Designer generated code
    ...
}

希望这能帮到你!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接