关于c#:如何让Visual Studio 2008 Windows窗体设计器呈现实现抽象基类的Form?

How can I get Visual Studio 2008 Windows Forms designer to render a Form that implements an abstract base class?

我遇到了Windows窗体中继承的控件的问题,需要一些建议。

我确实为列表中的项目(自制的由面板组成的GUI列表)和一些继承的控件使用了一个基类,这些控件用于可以添加到列表中的每种数据类型。

它没有问题,但我现在发现,使基控件成为一个抽象类是正确的,因为它有方法,需要在所有继承的控件中实现,从基控件内部的代码调用,但不能也不能在基类中实现。

当我将基控件标记为抽象时,Visual Studio 2008设计器拒绝加载窗口。

有没有一种方法可以让设计器处理抽象的基控件?


我知道必须有一种方法来做到这一点(我找到了一种干净的方法)。盛的解决方案正是我提出的临时解决方案,但在一位朋友指出,江户一〔1〕阶级最终继承自江户一〔2〕阶级之后,我们应该能够做到这一点。如果他们能做到,我们就能做到。

我们从这个代码到问题

1
Form1 : Form

问题

1
2
3
public class Form1 : BaseForm
...
public abstract class BaseForm : Form

这就是最初的问题产生的地方。如前所述,一位朋友指出,System.Windows.Forms.Form实现了一个抽象的基类。我们发现…

更好的解决方案的证明

  • 继承层次结构:

    • 对象系统
      • System.MarshalByRefObject(public **abstract** class MarshalByRefObject)
        • 系统组件模型组件
          • system.windows.forms.control系统.windows.forms.control
            • System.Windows.Forms.Scrollable控件
              • system.windows.forms.containerControl(系统.windows.forms.containerControl)
                • System.Windows.Forms.窗体

从这一点上,我们知道设计器可以显示一个实现了基本抽象类的类,它只是不能显示一个立即实现了基本抽象类的设计器类。必须有最多5个中间层,但是我们测试了1个抽象层,并且最初提出了这个解决方案。

初始解

1
2
3
4
5
6
public class Form1 : MiddleClass
...
public class MiddleClass : BaseForm
...
public abstract class BaseForm : Form
...

这实际上是可行的,设计师把它处理得很好,问题解决了……但是,您的生产应用程序中有一个额外的继承级别,这只是由于WinForms设计器中的不足而必需的!

这不是100%可靠的解决方案,但很好。基本上,您可以使用#if DEBUG来提出改进的解决方案。

精制溶液

格式1.CS

1
2
3
4
5
6
#if DEBUG
public class Form1 : MiddleClass
#else
public class Form1 : BaseForm
#endif
...

中提琴

1
2
public class MiddleClass : BaseForm
...

基础形式

1
2
public abstract class BaseForm : Form
...

如果处于调试模式,则只使用"初始解决方案"中概述的解决方案。其思想是,您将永远不会通过调试构建发布生产模式,并且您将始终以调试模式进行设计。

设计器将始终针对当前模式中生成的代码运行,因此不能在发布模式中使用设计器。但是,只要您在调试模式下进行设计,并在发布模式下发布内置的代码,您就可以开始了。

唯一可靠的解决方案是,如果您可以通过预处理器指令测试设计模式。


@smelch,有一个更好的解决方案,不需要创建中间控件,甚至是用于调试。

我们想要什么

首先,让我们定义最后一个类和基本抽象类。

1
2
3
4
public class MyControl : AbstractControl
...
public abstract class AbstractControl : UserControl // Also works for Form
...

现在我们只需要一个描述提供者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class AbstractControlDescriptionProvider<TAbstract, TBase> : TypeDescriptionProvider
{
    public AbstractControlDescriptionProvider()
        : base(TypeDescriptor.GetProvider(typeof(TAbstract)))
    {
    }

    public override Type GetReflectionType(Type objectType, object instance)
    {
        if (objectType == typeof(TAbstract))
            return typeof(TBase);

        return base.GetReflectionType(objectType, instance);
    }

    public override object CreateInstance(IServiceProvider provider, Type objectType, Type[] argTypes, object[] args)
    {
        if (objectType == typeof(TAbstract))
            objectType = typeof(TBase);

        return base.CreateInstance(provider, objectType, argTypes, args);
    }
}

最后,我们将一个typescriptionProvider属性应用于数据库控件。

