用户控件作为接口,在设计器中可见

7
我们有一个C# WinForms项目,其中包含一个包含大量UserControl的表单。每个UserControl自然会公开所有UserControl方法、属性等,以及其自身特定的成员。
我一直在考虑通过接口访问它们是减少处理这些UserControl复杂性的一种方式。因此,在构造函数中,不是拖放将UserControl放在表单上,而是像这样:
public class MyGiantForm
{
    ICustomerName cName;

    public MyForm()
    {
        InitializeComponent();

        var uc = new SomeCustomerNameUserControl();
        this.Controls.Add(uc);
        cName = uc;
    }
}

SomeCustomerNameUserControl 实现了 ICustomerName 接口,并且 ICustomerName 包含我真正关心的具体属性(比如 FirstNameLastName)。通过这种方式,我可以通过 cName 成员引用 UserControl,而不是被所有 UserControl 成员淹没,我只得到那些在 ICustomerName 中的成员。

一切都很好,但问题在于,如果我这样做,就看不到 SomeCustomerNameUserControl 在设计器中。有人知道我怎么才能这样做,同时仍然在表单设计界面上看到这个 UserControl 吗?

编辑:一种不太复杂的方法是将控件放在一个基本窗体上。默认情况下(在 C# 中),控件成员是私有的。然后为每个控件创建一个属性,通过接口将其公开。

但是,即使有更复杂的方法,我也对其他方法感兴趣。似乎可以使用 IDesignerHost 来实现它,但我找不到任何适用的示例。


请问您能否给我们一个期望结果的例子吗?一旦我们找到解决方案,您的代码会是什么样子? - DonkeyMaster
结果将是自定义UserControl在表单上可见,并且代码中有一个ICustomerName类型的成员,但代码中没有SomeCustomerNameUserControl类型的成员。如果不够清楚,请告诉我需要详细说明的地方。 - Ryan Lundy
好的,在阅读了你的解释和其他答案之后,我理解了你的问题。嘿,这就像“危险边缘”游戏! - DonkeyMaster
@Kyralessa,我为您提供了一种可能的解决方案,使用了Form类上的扩展方法。希望能对您有所帮助! - Joseph
8个回答

6
如果 SomeCustomerNameUserControl 被定义如下:
class SomeCustomerNameUserControl : UserControl, ICustomerName
{
}

您仍然可以在设计器中拖放此控件(这将创建一个名为someCustomerNameUserControl1的控件),并在需要时执行以下操作:

ICustomerName cName = someCustomerNameUserControl1;

也许我有所遗漏,但我认为这很简单。

1
除非我们缺少某些细节,否则我认为这很简单... 这就是我要说的。 - Brian ONeil
这并没有回答问题。我希望ICustomerName成为访问UserControl变量的唯一选项。这样开发人员就不必“仅仅记住”进行强制转换。我已经编辑了问题,以指示一种实现方式,但我正在寻找其他不需要基本表单的方法。 - Ryan Lundy

6
有一种方法可以实现你想要的--隐藏你不想看到的成员--但是让它自动应用,而不需要其他人在使用自定义界面方面进行合作。你可以通过重新引入所有你不想看到的成员,并给它们打上属性标签来实现。
这就是Windows Forms在某些情况下所做的事情,例如,基类属性对于特定的派生类没有意义。例如,Control有一个Text属性,但是在TabControl上,Text属性是无意义的。因此,TabControl覆盖了Text属性,并添加了属性覆盖的属性,说明“顺便说一下,在属性网格或Intellisense中不要显示我的Text属性。”属性仍然存在,但由于你从未看到它,它不会妨碍你。
如果你为成员(属性或方法)添加[EditorBrowsable(EditorBrowsableState.Never)]属性,则Intellisense将不再在其代码完成列表中显示该成员。如果我正确理解你的问题,这是你试图实现的重要事情:使应用程序代码难以意外地使用该成员。
对于属性,你可能还想添加[Browsable(false)]来隐藏属性从属性网格中,以及[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]以防止设计器将属性值写入.designer.cs文件。
这将使意外使用方法/属性变得非常困难。但是它们仍然不能保证。如果你需要保证,那么再加上一个[Obsolete]属性,并使用“将警告视为错误”进行构建--然后你就会被照顾到了。
如果基本成员是虚拟的,你可能想要覆盖它,并让你的覆盖只是调用基类。不要抛出异常,因为在正常情况下,基类将可能调用覆盖的成员。另一方面,如果基本成员不是虚拟的,那么你想使用“new”而不是“override”,你可以决定你的实现是否应该调用基类,或者只是抛出异常——没有人应该使用你重新引入的成员,所以这并不重要。
public class Widget : UserControl
{
    // The Text property is virtual in the base Control class.
    // Override and call base.
    [EditorBrowsable(EditorBrowsableState.Never)]
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [Obsolete("The Text property does not apply to the Widget class.")]
    public override string Text
    {
        get { return base.Text; }
        set { base.Text = value; }
    }

    // The CanFocus property is non-virtual in the base Control class.
    // Reintroduce with new, and throw if anyone dares to call it.
    [EditorBrowsable(EditorBrowsableState.Never)]
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [Obsolete("The CanFocus property does not apply to the Widget class.")]
    public new bool CanFocus
    {
        get { throw new NotSupportedException(); }
    }

    // The Hide method is non-virtual in the base Control class.
    // Note that Browsable and DesignerSerializationVisibility are
    // not needed for methods, only properties.
    [EditorBrowsable(EditorBrowsableState.Never)]
    [Obsolete("The Hide method does not apply to the Widget class.")]
    public new void Hide()
    {
        throw new NotSupportedException();
    }
}

是的,这是相当多的工作,但你只需要每个成员、每个类做一次……嗯,对。但是,如果那些基类成员确实不适用于你的类,并且让它们存在会导致混乱,那么付出努力可能是值得的。


1
实际上,对于任何UserControl,似乎我只需要这样做一次,然后在UserControl和我的各种派生控件之间的继承树中插入这个新的成员隐藏类。如果是这样的话,那么这不会是一项难以忍受的工作量。另一方面,如果你真的不必这样做,任何工作量都太多了。 - Ryan Lundy

4

我希望ICustomerName成为访问UserControl变量的唯一选项。这样一来,开发者就不必“仅仅记得”进行强制转换。

您面临的问题是,您的表单和它托管的控件有两种完全不同的用途。Visual Studio或Winforms中没有解决此问题的巧妙方法。虽然可能有可能实现,但有一种更清晰、面向对象的方法可以分离与控件交互的两种方法。

如果您想隐藏这些对象从UserControl继承的事实,并只想将它们视为IDoSomeThingYouShouldDealWith,则需要将处理表示层关注点(设计器+UI逻辑)的逻辑与业务逻辑分开。

您的表单类应该将控件视为UserControls,包括停靠、锚定等等,这里没有特别之处。您应该将所有需要处理ICustomerName.FirstName等逻辑的内容放入一个完全独立的类中。这个类不关心字体和布局,它只知道有另一个实例可以正确地呈现客户姓名;或者DateTime作为“选择出生日期”的控件等等。

这是一个非常糟糕的例子,但我现在必须离开。您应该能够在这里详细了解所涵盖的思路:

public interface ICustomerName
    {
        void ShowName(string theName);
    }

    public partial class Form1 : Form, ICustomerName
    {
        public Form1()
        {
            InitializeComponent();
        }

        #region ICustomerName Members

        public void ShowName(string theName)
        {
            //Gets all controls that show customer names and sets the Text propert  
            //totheName
        }

        #endregion
    }

    //developers program logic into this class 
    public class Form1Controller
    {
        public Form1Controller(ICustomerName theForm) //only sees ICustomerName methods
        {
            //Look, i can't even see the Form object from here
            theForm.ShowName("Amazing Name");
        }
    }

Noel,我对Model-View-Controller、Model-View-Presenter等都很了解。但是我暂时没有自由改变应用程序。我正在寻找适用于现有应用程序的解决方案。这里已经列出了几种可能性,可以以不同程度的完整性和易用性实现我想要的功能。如果我在这里寻找的东西很容易做到,我早就想出如何做了,也不会在StackOverflow上发帖询问。 - Ryan Lundy
嗨Kyralessa,谢谢您的反馈。不确定为什么您正在寻找更复杂的解决方案,您肯定会同意,这些都是某种程度上的黑客或变通方法。如果您能够(我接受您可能没有自由),我会尝试找到一种重构UI模式的方法,即使您不能立即转向该模式。例如,您可以在视图的构造函数中创建控制器,这与Joe建议的不那么远。您还可以逐步将功能和逻辑移动到控制器中,以便没有大爆炸。 - Noel Kennedy
这是一个工作项目,而不是个人项目,在工作项目中,人们并不总是有自由去重构我们想要的内容。我并不特别寻求复杂的解决方案。我寻求的是一个好的解决方案,如果它很复杂但是好用,那也没关系。我还在寻找一个非常具体的问题的解决方案。我的问题不是“如何修复这个应用程序?”而是“如何通过接口访问UserControls,但仍然让它们出现在设计器中?” - Ryan Lundy

3

在使用设计器添加UserControl后,您可以在属性窗口中将GenerateMember设置为false以抑制成员的生成。

然后,您可以在构造函数中使用其他技术来赋值cName引用,例如:

foreach(Control control in this.Controls)
{
    cName = control as ICustomerName;
    if (cName != null) break;
}

cName将是对UserControl唯一的引用。


这是我看到的第一个好的可能性。GenerateMember甚至没有出现在我的脑海中。在我看来,比你描述的方法更好的方式是使用:cName = this.Controls["controlName"] as ICustomerName;然而,它明显存在一个弱点,如果控件被重命名,就会出现问题,所以这仍然不是一个完美的解决方案。我不喜欢你提到的类型转换解决方案的原因是,在同一个窗体上可能会有多个相同的客户UserControl。事实上,在我正在处理的项目中,我知道至少有一个明确的情况是这样的。 - Ryan Lundy
是的,这只是一种从控件集合中获取所需引用的技巧的示例。如果您有多个控件实例,那么需要使用控件的其他特征,例如名称,来区分它们。最好的解决方案取决于您的应用程序。 - Joe
回过头来看这个问题,我突然想到可以使用您的方法而无需将控件名称用引号括起来,并且不会因为多个控件实现相同接口而混淆。一种方法是创建我的接口(例如IStuff),然后在表单上的“接口控件”中每个都实现一个继承自IStuff的接口(没有其他成员)。然后我可以使用上面的代码与这些特定接口一起访问各种控件。 - Ryan Lundy

1
你可以编写一个扩展方法,使您能够返回实现接口的表单上的任何控件。
public static class FormExtensions
{
    public static IDictionary<string, T> GetControlsOf<T>(this Form form) 
           where T: class
    {
        var result = new Dictionary<string, T>();
        foreach (var control in form.Controls)
        {
            if ((control as T) != null)
                result.Add((control as T).Tag, control as T);
        }
        return result;
    }
}

然后在您的表单中,您可以通过以下方式随时调用它:

this.GetControlsOf<ICustomerName>()["NameOfControlHere"];

如果它返回了多个用户控件,你需要以某种方式处理它,例如通过在接口中添加标签属性来唯一跟踪每个用户控件或其他方式。
public partial class UserControl1 : UserControl, ICustomerName
{
     public string Tag { get { return this.Name; } }
}

然后您可以从设计器中将用户控件拖放到表单上。标签始终返回控件的名称,这将允许您通过IDictionary接口直接访问控件。您的开发人员可以在控件名称中放置任何唯一标识符,并且它将传递到接口。

此外,应注意,此方法还将允许您在解决方案中的所有表单上使用此方法。

您需要做的唯一其他事情就是将GenerateMember设置为false。


非常有趣。它与乔的解决方案类似,但更加优雅。它具有与他相同的弱点(多个具有相同接口的控件),但我认为传递UserControl名称将缓解这一问题,尽管如果控件名称更改仍然是一个问题。 - Ryan Lundy
@Kyralessa 实际上,多个控件没有问题,您可以使用字典,根据控件的名称进行索引。然后,您可以完全删除额外的标记。我将修改我的示例以说明。 - Joseph
如果你要使用Tag返回Name属性,为什么不直接使用Name? - Ryan Lundy
因为Name属性不是接口的一部分。然而,如果你只想将其用于字典目的,那么你可以完全舍弃Tag属性。请注意,如果你像我上面所示的那样将其用作只读属性,它将不会在设计器中显示,所以你可以指导开发人员仅使用Name属性,一切都会正常工作。 - Joseph

0

你也可以像Bob说的那样,在构造函数中分配所有成员变量,这样你就可以在一个地方拥有它了。


这仍然让程序员有可能不经意地使用 UserControl 引用而不是接口引用。如果可能的话,我希望接口引用成为唯一引用 UserControl 的成员。 - Ryan Lundy

0

你似乎想要实现中介者模式。不必直接处理每个控件,而是通过中介者与它们交互。每个中介者都定义了你想从每个控件看到的简洁接口。这将通过使你的设计更加明确和简洁来减少总体复杂性。例如,你不需要一个控件上可用的20个属性和50个方法。相反,你将与该控件的中介者打交道,该中介者定义了你真正关心的2个属性和5个方法。所有内容仍将显示在设计器中,但应用程序的其他部分将不会与这些控件交互,而是与中介者交互。

这种方法的一个重大优点是它极大地简化了维护工作。如果你决定重写MyCrappyUserControl,因为实现有问题,你只需要更新该控件的中介者类。所有与该控件交互的其他类都是通过中介者进行交互,并且不需要更改。

最终,这取决于纪律:你和你的团队需要有足够的纪律使用中介者/接口/任何东西,而不是直接操作控件。如果你的团队纪律水平较低,可以由一位领导程序员进行代码审查。


1
我正在寻找一种不需要太多纪律的解决方案,因为我知道当一个解决方案需要程序员付出纪律时会发生什么。我们已经有太多要考虑和记住的事情了;一个“你只需要记住”的解决方案并不是真正的解决方案。 - Ryan Lundy
这只是您的商店生产代码的标准设计的一部分。如果您在向程序员介绍设计之前就让他们进入代码,那么UserControls暴露过多属性的问题将成为您最不用担心的问题。 - Mike Post
兄弟,我不是负责人。我不能强制别人遵守编码标准。我正在尝试找到一种微妙但有效的方法来改善设计,而不是强迫人们按照某种方式做事情。也许是通过身教而非命令来领导。 - Ryan Lundy
并非每个编程问题都有技术解决方案。有时候正确的答案是人员解决方案。 - Mike Post
我总体上同意。但是这个编程问题有很多技术解决方案。有些比其他的更好。 - Ryan Lundy

-3
假设MyUserControl是这样定义的:
class MyUserControl : UserControl, IMyInterface
{
    // ...
}

然后在你的表单中,你应该有类似这样的内容:

public class MyForm : Form
{
    IMyInterface cName;

    public MyForm()
    {
        InitializeComponent();

        cName = new MyUserControl();
        Controls.Add((UserControl)cName);
    }
}

这样,cName就是访问我们用户控件实例的唯一方法。


1
这样做不能让你在设计器中看到MyUserControl。 - Mike Post
1
如果您将此代码放入设计器文件中,您将能够看到控件。问题在于,如果重新生成,您的更改将会丢失。可以使用宏来解决这个问题。 - M. Jahedbozorgan
这正是我在原问题中提出的解决方案。因此,它根本没有帮助。 - Ryan Lundy

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