如何让Visual Studio 2008的Windows Forms设计器呈现实现抽象基类的表单?

104

我在使用Windows Forms中继承控件时遇到了问题,需要一些建议。

我为自制GUI列表中的项目使用了一个基类以及一些针对每种类型数据的继承控件。

一开始没有问题,但现在发现将基类控件变成抽象类是正确的决定,因为它有一些方法需要在所有继承控件中实现并从基类控件内部的代码中调用,但不能在基类中实现。

当我将基类控件标记为抽象类时,Visual Studio 2008设计工具无法加载窗口。

有没有办法让设计工具与抽象化的基类控件配合工作?

10个回答

102

我知道一定有方法可以做到这一点(我找到了一种干净的方法)。Sheng的解决方案正是我想到的一个临时解决办法,但是一个朋友指出Form类最终会继承自一个抽象类,我们应该能够完成这个任务。如果他们能做到,我们也能做到。

我们从这段代码到这个问题。

Form1 : Form

问题

public class Form1 : BaseForm
...
public abstract class BaseForm : Form

这正是最初的问题所在。如前所述,一位朋友指出System.Windows.Forms.Form实现了一个抽象的基类。我们能够找到...

更好解决方案的证明

由此,我们知道设计器能够显示一个实现了基本抽象类的类,但它不能显示一个立即实现基本抽象类的设计器类。中间最多只有5个抽象层级,但我们测试了1个抽象层级并最初提出了这个解决方案。

最初解决方案

public class Form1 : MiddleClass
...
public class MiddleClass : BaseForm
... 
public abstract class BaseForm : Form
... 

这个方法实际上是有效的,并且设计师可以正常渲染它,问题解决了...除非你在生产应用中有一个额外的继承级别,这只是因为winforms设计师的不足而必要!

这并不是百分之百的万无一失的解决方案,但它相当不错。基本上,您使用#if DEBUG来得出精炼的解决方案。

精炼的解决方案

Form1.cs

public class Form1
#if DEBUG
    : MiddleClass
#else 
    : BaseForm
#endif
...

MiddleClass.cs

public class MiddleClass : BaseForm
... 

BaseForm.cs

public abstract class BaseForm : Form
... 

这段代码的作用是只有在调试模式下才使用“初始解决方案”中概述的方法。其想法是您永远不会通过调试构建发布生产模式,并且您始终会在调试模式下进行设计。

设计者始终会针对以当前模式构建的代码运行,因此您不能在发布模式下使用设计者。但是,只要您在调试模式下设计并发布在发布模式下构建的代码,就可以正常运行。

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


3
你的类和抽象基类是否有无参构造函数?因为这是我们所需要添加的全部内容,才能让设计器显示从抽象表单继承而来的表单。 - nos
1
你的解决方案非常好。我只是不敢相信Visual Studio需要让你跳过这么多 hoops 来做一些如此普遍的事情。 - RB Davidson
@alexw 因为如果你这样做,就无法在基类中使用任何抽象方法或属性(在调试模式下编译失败)。 - kad81
1
但是如果我使用一个不是抽象类的middleClass,那么继承middleClass的人就不必再实现抽象方法,这违背了使用抽象类的初衷...如何解决这个问题? - Darius
1
@ti034 我找不到任何解决方法。所以我只是让中间类的抽象函数具有一些默认值,这些默认值可以很容易地提醒我去覆盖它们,而不必让编译器抛出错误。例如,如果应该是抽象方法返回页面的标题,则我将使其返回一个字符串“请更改标题”。 - Darius
显示剩余5条评论

84
@smelch,有一个更好的解决方案,而不必创建中间控件,即使是为了调试也是如此。 我们想要什么 首先,让我们定义最终类和基础抽象类。
public class MyControl : AbstractControl
...
public abstract class AbstractControl : UserControl // Also works for Form
...

现在我们需要的是一个描述提供者
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);
    }
}

最后,我们只需将TypeDescriptionProvider属性应用于抽象控件即可。
[TypeDescriptionProvider(typeof(AbstractControlDescriptionProvider<AbstractControl, UserControl>))]
public abstract class AbstractControl : UserControl
...

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

而且,提供者类可以应用于同一解决方案中我们想要的许多抽象基类。

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

<appSettings>
    <add key="EnableOptimizedDesignerReloading" value="false" />
</appSettings>

感谢 @user3057544 的建议。