1
2
3
[TypeDescriptionProvider(typeof(AbstractControlDescriptionProvider<AbstractControl, UserControl>))]
public abstract class AbstractControl : UserControl
...

就是这样。不需要中间控制。

在同一个解决方案中,provider类可以应用于任意多个抽象基。

*编辑*在app.config中还需要以下内容

1
</appSettings>

感谢@user3057544的建议。


@斯梅尔奇,谢谢你的帮助,因为我最近碰到了同样的问题。

以下是为了防止编译警告而对文章做的一个小改动(通过将基类放在#if DEBUG预处理器指令中):

1
2
3
4
5
6
public class Form1
#if DEBUG  
 : MiddleClass
#else  
 : BaseForm
#endif

我也遇到了类似的问题,但找到了一种方法来重构事物,以使用接口代替抽象基类:

1
2
3
4
5
interface Base {....}

public class MyUserControl<T> : UserControl, Base
     where T : /constraint/
{ ... }

这可能不适用于每种情况,但在可能的情况下,它会导致比条件编译更干净的解决方案。


我有胡安·卡洛斯·迪亚兹解决方案的建议。它对我很有用,但有点问题。当我启动vs并进入设计器时,一切都正常。但是在运行解决方案之后,停止并退出它,然后尝试进入设计器,异常会一次又一次出现,直到重新启动vs。但是我找到了解决方案-所有要做的事情都添加到你的app.config下面

1
  </appSettings>

我将在这个答案中使用另一个链接本文的问题的解决方案。本文建议使用自定义TypeDescriptionProvider并具体实现抽象类。设计器将询问自定义提供程序要使用哪种类型,您的代码可以返回具体的类,这样,当您完全控制抽象类作为具体类的显示方式时,设计器会很高兴。

更新:我在另一个问题的答案中包含了一个文档化的代码示例。那里的代码确实有效,但有时我必须通过一个干净/构建周期,正如我的答案中所指出的那样,才能使它正常工作。


对于那些说Juan Carlos Diaz的TypeDescriptionProvider不起作用,也不喜欢条件编译的人,我有一些建议:

首先,您可能需要重新启动Visual Studio,以使代码中的更改在表单设计器中工作(我不得不这样做,简单的重新生成没有工作——或者不是每次都这样)。

我将为抽象基形式的情况介绍这个问题的解决方案。假设您有一个BaseForm类,并且您希望基于它的任何表单都是可设计的(这将是Form1)。胡安·卡洛斯·迪亚兹(JuanCarlosDiaz)提出的TypeDescriptionProvider也不适合我。下面是我如何使它工作起来的,通过将它与中间类解决方案(由smelch)结合起来,但不使用#if DEBUG条件编译和一些更正:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[TypeDescriptionProvider(typeof(AbstractControlDescriptionProvider<BaseForm, BaseFormMiddle2>))]   // BaseFormMiddle2 explained below
public abstract class BaseForm : Form
{
    public BaseForm()
    {
        InitializeComponent();
    }

    public abstract void SomeAbstractMethod();
}


public class Form1 : BaseForm   // Form1 is the form to be designed. As you see it's clean and you do NOTHING special here (only the the normal abstract method(s) implementation!). The developer of such form(s) doesn't have to know anything about the abstract base form problem. He just writes his form as usual.
{
    public Form1()
    {
        InitializeComponent();
    }

    public override void SomeAbstractMethod()
    {
        // implementation of BaseForm's abstract method
    }
}

注意baseform类上的属性。然后你只需要声明TypeDescriptionProvider和两个中产阶级,但不要担心,他们是无形的,与form1的开发者无关。第一个实现抽象成员(并使基类非抽象)。第二个是空的——这只是vs表单设计器工作所必需的。然后你把第二个中产阶级分配给EDOCX1的TypeDescriptionProvider。没有条件编译。

我还有两个问题:

  • 问题1:在Designer(或某些代码)中更改Form1后,再次出现错误(尝试在Designer中再次打开时)。
  • 问题2:当在设计器中更改表单1的大小,并且在表单设计器中关闭并重新打开表单时,BaseForm的控件放置不正确。

第一个问题(您可能没有这个问题,因为在我的项目中,它在其他几个地方困扰着我,通常会产生"无法将X类型转换为X类型"异常)。我在TypeDescriptionProvider中通过比较类型名(全名)而不是比较类型(见下文)来解决这个问题。

第二个问题。我真的不知道为什么基窗体的控件不能在Form1类中设计,并且在调整大小后它们的位置会丢失,但是我已经解决了这个问题(不是一个好的解决方案-如果你知道的更好,请写)。我只是手动地将baseform的按钮(应该在右下角)移动到从baseform的load事件异步调用的方法中的正确位置:BeginInvoke(new Action(CorrectLayout));,我的基类只有"OK"和"Cancel"按钮,所以情况很简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class BaseFormMiddle1 : BaseForm
{
    protected BaseFormMiddle1()
    {
    }

    public override void SomeAbstractMethod()
    {
        throw new NotImplementedException();  // this method will never be called in design mode anyway
    }
}


class BaseFormMiddle2 : BaseFormMiddle1  // empty class, just to make the VS designer working
{
}

这里有稍微修改过的TypeDescriptionProvider版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class AbstractControlDescriptionProvider<TAbstract, TBase> : TypeDescriptionProvider
{
    public AbstractControlDescriptionProvider()
        : base(TypeDescriptor.GetProvider(typeof(TAbstract)))
    {
    }

    public override Type GetReflectionType(Type objectType, object instance)
    {
        if (objectType.FullName == typeof(TAbstract).FullName)  // corrected condition here (original condition was incorrectly giving false in my case sometimes)
            return typeof(TBase);

        return base.GetReflectionType(objectType, instance);
    }

    public override object CreateInstance(IServiceProvider provider, Type objectType, Type[] argTypes, object[] args)
    {
        if (objectType.FullName == typeof(TAbstract).FullName)  // corrected condition here (original condition was incorrectly giving false in my case sometimes)
            objectType = typeof(TBase);

        return base.CreateInstance(provider, objectType, argTypes, args);
    }
}

就这样!

您不必向基于您的baseform的表单的未来开发人员解释任何内容,他们也不必做任何设计表单的技巧!我认为这是最干净的解决方案(除了控制重新定位)。

还有一个提示:

如果出于某种原因,设计人员仍然拒绝为您工作,您可以在代码文件中将public class Form1 : BaseForm更改为public class Form1 : BaseFormMiddle1(或BaseFormMiddle2),在vs表单设计人员中编辑它,然后再次更改它。比起条件编译,我更喜欢这种技巧,因为它不太可能忘记并发布错误的版本。


由于抽象类public abstract class BaseForm: Form给出了一个错误并避免使用设计器,所以我附带使用了虚拟成员。基本上,我不是声明抽象方法,而是尽可能用最小的主体声明虚拟方法。以下是我所做的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DataForm : Form {
    protected virtual void displayFields() {}
}

public partial class Form1 : DataForm {
    protected override void displayFields() { /* Do the stuff needed for Form1. */ }
    ...
}

public partial class Form2 : DataForm {
    protected override void displayFields() { /* Do the stuff needed for Form2. */ }
    ...
}

/* Do this for all classes that inherit from DataForm. */

由于DataForm应该是抽象成员displayFields的抽象类,所以我用虚拟成员"伪造"这种行为以避免抽象。设计师不再抱怨了,我觉得一切都很好。

这是一个更具可读性的解决方案,但由于它不是抽象的,所以我必须确保DataForm的所有子类都有displayFields的实现。因此,使用此技术时要小心。


Windows窗体设计器正在创建窗体/控件的基类实例,并应用InitializeComponent的分析结果。这就是为什么您可以设计由项目向导创建的表单,而不必构建项目。由于这种行为,您也无法设计从抽象类派生的控件。

您可以实现这些抽象方法,并在设计器中没有运行异常时引发异常。从控件派生的程序员必须提供不调用基类实现的实现。否则程序会崩溃。


您只需在abstract关键字中有条件地编译,而无需插入单独的类:

1
2
3
4
5
6
7
8
9
10
11
#if DEBUG
  // Visual Studio 2008 designer complains when a form inherits from an
  // abstract base class
  public class BaseForm: Form {
#else
  // For production do it the *RIGHT* way.
  public abstract class BaseForm: Form {
#endif

    // Body of BaseForm goes here
  }

如果BaseForm没有任何抽象方法(因此,abstract关键字只防止类的运行时实例化),则可以进行此工作。