此外请注意,在 AbstractControlDescriptionProvider 类中添加 XML 文档注释头(以 /// 开始的行)会破坏 VS 2022 中的设计器,和未使用属性标签的效果相同。

1
这对我也起作用了,因为我正在使用CF 3.5,没有TypeDescriptionProvider - Adrian Botor
4
在VS 2010中我无法让这个工作起来,尽管smelch的可以。有人知道为什么吗? - RobC
6
设计师因某些原因有点不高兴。我发现在实施这个修复后,我必须清理解决方案,关闭并重新启动VS2010,然后重新构建;只有这样才能让我设计子类。 - Oblivious Sage
4
值得注意的是,由于这个修复替换了抽象类的一个基类实例,所以在为子类设计时,在设计师中添加到抽象类的可视元素将不会可用。 - Oblivious Sage
1
这对我有用,但在构建项目后,我必须先重新启动VS 2013。@ObliviousSage - 感谢您的提醒;至少在我的当前情况下,这不是一个问题,但仍然需要注意。 - InteXX
显示剩余11条评论

11

@Smelch,感谢您的有用答案,因为我最近也遇到了同样的问题。

以下是对您的帖子进行的轻微更改,以防止编译警告(通过将基类放在#if DEBUG预处理器指令中):

public class Form1
#if DEBUG  
 : MiddleClass 
#else  
 : BaseForm 
#endif 

6

针对那些认为Juan Carlos Diaz的TypeDescriptionProvider无法使用且不喜欢条件编译的人,我提供了一些小技巧:

首先,你可能需要重新启动Visual Studio,这样你代码中的更改才能在表单设计器中生效(我不得不这样做,简单的重建并不能解决问题 - 或者并非每次都有效)。

现在我会介绍我的解决方案,以抽象基础Form为例。假设你有一个BaseForm类,并希望任何基于它的窗体都可以进行设计(这将是Form1)。像Juan Carlos Diaz所展示的TypeDescriptionProvider对我也没有用。以下是我如何使其工作的方法,结合了smelch的MiddleClass解决方案,但没有使用#if DEBUG的条件编译,并进行了一些修正:

[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窗体设计器工作。然后将第二个中间类分配给BaseForm的TypeDescriptionProvider。无条件编译。 我遇到了另外两个问题:
问题1:在更改设计器中的Form1(或某些代码)后,再次尝试在设计器中打开它时会再次出现错误。
问题2:当在设计器中更改Form1的大小并关闭并重新打开该表单时,BaseForm的控件位置不正确。
第一个问题,我通过在TypeDescriptionProvider中比较类型名称(FullName)而不是比较类型(请参见下文)来解决它。
至于第二个问题,我不知道为什么BaseForm的控件在Form1类中不可设计,并且它们的位置在调整大小后会丢失,但我已经解决了它。我只是手动将BaseForm的按钮(应位于右下角)移动到其正确位置,方法是从BaseForm的Load事件异步调用:BeginInvoke(new Action(CorrectLayout)); 我的基类只有“OK”和“Cancel”按钮,所以情况很简单。
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版本:

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表单设计器中进行编辑,然后再将其改回。与条件编译相比,我更喜欢这个技巧,因为它不太可能忘记并发布错误的版本


1
这解决了我在VS 2013中使用Juan的解决方案时遇到的问题;重新启动VS后,控件现在可以一致地加载了。 - Luke Merrett

5

我曾遇到类似的问题,但找到了一种重构方法,用接口替代抽象基类:

interface Base {....}

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

这可能并不适用于每种情况,但在可行的情况下,它比条件编译产生了更干净的解决方案。

1
你能提供更完整的代码示例吗?我正在努力更好地理解你的设计,我也将把它翻译成VB。谢谢。 - InteXX
我知道这个方法有点老,但我发现这是最不破坏原有结构的解决方案。由于我仍然希望我的接口与UserControl相关联,因此我在接口中添加了一个“UserControl”属性,并在需要直接访问它时引用它。在我的接口实现中,我扩展了UserControl并将“UserControl”属性设置为“this”。 - chanban

3
我正在使用这个答案中的解决方案来回答另一个问题,该答案链接了这篇文章。该文章建议使用自定义TypeDescriptionProvider和抽象类的具体实现。设计师将询问自定义提供程序要使用哪些类型,您的代码可以返回具体类,以便设计师满意,同时您完全控制抽象类如何显示为具体类。
更新:我在回答那个问题时包含了一个有文档记录的代码示例。那里的代码确实可行,但有时我必须按照我的答案中所述进行清理/构建循环才能使其正常工作。

3

我有一个关于Juan Carlos Diaz解决方案的提示。它对我非常有效,但也存在一些问题。当我启动VS并进入设计模式时,一切都很好。但是在运行解决方案、停止和退出后,再尝试进入设计模式,异常就会不断出现,直到重新启动VS。 但是我找到了解决方法 - 只需将以下内容添加到您的app.config中即可。

  <appSettings>
   <add key="EnableOptimizedDesignerReloading" value="false" />
  </appSettings>

2

由于抽象类public abstract class BaseForm: Form会出现错误并避免使用设计器,我采用了虚成员的方法。基本上,我没有声明抽象方法,而是声明了尽可能少的具有最小体量的虚拟方法。以下是我所做的:

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 实现。因此,在使用此技术时要小心。


这就是我采用的方法。我只是在基类中抛出NotImplementedException异常,以便在忘记时使错误明显。 - Shaun Rowan

1

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

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


有点遗憾,但目前还是这样做的。希望能找到正确的方法来解决这个问题。 - Oliver Friedrich
有更好的方法,请看 Smelch 的答案。 - Allen Rice

-1

您可以在不插入单独的类的情况下有条件地编译abstract关键字:

#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关键字仅防止类的运行时实例化),这个方法就可以工作。


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