65.9K
CodeProject 正在变化。 阅读更多。
Home

利用 .NET 组件和 IDE 集成:MVC 用例中的 UI AOP

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (119投票s)

2005 年 5 月 25 日

98分钟阅读

viewsIcon

368438

downloadIcon

835

深入探讨 .NET 组件模型架构的功能和强大之处,其与 IDE 在设计时集成,以及通过运行时扩展所带来的可能性。

目录

引言

微软将 .NET 定位为一个非常适合组件开发的平台。这一论断背后是一种全新的架构,它确实使其远不止是营销噱头。微软表明,它已经从其先前的面向组件的架构(COM/ActiveX/OLE)中吸取了大量教训,并为专业组件的开发者提供了一套全面的功能(包括设计时在 VS.NET IDE 中,以及运行时),从而使它们更容易开发,同时又更加强大和有用。

那么,“软件组件”到底是什么?通过本文,我们将揭示 .NET 和 VS.NET 世界中的组件真正是什么,并将专门讨论 IDE 为它们提供的先进功能,使我们能够创建有效的专业组件,从而大大提高程序员的生产力,增加关注点的分离,并在公司内部强制执行设计模式。

我们将尽可能多地利用先进功能,并在它们之上构建一个模型-视图-控制器框架。同时,我们将看到一些在开发高端组件化架构时通常必需的细微的缺点和技巧。这是一个原型应用程序开发模型,可以轻松完成和扩展,以在其之上构建生产质量的应用程序。

在本文的过程中,我们将讨论

  • .NET 和 VS.NET 对组件的设想:构建块以及它们如何协同工作。
  • 设计时架构。
  • MVC 模式:分离关注点和组件职责。简要概述和我们提出的架构。
  • 面向方面编程 (AOP):通过 VS.NET 架构扩展现有组件的新功能。如何在不进行继承或组合的情况下实现。
  • 组件与 IDE 的集成:通过属性浏览器和设计器。
  • 利用宿主(IDE)提供的服务。
  • 如何控制代码生成。
  • 增加组件重用的设计模式:使组件跨技术(Web 和 Windows 感知)。
  • 如何通过 VS.NET 架构提供自定义服务。
  • 在运行时扩展设计时基础设施。

在深入实现和代码之前,我们将从简要的架构概述开始。

面向组件的架构

让我们从定义组件开始

.NET 世界中的组件是任何直接或间接实现 System.ComponentModel.IComponent 接口的类。

这非常直接。现在让我们看一个简短的列表,列出 .NET 框架中一些具有这种意义的类(我们在本文其余部分将使用的类)

  • System.ComponentModel.ComponentIComponent 的基本实现,通常用作非可视化组件的基类。
  • System.Web.UI.Control:所有 ASP.NET 控件的基类,直接实现 IComponent
  • System.Windows.Forms.Control:继承自 System.ComponentModel.Component,是所有 Windows Forms 控件的基类。
  • System.Data.DataSet:此类继承自 System.ComponentModel.MarshalByValueComponent,后者又实现 IComponent
  • System.Diagnostics.EventLog:继承自 System.ComponentModel.Component

正如你所见,.NET 平台上的几乎所有东西都是组件。一个类成为组件的主要结果是 IDE 具有可用于它的功能。IDE 为组件提供服务的关键属性是 IComponent.Site。所谓的“有站点”组件是指被放置在 *容器* 中的组件。这种包含关系是通用的,与视觉包含无关。

例如,当一个 ASP.NET 服务器控件放置在 Web 窗体中时,它就被称为有站点的。它的 Site 属性(IComponent 接口实现的一部分)被设置为组件现在所处的宿主,在 VS.NET 中,这是一个 Microsoft.VisualStudio.Designer.Host.DesignSite 类的实例。当 Windows Forms 用户控件放置在窗体设计器中时,或者在设计时放置一个非可视化组件时,分配给 Site 属性的正是同一个对象类型。最后一个与前两者之间存在差异,当我们将组件视为严格意义上的组件(非可视化 IComponent 实现)时,我们将讨论这些差异。

Site 属性,类型为 ISite,包含允许组件与其他组件、其容器(逻辑容器)以及它提供的服务进行通信的成员。随着我们继续进行,我们将学习这些好处以及如何利用它们。所以整体架构是

容器是一个实现 System.ComponentModel.IContainer(以下简称 `IContainer`)接口的对象。在设计时,容器始终是 Microsoft.VisualStudio.Designer.Host.DesignerHost 的实例。该对象是 VS.NET IDE 组件功能的内核,因此让我们更深入地研究它。

托管组件

首先,要查看 DesignerHost 类,你需要一个使用反射并且能够显示程序集非公共成员的工具。其中一个工具是 Reflector,我强烈建议你如果还没有使用它,就尽快熟悉它。它是学习任何 .NET 程序集内部结构的一个无价的(且免费的)工具。你可以从 这里 下载。我们感兴趣的类位于 VS.NET 安装的 Common7\IDE 子文件夹中的 Microsoft.VisualStudio.dll 程序集中(默认是 C:\Program Files\Microsoft Visual Studio .NET\Common7\IDE)。你必须通过其“视图 - 自定义”菜单启用 Reflector 显示非公共成员。

该类(在 **许多** 其他接口中)实现了我们之前提到的 IContainer 接口,该接口是能够包含组件所必需的,并且间接实现了 System.ComponentModel.Design.IDesignerHost 接口(通过实现派生的 Microsoft.VisualStudio.Designer.IDesignerDocument 接口)。该接口提供了更高级别的服务,例如创建和销毁组件、访问与组件关联的设计器等。放置在实现此接口的对象内的(因此被包含在内)有站点的组件称为 *托管组件*,这意味着它可以利用它提供的服务。目前只有 DesignerHost 实现它,但其他 IDE 也可能选择这样做。

宿主包含了我们可以在组件中使用的绝大多数服务,这些服务可以通过组件的 Site 属性访问。目前,宿主包含以下服务:IDesignerHostIComponentChangeServiceIExtenderProviderServiceIContainerITypeDescriptorFilterServiceIMenuEditorServiceISelectionServiceIMenuCommandServiceIOleCommandTargetIHelpServiceIReferenceServiceIPropertyValueUIServiceManagedPropertiesService。正如你所见,有很多服务供我们的组件使用。我们将在本文中展示其中大部分的使用方法。

获取这些服务的入口点是 IServiceProvider 接口。DesignSiteDesignerHost 类都间接实现了此接口。

该接口只包含一个方法:GetService,它接收一个 Type 来指示要检索的服务。例如

IComponentChangeService ccs = (IComponentChangeService) 
host.GetService(typeof(IComponentChangeService));

目前,DesignSite(我们通过组件的 Site 属性访问)本身提供了两项服务:IDictionaryServiceIExtenderListService。对其他服务的请求会被转交给宿主。

VS.NET 宿主提供的这些服务不仅可以通过组件的 Site 属性直接访问,还可以从通过(属性)关联的类中访问,这些类为组件提供了设计时改进。这些其他类完成了架构。

设计时架构

除了组件/站点/容器架构之外,还有许多其他方面构成了功能丰富的平台,以提供改进的组件设计时支持。这对于 RAD 工具和提高程序员生产力非常重要。专业组件应为开发者提供丰富的设计时体验,如果它们要想成功。

这些附加功能通过 *属性* 添加到组件中。每种类型的属性都为组件分配了不同的设计时(其中一些也支持运行时)功能。IDE 在两个主要区域使用这些属性:设计图面及其与代码(后端)文件和属性浏览器的交互。请注意,*设计图面* 不仅包括 Windows 或 Web Forms 设计图面,还包括它们下方的组件区域,当非可视化组件被拖放到设计器上时,该区域会显示出来。

大多数设计时功能都包含在 System.Design.dll 程序集中。下图显示了属性的类型及其在设计时的用法。

在此图中,我们没有显示更基本的属性,如 DescriptionAttributeCategoryAttribute 等,它们提供主要由属性浏览器使用的有限功能。但是,还有一些其他属性为组件添加了更复杂和有用的特性,我们也没有显示,因为它们非常具体。随着我们转向更高级的场景,我们将介绍它们。

这三个属性中的每一个(以及关联的类)都会将另一个相关的类与组件关联起来。DesignerAttribute 将直接或间接实现 System.ComponentModel.Design.IDesigner 的类(例如 System.ComponentModel.Design.ComponentDesignerSystem.Web.UI.Design.HtmlControlDesignerSystem.Windows.Forms.Design.ControlDesigner)与组件关联起来。TypeConverterAttribute 对派生自 System.ComponentModel.TypeConverter 的类做同样的事情,而 EditorAttribute 则对派生自 System.Drawing.Design.UITypeEditor 的类做同样的事情(是的,这个命名空间位置对它来说有点奇怪!)。

通常,作者认为错误的是,这些属性根据它们提供的设计时增强级别进行分类,如下面的分类:

  • 基本:图中未涵盖的属性。
  • 中级:TypeConverterEditor 属性。
  • 高级:Designer 属性。

我们不认同这种分类,因为许多属性允许我们提供简单和高级的设计时功能。这一点在本篇文章结束时会很清楚。

这三个属性(以及关联的类)提供以下功能:

  • Designer:与设计器宿主交互以提供各种设计功能。可以在 System.ComponentModel.Design.ComponentDesigner 类中找到设计师提供的通用功能,它通常是任何设计器的基类。有一些成员可以通知宿主有关组件的更改以及过滤掉宿主和属性浏览器将看到的属性,例如。

    更具体的功能取决于设计器的类型,而设计器的类型又取决于设计器所附着的组件的类型。因此,System.Windows.Forms.Design.ControlDesigner(所有 Windows Forms 控件的基类)处理控件与其子控件之间的挂钩、拖放操作、鼠标事件等。另一方面,System.Web.UI.Design.ControlDesigner(所有 Web Forms 控件的基类)负责发出在设计时显示控件的 HTML,以及将控件状态持久化到页面源(* .aspx* 文件),显示错误等。

    通常,设计器处理适用于组件在设计时整个生命周期的功能,并且它们是架构中最灵活的部分。

  • Editor:为编辑组件的属性提供自定义用户界面,并可以选择性地在显示它的属性浏览器单元中绘制属性值。每当一个具有关联编辑器的属性即将被更改时,就可以通过属性浏览器访问它。编辑器的示例是 System.Drawing.Design.FontEditor,当您单击类型为 System.Drawing.Font 的属性旁边的省略号时会显示它;或者 System.Drawing.Design.ColorEditor,它提供一个颜色选择对话框。

    您可能会注意到这两种编辑器类型不同:FontEditor 显示为模态对话框(例如,在 Windows Forms 控件属性中),而 ColorEditor(在 Windows 和 Web 控件中)显示为下拉控件(也像 AnchorEditor)。

  • TypeConverter:这是迄今为止最难分类和描述的部件,因为许多功能都属于它。首先,它提供了大量方法,其中许多是虚拟的,用于将对象转换为其他类型以及从其他类型转换回来。这种转换主要由属性浏览器用于与对象的字符串表示形式相互转换,该表示形式用于显示属性值。但我们将看到其他转换可能适用,甚至会影响组件的代码序列化。

    它还可以提供一个值列表供属性选择,该列表以下拉模式显示。您可能会想,这与“Converter”这个词有什么关系。我也这么想。也许这个功能应该放在 Editor 里……

    最后,我们可以过滤将在属性浏览器中显示的属性列表。这可能很有用,例如,当您想根据自己创建的自定义属性过滤可编辑属性时(而不是默认的 BrowsableAttribute)。这是一个高级案例,但您会注意到此功能也可通过 ComponentDesigner(几乎所有设计器的基类)获得,它允许预/后过滤属性、事件和属性。所以我也很想知道这个功能为什么在这里……

TypeConverterEditor 属性可以应用于单个属性或直接应用于类型。在后一种情况下,任何具有该类型属性的对象都将自动附加编辑器/转换器。

设计器的一些功能也由属性浏览器使用,例如我们稍后将与每个属性及其用法的示例一起讨论的 DesignerVerbs

根组件和设计器

一些类会显示一个设计图面,我们可以在其中放置其他组件。Web 窗体(Page 类)、Windows 窗体或任何 Component 继承类(一般情况下)就是如此。您会注意到,对于您自定义的类或 ASP.NET 自定义控件,情况并非如此。两个属性的组合使得 IDE 可以为一个类提供设计图面。

  • DesignerCategoryAttribute:属性的构造函数必须指定“Component”类别。
    [System.ComponentModel.DesignerCategory("Component")]
  • DesignerAttribute:必须传递采用基设计器类型的重载,并且设计器必须实现 IRootDesigner
    [System.ComponentModel.Designer(typeof(MyRootDesigner), 
             typeof(System.ComponentModel.Design.IRootDesigner)]

IComponent 接口实现了最后一个属性。因此,任何直接或间接实现此接口的类,如果具有相应的类别,将拥有我们在双击组件类时看到的所有默认设计图面。Component 类是一个示例。ASP.NET 自定义控件的情况是,基类 System.Web.UI.Control 指定了“Code”的 DesignCategory,这就是为什么它们“不可设计”(我希望现在是)的原因。

当在解决方案资源管理器中选择一个项进行编辑时,如果它具有相应的类别和根设计器,IDE 将实例化根设计器并向用户显示设计图面。它还将创建一个组件实例,并使其成为设计器宿主的根组件。例如,当在设计视图中打开一个带有某些子组件的 Web 窗体页面时,对象之间的关系是这样的:

以下伪代码显示了 Page 类的声明以及导致此行为的属性:

[Designer(typeof(WebFormDesigner), typeof(IRootDesigner))]
public class Page

WebFormsDesigner 类位于与我们之前提到的 Microsoft.VisualStudio.dll 相同的文件夹中的 Microsoft.VSDesigner.dll 程序集中。该设计器(除其他外)继承自 System.ComponentModel.Design.ComponentDesigner,它是大多数设计器的基类,并且是 IDesigner 接口的默认实现。

对于 Windows Forms 组件,也会发生类似的过程。

IComponent 包含以下属性声明:

[Designer(typeof(ComponentDocumentDesigner), typeof(IRootDesigner)]
[Designer(typeof(ComponentDesigner)]
public interface IComponent

如前所述,第一个属性定义了当组件是根组件时将处理设计图面显示的类。其他设计器指定当组件放置在另一个组件(例如 Web 窗体)内时组件将提供的行为。

我们到目前为止讨论的所有架构都有一个主要目标,即通过增强设计时体验来提高程序员的生产力。有太多的功能需要探索,但它们只能在具体且高度集成的应用程序的上下文中完全实现,而不是孤立的示例。为此,为了深入研究 IDE,我们将实现一个 MVC 框架,该框架允许 Web 和 Windows 应用程序共享通用的代码库,并将可视化程序员/设计器与业务对象的复杂性隔离开来。该框架将主要基于非可视化组件,但我们将实现的大多数 IDE 集成功能对于可视化控件同样适用。

随着我们继续实现,我们将重新审视此初始架构概述中的许多概念,并将它们置于具体代码的上下文中。如果您已经熟悉 MVC 设计模式,请随时跳过下一节。它不旨在全面解释该模式,而只是一个介绍,让您可以继续进行实现。

MVC:模型-视图-控制器设计模式

在该模式下,应用程序中有三个非常清晰分离的关注点:

  • 模型:这是架构中保存您的业务实体及其行为数据的部分。例如,这是唯一负责访问数据库执行某些操作的部分。
  • 视图:这是显示(或输出)模型信息的部件。通常由窗体及其控件表示。
  • 控制器:视图和模型之间的所有交互都由控制器隔离。因此,当视图需要对模型执行操作时,它会请求控制器。当它需要显示模型数据时,它会向控制器请求模型。

这种分离使得一个松散耦合的架构,其中组件可以独立演进,并且其中一个组件的更改不会影响其他组件,可维护性大大提高。更重要的是,模型中的相同代码可以被不同的视图重用。并且根据控制器本身的编程方式,它也可以被重用。

我们将实现的架构将具有以下交互:

这个模式已经被实现了许多次并进行了调整,以至于一些纯粹主义者肯定会反对这“不是一个真正的” MVC。在更传统的概念中,视图负责以 *拉取* 的方式显示模型中的数据。在我们的实现中,控制器实际上将数据 *推送* 到视图。这在 Web 场景中更有效,而不会阻碍其在桌面应用程序中的适用性。

为了使此模型对 Windows Forms 和 Web Forms 视图都可行,我们实现了另一个模式——适配器,它将负责更新相应的 UI。

这种方法的好处是,相同的控制器可以在各种视图技术之间重用。

我们将开始实现,并踏上 IDE 功能的探索之旅,通过实现此难题的核心启用部分:控制器和视图之间的视图映射。

.NET 时代的 AOP

映射两个组件有很多方法,其中一种方法是使用包含数据的 XML 文件,另一种方法是将这些映射存储在数据库中。但是,它们两者(以及其他方法)都有一些重要的缺点:

  • 映射文件/存储成为另一个维护点。
  • 映射的加载/解析在高负载下成为一个问题。
  • 与通常的拖放控件集属性运行开发风格有显著的偏差。

一种避免这些问题的方法是扩展内置控件并在控件本身中实现映射配置。除了这项艰巨的任务(只需计算内置的 Web 和 Windows Forms 控件的数量),任何对映射功能的更改都需要修改控件的代码,这似乎不是一个好主意。除此之外,我们可能无法继承第三方获得的控件。

VS.NET 支持扩展器的概念,扩展器是一种组件,它从外部扩展现有组件的功能集,而无需继承、包含,甚至访问扩展组件的任何内部。这种扩展模式通常称为面向方面编程,因为它允许我们轻松地从外部为现有组件添加和删除方面(在本例中为映射)。这项技术有 自己的网站,互联网上也有很多关于它的文章。关于该主题的一篇有趣的文章可以在 MSDN 上找到,尽管它的方法采用了自定义属性的路径。

AOP 的组件化方法是 System.ComponentModel 命名空间中的 IExtenderProvider 接口和 ProvidePropertyAttribute 类。该属性必须应用于将要扩展其他组件的组件。

[ProvideProperty("WebViewMapping", typeof(System.Web.UI.Control))] 
public class BaseController : Component, IExtenderProvider

此属性告诉 VS.NET,组件(BaseController)将为根组件(Web 窗体)中的所有类型为 System.Web.UI.Control 的组件提供 WebViewMapping 属性。我们可以在接口的唯一方法实现中细化我们提供扩展器属性的控件。

bool IExtenderProvider.CanExtend(object extendee)
{
  return true;
}

在这里,我们说明我们始终支持 Control 继承的对象(此时 ProvideProperty 过滤器已经通过)。然而,映射对于根组件(即 Page 对象本身)没有意义,它也继承自 Control。为了验证此条件,我们可以利用 IDesignerHost 服务,该服务可以直接通过调用 GetService 方法从我们的组件访问,正如我们在开头所讨论的。

bool IExtenderProvider.CanExtend(object extendee)
{
  //Retrieve the service.

  IDesignerHost host = (IDesignerHost) GetService(typeof(IDesignerHost));
  //Never allow mappings at the root component level (Page or Form).

  if (extendee == host.RootComponent) 
  {
    return false;
  }
  else
  {
    return true;
  }
}

Component 类对 GetService 的实现只是将请求转发给 Component.Site 属性值对象,该对象正如我们所见,是 DesignSite,并且包含我们将在后续使用的一些其他服务。

需要注意的是,为了使此功能正常工作,接口和属性都必须实现。最后一部分是具有特定命名约定的几个方法,这些方法必须存在于扩展器组件中。

/// <summary>

/// Gets/Sets the view mapping that is used with the control.

/// </summary>

[Category("ClariuS MVC")]
[Description("Gets/Sets the view mapping that is used with the control.")]
public string GetWebViewMapping(object target)
{
  //Retrieve a mapping

}

/// <summary>

/// Sets the mapping that applies to this control.

/// </summary>

public void SetWebViewMapping(object target, string value)
{
  //Store the mapping

}

命名约定是:Get/Set + [在 ProvidePropertyAttribute 中使用的属性名]。GetXX 的返回值必须与 SetXX 方法的 value 参数匹配。如果我们只实现了到目前为止的代码(加上一个 private 字段来保存值),并且我们将一个 BaseController 组件拖放到 WebForm 上,我们将在属性浏览器中看到以下新属性,它附加到页面上的任何控件。

请注意,应用于 GetWebViewMapping 方法的 CategoryDescription 属性的使用方式与应用于普通属性的方式相同。对于所有常规属性属性,Get 始终是关键。

由于属性的状态保存在控件本身之外,存储在我们的组件内,因此这两个方法接收的 `target` 参数允许我们确定正在访问该属性的对象。我们通常会根据控件的唯一标识符维护一个包含配置属性的哈希表。此外,单个值通常不足以保存我们的信息,因此我们可以创建另一个类来保存设置。在我们的例子中,它是 ViewInfo 类。该类包含以下属性:ControlIDControlPropertyModelModelProperty。它们都是字符串,将用于同步模型值与配置的控件。如果该属性不是简单类型,我们可以通过为该类分配特定的类型转换器来提供改进的属性浏览器集成。

[TypeConverter(typeof(ExpandableObjectConverter))]
public class ViewInfo

此转换器在 System.ComponentModel 命名空间中提供,并导致属性浏览器显示属性如下:

属性可以直接在属性浏览器中展开和配置。属性名旁边显示的自定义字符串表示形式只是重写类 ToString 方法的问题。

public override string ToString()
{
  if (_controlproperty == String.Empty && 
    _model == String.Empty && _modelproperty == String.Empty)
    return "No mapping configured.";
  else
    return _model + "." + _modelproperty + " -> " + 
      _controlid + "." + _controlproperty;
}

为了访问当前控件的属性,我们可以使用以下代码:

public ViewInfo GetWebViewMapping(object target)
{
  return ConfiguredViews[((System.Web.UI.Control)target).ID] as ViewInfo;
}

public void SetWebViewMapping(object target, ViewInfo value)
{
  ConfiguredViews[((System.Web.UI.Control)target).ID] = value;
}

其中 ConfiguredViews 是一个类型为 Hashtable 的属性,用于保存映射。请注意,扩展属性就像 IDE 的另一个成员一样,并且在代码中也是如此。所以现在,IDE 将不知道如何持久化控件的新 WebViewMapping 属性。它也不知道如何持久化组件本身的 ConfiguredViews 属性。在下一节中,我们将讨论如何发出自定义代码来持久化这些值。为了告诉 VS.NET 在持久化(称为序列化)过程中忽略这些属性,我们添加以下属性:

[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]

此属性只需应用于 GetXX 方法,因为如上所述,该方法是关键的属性。

您可能已经注意到,我们使用的 ProvideProperty 属性专门指出我们正在扩展 Web 控件。如果具有以下属性,会不会很棒?

[ProvideProperty("ViewMapping", typeof(System.Web.UI.Control))] 
[ProvideProperty("ViewMapping", typeof(System.Windows.Forms.Control))] 
public class BaseController : Component, IExtenderProvider

嗯,这实际上是行不通的。即使我们可以同时拥有两者,第一个使用的扩展属性“获胜”。也就是说,如果我们打开一个带有控制器的 Windows Forms 并定义一些映射,Web Forms 控件将在整个 VS.NET 会话中不再看到扩展属性。我们将不得不关闭并重新打开 VS.NET 才能在 Web Forms 中再次获取扩展属性。但是,如果 Web Forms “获胜”,Windows Forms 就会失败。所以我们为每个实现 Get/Set:

[ProvideProperty("WinViewMapping", typeof(System.Windows.Forms.Control))]
[ProvideProperty("WebViewMapping", typeof(System.Web.UI.Control))]
public class BaseController : Component, IExtenderProvider

我们有效地实现了在不实际修改它们的情况下为现有控件添加功能。现在我们需要一种方法来持久化这些配置值,然后再继续,因为目前,一旦我们关闭窗体,所有值都会丢失。

自定义代码序列化:CodeDom 的强大功能

VS.NET 中的主要对象持久化机制是通过直接代码生成来处理的。您已经在所有 Web 和 Windows Forms 都包含的 InitializeComponent 方法中看到过这一点。它也存在于 Component 继承类中。两个属性决定了这种行为:System.ComponentModel.Design.Serialization.RootDesignerSerializerAttributeSystem.ComponentModel.Design.Serialization.DesignerSerializerAttribute。就像我们开头讨论的 DesignerAttribute 一样,根序列化器和普通序列化器之间存在区别。但与 DesignerAttribute 不同的是,普通(非根)序列化器 **始终** 使用,而根序列化器在组件同时是正在设计的根组件时另外使用。通常只自定义非根序列化器。一个指示是 IComponent 接口已经包含了根序列化器。

[RootDesignerSerializer(typeof(RootCodeDomSerializer), typeof(CodeDomSerializer))] 
public interface IComponent

但它没有提供指定常规序列化器的属性。相反,这个属性由 IComponent 的特定实现提供,例如:

[DesignerSerializer(typeof(Microsoft.VSDesigner.WebForms.ControlCodeDomSerializer)), 
                                                        typeof(CodeDomSerializer))] 
public class System.Web.UI.Control : IComponent

[DesignerSerializer(typeof(System.Windows.Forms.Design.ControlCodeDomSerializer)), 
      typeof(CodeDomSerializer))] 
      public class System.Windows.Forms.Control : Component

请注意,两者都有其唯一的序列化器,因为 Windows Forms 控件如何持久化到代码与 Web Forms 控件非常不同。前者将所有值和设置序列化到 InitializeComponent 方法,而后者仅将事件处理程序的附加项存储在代码隐藏中,因为控件属性本身就持久化在 aspx 页面中。

无论控件是用于 VB.NET 项目还是 C# 项目(或者任何其他 .NET 语言),InitializeComponent 始终以正确的语言生成代码。这得益于 .NET 中一项名为 *CodeDom* 的新功能。CodeDom 是一组类,允许我们编写表示最常见语言构造的对象层次结构,例如类型、字段和属性声明、事件附加、try...catch...finally 块等。它们允许我们构建所谓的 *抽象语法树*(AST),即目标代码。它是抽象的,因为它不代表 VB.NET 或 C# 代码,而是 *构造本身*。

序列化器交给 IDE 的是包含它们想要持久化的代码的 AST。IDE 反过来创建一个 System.CodeDom.Compiler.CodeDomProvider 继承类,该类匹配当前项目,例如 Microsoft.CSharp.CSharpCodeProviderMicrosoft.VisualBasic.VBCodeProvider。这个对象最终负责将 AST 转换为插入 InitializeComponent 方法的混凝土语言代码。

CodeDom 没有什么特别复杂之处,但请注意,它非常冗长,并且需要一些时间来适应。让我们快速了解一下 CodeDom。

CodeDom 语法

CodeDom 最好的学习方式是通过示例,所以让我们来看一些 C# 代码及其等效的 CodeDom 语句(我们假设它们都发生在一个类中)。代码下载包含 CodeDomTester 文件夹中的一个项目用于测试 CodeDom。这是一个简单的控制台应用程序,其中有两个骨架方法 GetMembersGetStatements,您可以在其中放入示例 CodeDom 代码并查看输出结果。让我们看一些例子:

C#

private string somefield;

CodeDom

CodeMemberField field = new CodeMemberField(typeof(string), "somefield");

所有类级别成员表示 CodeMemberEventCodeMemberFieldCodeMemberMethodCodeMemberProperty,它们都继承自 CodeTypeMember,默认情况下具有 private 和(如果适用)final 属性。

C#

public string somefield = "SomeValue";

CodeDom

CodeMemberField field = new CodeMemberField(typeof(string), "somefield");
field.InitExpression = new CodePrimitiveExpression("SomeValue");
field.Attributes = MemberAttributes.Public;

C#

this.somefield = GetValue();

CodeDom

CodeFieldReferenceExpression field = new CodeFieldReferenceExpression(
  new CodeThisReferenceExpression(), "somefield");
CodeMethodInvokeExpression getvalue = new CodeMethodInvokeExpression(
  new CodeThisReferenceExpression(), "GetValue", new CodeExpression[0]);
CodeAssignStatement assign = new CodeAssignStatement(field, getvalue);

请注意,冗长性几乎呈指数级增加。请注意,C# 代码中的 GetValue() 方法调用隐式引用 this,在 CodeDom 中必须显式声明。

C#

this.GetValue("Someparameter", this.somefield);

CodeDom

CodeMethodInvokeExpression call = new CodeMethodInvokeExpression();
call.Method = new CodeMethodReferenceExpression(
  new CodeThisReferenceExpression(), "GetValue");
call.Parameters.Add(new CodePrimitiveExpression("Someparameter"));
CodeFieldReferenceExpression field = new 
  CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "somefield");
call.Parameters.Add(field);

这里我们调用了与之前调用相同方法的一个假设的重载。请注意,我们首先创建方法调用表达式。然后,我们将它的方法引用分配给指向 this 和方法名称的引用。接下来,我们追加两个参数,第二个参数是对字段的引用,就像我们之前进行赋值时那样。

如果您想(而且我保证您会的)避免无休止(且无用)的变量声明,您可以组合语句而不诉诸于临时变量。这使得代码不那么可读,但要紧凑得多。创建那些(相当长的)语句的一个好技巧是从内到外思考目标代码。例如,在上面的代码中:

this.GetValue("Someparameter", this.somefield);

我们应该从参数开始,然后考虑方法引用,一旦我们有了它,就写成这样:

CodeMethodInvokeExpression call = 
  new CodeMethodInvokeExpression(new CodeThisReferenceExpression(), 
  "GetValue", 
  new CodeExpression[] { new CodePrimitiveExpression("Someparameter"),
      new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), 
                                       "somefield")});

这看起来很糟糕,但请再次从内到外分析:我们最后看到的是 this.somefield。接下来是原始表达式。它作为参数数组的初始化表达式传递给方法调用。然后是 this.GetValue,最后是实际的调用。

请注意,适当的缩进可以极大地帮助,但您必须主要自己完成,尤其是在有多个嵌套级别的情况下。为了实现一些可读性,最重要的嵌套是数组初始化,如上所示。还建议您将预期的 C#(或 VB.NET)输出代码放在大段语句的上方,以便任何人都可以知道您试图发出什么(也许一周后连您自己也能知道!)。

有类可以定义所有跨语言功能。但让我们看看我们需要为扩展属性准备的具体持久化代码。

生成 CodeDom

正如我们所说,我们需要将自定义序列化器与我们的 BaseController 关联起来,以自定义持久化并发出代码来保存视图映射以及任何可能需要的代码。

[DesignerSerializer(typeof(ControllerCodeDomSerializer), 
                             typeof(CodeDomSerializer))]
public class BaseController : Component, IExtenderProvider

我们的自定义序列化器必须继承自 CodeDomSerializer。这个抽象基类位于 System.ComponentModel.Design.Serialization 中,包含两个我们必须实现的抽象方法:

public abstract class CodeDomSerializer
{
  public abstract object Serialize(
      IDesignerSerializationManager manager, object value);
  public abstract object Deserialize(
      IDesignerSerializationManager manager, object codeObject);
}

Serialize 方法在我们的对象需要持久化时被调用,例如当属性更改时。返回值必须是 CodeStatementCollection 类型的一个对象,其中包含要持久化的代码。同样,Deserialize 方法中的 codeObject 参数包含先前已发出的语句。

这里对这个过程以及 IDE 如何工作有一些深入的了解会很有帮助。我们在介绍部分提到 VS.NET 会实例化根组件,以便通过根设计器对其进行“设计”。这个过程实际上发生在根组件中的所有组件(和控件)上。实际上发生的是 IDE 执行 InitializeComponent 中的大多数代码,以便重新创建对象,就像它们在运行时一样。我们说 *大多数* 而不是 *全部*,因为只有修改给定组件的语句才会被调用:即其中的属性设置和方法调用。通过自定义 Deserialize 方法,我们有机会参与此设计时重过程。通常这并不必要,所以大多数时候我们只会将球传递给原始组件序列化器,ComponentCodeDomSerializer,它基本上“执行”代码。为了获取某个类型的序列化器,我们使用我们收到的 IDesignerSerializationManager 参数的 GetSerializer 方法。这个对象还有其他有用的方法,我们稍后会用到。

所以我们的 Deserialize 实现通常看起来像这样:

public override object Deserialize(
  IDesignerSerializationManager manager, object codeObject)
{
  CodeDomSerializer serializer = 
    manager.GetSerializer(typeof(Component), typeof(CodeDomSerializer));
  return serializer.Deserialize(manager, codeObject);
}

检索组件的原始序列化器是管理器的一个常见用法,并且由于反序列化通常(并且对我们的实现来说总是)是相同的,我们将它放在一个我们将继承我们的控制器序列化器的基类中。

internal abstract class BaseCodeDomSerializer : CodeDomSerializer
{
  protected CodeDomSerializer GetBaseComponentSerializer(
    IDesignerSerializationManager manager)
  {
    return (CodeDomSerializer) 
      manager.GetSerializer(typeof(Component), typeof(CodeDomSerializer));
  }

  public override object Deserialize(
    IDesignerSerializationManager manager, object codeObject)
  {
    return GetBaseComponentSerializer(manager).Deserialize(manager, 
                                                       codeObject);
  }
}

我们将在该类中添加其他常用方法。现在我们必须进行序列化过程。我们需要迭代 Hashtable 的所有 DictionaryEntry 元素并发出以下代码来持久化我们的 ConfiguredViews 属性:

controller.ConfiguredViews.Add("txtID", 
         new ViewInfo("txtID", "Text", "Publisher", "ID"));

另一种常见做法是让原始组件序列化器完成工作,然后添加我们的自定义语句。这样,我们就避免了自己持久化常见的组件属性。所以我们的序列化器首先会做这件事:

internal class ControllerCodeDomSerializer : BaseCodeDomSerializer
{
  public override object 
         Serialize(IDesignerSerializationManager manager, object value)
  {
    CodeDomSerializer serial = GetBaseComponentSerializer(manager);
    if (serial == null) 
      return null;
    CodeStatementCollection statements = (CodeStatementCollection) 
      serial.Serialize(manager, value);

需要注意的是,即使根组件本身是 BaseController 时,我们的序列化器也会被调用。在这种情况下,我们不想持久化自定义代码,因为它基本上适用于当它用在其他组件中时,例如 PageForm。为了考虑到这一点,我们请求我们之前使用的 IDesignerHost 服务,并通过其 RootComponent 属性进行检查。IDesignerSerializationManager 实现 IServiceProvider,所以我们有通常的 GetService 方法来做到这一点。

  IDesignerHost host = (IDesignerHost)
    manager.GetService(typeof(IDesignerHost));
  if (host.RootComponent == value)
    return statements;

基类 CodeDomSerializer 具有一些在序列化/反序列化代码时工作的有用方法。当我们访问 ConfiguredViews 属性时,我们通过对正在处理的实际控制器的引用(value 参数)来实现这一点。基类中的一个辅助方法创建了要用于我们所谓的 CodeDom 图中的此引用的适当 CodeDom 对象。

  CodeExpression cnref = SerializeToReferenceExpression(manager, value);

现在可以使用 CodeExpression 来创建属性引用。

  CodePropertyReferenceExpression propref = 
    new CodePropertyReferenceExpression(cnref, "ConfiguredViews");

我们主要定义这两个变量是为了稍微简化我们将要构建的下一个表达式。让我们再次看一下示例目标方法调用:

controller.ConfiguredViews.Add("txtID", 
          new ViewInfo("txtID", "Text", "Publisher", "ID"));

我们已经有了第一个部分,一直到 ConfiguredViews 属性的访问。接下来的部分是:

  • CodeMethodInvokeExpression:调用 Add
  • CodeExpression[]:方法调用的参数。
  • CodePrimitiveExpression:"txtID" 原始字符串值。
  • CodeObjectCreateExpression:新的 ViewInfo 部分。
  • CodePrimitiveExpression:为传递给构造函数的每个原始字符串值一个。

所以代码是:

BaseController cn = (BaseController) value;
CodeExpression cnref = SerializeToReferenceExpression(manager, value);

CodePropertyReferenceExpression propref = 
  new CodePropertyReferenceExpression(cnref, "ConfiguredViews");
//Iterate the entries

foreach (DictionaryEntry cv in cn.ConfiguredViews)
{
  ViewInfo info = (ViewInfo) cv.Value;
  if (info.ControlID != String.Empty && info.ControlProperty != null &&
    info.Model != String.Empty && info.ModelProperty != String.Empty)
  {  
    //Generates:

    //controller.ConfiguredViews.Add(key, new ViewInfo([values]));

    statements.Add(
      new CodeMethodInvokeExpression(
        propref, "Add", 
        new CodeExpression[] { 
          new CodePrimitiveExpression(cv.Key), 
          new CodeObjectCreateExpression(
            typeof(ViewInfo),
            new CodeExpression[] { 
              new CodePrimitiveExpression(info.ControlID),
              new CodePrimitiveExpression(info.ControlProperty), 
              new CodePrimitiveExpression(info.Model), 
              new CodePrimitiveExpression(info.ModelProperty) }
          ) }
        ));
  }
}

注意适当的缩进如何使语句更具可读性。而且我们只声明了两个临时变量,实际上甚至可以省略它们。

我们也可以向代码添加注释,使用:

statements.Add(new 
  CodeCommentStatement("-------- ClariuS Custom Code --------"));

回到使用该组件的窗体,我们将在相关的 InializeComponent 部分看到以下代码:

private void InitializeComponent()
{
  ...
  // ------------- ClariuS Custom Code -------------

  this.controller.ConfiguredViews.Add("txtID", 
       new Mvc.Components.Controller.ViewInfo("txtID", 
       "Text", "Publisher", "ID"));
  this.controller.ConfiguredViews.Add("txtName", 
       new Mvc.Components.Controller.ViewInfo("txtName", 
       "Text", "Publisher", "Name"));
  ...

代码生成器会完全限定所有类引用,因为不能保证开发人员会在类中添加必要的 using 子句。

最后,我们还可以发出在代码生成过程中发现的错误。例如,如果我们检测到属性未正确设置,我们可以这样做:

if (info.ControlID != String.Empty && info.ControlProperty != null &&
  info.Model != String.Empty && info.ModelProperty != String.Empty)
{  
  //Report errors if necessary

  object ctl = manager.GetInstance(info.ControlID);
  if (ctl == null)
  {
    manager.ReportError(String.Format("Control '{0}' associated" + 
       " with the view mapping in " + "controller '{1}' doesn't " + 
       "exist in the page.", info.ControlID, manager.GetName(value)));
    continue;
  }
  if (ctl.GetType().GetProperty(info.ControlProperty) == null)
  {
    manager.ReportError(String.Format("Control property '{0}' in" + 
       " control '{1}' associated " + "with the view mapping in controller" + 
       " '{2}' doesn't exist.", info.ControlProperty, info.ControlID, 
       manager.GetName(value)));
    continue;
  }

请注意,我们使用其他 manager 方法来格式化错误消息,GetInstanceGetName,它们允许我们分别检索对象引用及其名称。通过在发现错误后使用 continue,我们避免了无效设置的序列化,有效地将它们删除。组件用户将在出现无效值时看到类似以下内容:

回归简洁

讨论了自定义 CodeDom 持久化的强大功能和灵活性后,使用这种技术序列化多个成员确实是一项艰巨的任务。幸运的是,VS.NET 支持一种更简单的方式将自定义类型序列化到代码隐藏文件中。

该过程只需我们为类实现一个类型转换器,该转换器可以将对象转换为 InstanceDescriptor 类型。这就像我们之前为字符串转换所做的一样:

internal abstract class InstanceConverter : TypeConverter
{
  public override bool CanConvertFrom(ITypeDescriptorContext context, 
    Type sourceType)
  {
    if (sourceType == typeof(InstanceDescriptor))
    {
      return true;
    }
    return base.CanConvertFrom(context, sourceType);
  }

  public override bool CanConvertTo(ITypeDescriptorContext context, 
    Type destinationType)
  {
    if (destinationType == typeof(InstanceDescriptor))
    {
      return true;
    }
    return base.CanConvertTo(context, destinationType);
  }

  public override object ConvertTo(ITypeDescriptorContext context, 
    CultureInfo culture, object value, Type destinationType)
  {
    if (destinationType == typeof(string))
    {
      ViewInfo info = (ViewInfo) value;
      return new InstanceDescriptor(
        typeof(ViewInfo).GetConstructor(
          new Type[] { 
                typeof(string), 
                typeof(string), 
                typeof(string), 
                typeof(string)
                }), 
        new  object[] { 
              info.ControlID, 
              info.ControlProperty,
              info.Model,
              info.ModelProperty
                },
        true);
    }
    return base.ConvertTo(context, culture, value, destinationType);
  }

InstanceDescriptor 构造函数接收第一个参数为 MemberInfo,即用于创建自定义类实例的方法或构造函数。我们使用反射来检索与我们指定的参数类型签名匹配的构造函数。第二个参数传递实际参数,最后,第三个参数指定通过此方法调用是否完成了对象实例化。如果说它没有完成,则每个属性将依次序列化。

如果待序列化的属性是 ViewInfo 对象数组,则此机制会自动工作,我们将自动生成以下代码:

private void InitializeComponent()
{
  …
  this.publisherController.ConfiguredViews = 
    new ViewInfo [] { 
        new ViewInfo("txtID", "Text", "Publisher", "ID"),
        new ViewInfo("txtName", "Text", "Publisher", "Name")
              };

在我们的例子中,这不符合我们的需求,但有很多情况这种序列化就足够了。

最后,另一种(作者认为不推荐)方法是实现 ISerializable。这样,VS.NET 会直接将对象序列化到资源文件中。这使得查看序列化过程背后的情况更加困难,这可能是好是坏,取决于视角。

现在我们了解了属性扩展器和自定义代码序列化的基础知识,让我们完成 MVC 的图景。

完善 MVC 组件模型

在我们的模型中,控制器包含模型。视图通过控制器访问模型。模型也是被拖放到控制器设计器中的组件。我们知道默认设计器允许任何类型的组件被拖放到图面上。这不是我们想要的。我们只希望在我们的控制器中使用模型,我们还希望避免将模型拖放到控制器以外的容器中。

实现这种级别的控制的方法是使用两个相关的 IDE 功能。一个是将自定义 RootDesigner 附加到所有自定义控制器将继承的基控制器类:

[Designer(typeof(ControllerRootDesigner), typeof( IRootDesigner))]
public class BaseController : Component, IExtenderProvider

另一个是将 ToolboxItemFilter 附加到新根设计器和 BaseModel 类(所有自定义模型将继承自该类)上:

[ToolboxItemFilter("Mvc.Components.Controller", ToolboxItemFilterType.Require)]
public class ControllerRootDesigner : ComponentDocumentDesigner
{
  public ControllerRootDesigner()
  {
  }
}
[ToolboxItemFilter("Mvc.Components.Controller", ToolboxItemFilterType.Require)]
public class BaseModel : Component
{
  public override string ToString()
  {
    return _name;
  }
  
  public virtual string ModelName
  {
    get { return _name; } 
    set { _name = value; }
  } string _name = "BaseModel";
}

ToolboxItemFilterType.Require 值告诉 IDE,只有当当前根设计器和组件都具有匹配的字符串(第一个属性参数)时,才会启用工具箱中的项。有一个关于此属性值及其对工具箱项影响的组合的有趣文章,位于 这里

BaseModel 类提供了一个虚拟属性,允许继承者为模型分配一个名称,该名称将用于通过视图映射引用它。作为最后一步,我们可以通过向它们添加以下属性来防止这两个基类出现在工具箱中:

[ToolboxItem(false)]

请注意,此属性是可继承的,因此如果派生类想在工具箱中使用,它们必须具有 ToolboxItem(true) 属性。通常的 OO 编程建议是使开发者不打算使用的基类为 abstract。但我们不能这样做,因为目前 IDE 需要能够实例化它们才能工作。

我们现在可以创建一个 PublisherController 和一个 PublisherModel,它们继承自这两个类,编译所有内容并将它们添加到工具箱中。在 PublisherController 类设计模式下,我们可以看到过滤器的效果,它导致 PublisherController 组件被禁用。在窗体(Windows 或 Web)中则相反,它将是唯一启用的。

然而,在模型完成之前,我们必须考虑到 VS.NET 在我们拖放组件到设计器中时构建组件层次结构的一些细节,尤其是当组件继承其他组件时。

.NET 组件模型:深入了解和继承问题

当我们向项目添加新组件时,我们会从基本模板获得一些自动生成的代码。如果我们将其拖放到另一个自定义组件中,组件代码将如下所示(除注释外):

public class Component1 : System.ComponentModel.Component
{
  private Mvc.Tests.Component2 component21;
  private System.ComponentModel.IContainer components;

  public Component1(System.ComponentModel.IContainer container)
  {
    container.Add(this);
    InitializeComponent();
  }

  public Component1()
  {
    InitializeComponent();
  }

  private void InitializeComponent()
  {
    this.components = new System.ComponentModel.Container();
    this.component21 = new Mvc.Tests.Component2(this.components);

  }
}

需要注意的关键是 private components 变量。它的类型是 Container,并且传递给 Component2 构造函数。我们知道,反过来,这个构造函数将执行与 Component1 构造函数相同的操作:将自己添加到容器。

  public Component2(System.ComponentModel.IContainer container)
  {
    container.Add(this);
    InitializeComponent();
  }

这个组件反过来包含它自己的 components 字段,它将传递给任何我们拖入其设计图面的其他组件,这些组件又会将其自身添加到其中,依此类推。Container 类的实现会在 Add 方法中为传入的组件 Site 属性分配一个新创建的 ISite,该 ISite 指向容器以及组件。这个 Site,类型为 Container.Site,以后可以被组件用来知道谁包含它。回想一下,在设计时,这个 Site 将指向 DesignSite,正如在引言中所解释的那样。

这种机制对于包含关系很好,但当组件继承自其他组件时,我们会得到一个破碎的包含链,因为基类将拥有自己的 components 字段。所以我们将有一个类似的层次结构:

这种层次结构之所以这样创建,是因为拖放到 PublisherControler 中的模型被传递了“错误”的容器。结果是,我们放在 BaseController 类中的任何代码都将无法访问包含的组件。解决方案是将基类 components 字段传递给模型构造函数。然而,这会涉及使该字段可供继承者访问(protected),这在某种程度上违反了 OO 的封装原则。更好的解决方案是在基类本身中实现 IContainer,并将 this 引用传递给模型构造函数。基类中的接口实现将提供对 components 字段的正确访问。

public class BaseController : Component, IContainer, IExtenderProvider
{
  #region Implementation of IContainer

  /// <summary>

  /// Always initialize the container because we will host components 

  /// from inheriting controllers.

  /// </summary>

  private Container components = new Container();

  public void Remove(IComponent component)
  {
    components.Remove(component);
  }

  public void Add(IComponent component, string name)
  {
    components.Add(component, name);
  }

  public void Add(IComponent component)
  {
    components.Add(component);
  }

  public ComponentCollection Components
  {
    get
    {
      return components.Components;
    }
  }
  #endregion

  ...

我们可以安全地删除接受容器的基类构造函数,因为 PublisherController 等继承类将自行添加到其中,这是默认行为,并且与继承不冲突。

public PublisherController(IContainer container)
{
  container.Add(this);
  InitializeComponent();
}

基类不需要 InitializeComponent 或特殊的构造函数代码,因为 components 字段总是在字段声明本身中初始化的。

现在,为了自定义模型构造函数的代码,以便它使用以下代码:

this.model = new Mvc.Components.Model.PublisherModel(this);

而不是当前的:

this.model = new Mvc.Components.Model.PublisherModel(this.components);

我们必须像为基控制器一样,将自定义 CodeDomSerializer 附加到它:

[DesignerSerializer(typeof(ModelCodeDomSerializer), 
                        typeof(CodeDomSerializer))]
public class BaseModel : Component

序列化器代码与控制器代码相比相当简单:

internal class ModelCodeDomSerializer : BaseCodeDomSerializer
{
  public override object Serialize(
    IDesignerSerializationManager manager, object value)
  {
    CodeDomSerializer serializer = GetBaseComponentSerializer(manager);
    if (serializer == null) 
      return null;

    CodeStatementCollection statements = (CodeStatementCollection)
      serializer.Serialize(manager, value);

到目前为止,内置序列化器生成的语句包含一个表示以下代码行的表达式:

this.model = new Mvc.Components.Model.PublisherModel(this.components);

如果我们根据其 CodeDom 表示来分析它,我们将看到它将具有以下形式:

  • CodeAssignStatement= 操作,有右部和左部。
  • CodeObjectCreateExpression:赋值的右部。
  • CodeFieldReferenceExpression:这是表达式 Parameters 集合中的第一个参数,它指向 this.components 字段。

我们只需替换(或添加)该参数:

    CodeAssignStatement assign = (CodeAssignStatement) statements[0];
    //The expression at the right is the actual constructor call.

    CodeObjectCreateExpression create = (CodeObjectCreateExpression) 
      assign.Right;
    if (create.Parameters.Count > 0)
    {
      create.Parameters[0] = new CodeThisReferenceExpression();
    }
    else
    {
      create.Parameters.Add(new CodeThisReferenceExpression());
    }

    return statements;
  }
}

我们可以安全地传递 CodeThisReferenceExpression,因为包含组件的基类实现了 IContainer,正如构造函数重载所期望的那样。

到目前为止,除了 ExpandableObjectConverter 和扩展属性之外,我们还没有提供任何与 IDE 的深度集成。让我们看看如何增强设计时体验。

深度 IDE 集成

我们到目前为止的实现有很多方面非常薄弱:

  • 在属性浏览器中展开的属性中对映射值的更改不总是立即持久化(有时根本不持久化)。
  • 当一个可视化组件被重命名时,我们会丢失所有视图映射,因为它们存储在 ConfiguredViews 表中,基于其名称。同样,当它被移除时,映射不会相应地从该表中移除。
  • 一些属性不应出现在属性浏览器中(甚至不在 IntelliSense 中),例如当控制器是根组件时的控制器 ComponentsConfiguredViews
  • 控制器可能需要访问数据库连接。支持 *web.config* 可绑定属性(或 EXEs 的 *app.config*)将是很棒的。
  • 输入控件和模型属性名称,以及模型本身的名称,都容易出错。
  • 逐个控件设置视图映射可能会很烦人,如果涉及许多控件。
  • 无法一次性检查所有应用的映射。

第一个问题是 IDE 无法知道 ViewInfo 对象属于控制器组件,并且只要它发生更改就应该再次序列化它。一种方法是修改所有属性的 setter,使它们引发某种可以被包含控制器捕获的事件。虽然这是最明显的解决方案,但它需要编写代码来通知所有属性 setter 的更改,而 .NET 为我们提供了一种更优雅的方法。

在 IDE 中,对组件的所有属性更改都通过所谓的描述符执行,这些描述符是 System.ComponentModel.PropertyDescriptor 类的实例,它们描述了一个属性。尤其是在通过属性浏览器应用更改时,这是真的。事实上,许多控件和 Windows Forms 绑定机制也使用这种方法。您可以通过转储 System.Windows.Forms.dll IL 代码程序集并搜索 PropertyDescriptor::SetValue 来进行检查。

该描述符有一个方法 AddValueChanged,它允许我们添加一个处理程序,该处理程序将在属性更改时被调用。这正是我们触发序列化过程所需要的。我们还将向 ViewInfo 类添加一个 IsHooked 标志,以便我们知道是否已附加处理程序。

//Verify that we are trapping the changes. 

if (!info.IsHooked)
{
  PropertyDescriptorCollection props = TypeDescriptor.GetProperties(info);
  props["ControlProperty"].AddValueChanged(info, 
    new EventHandler(RaiseViewInfoChanged));
  props["Model"].AddValueChanged(info, 
    new EventHandler(RaiseViewInfoChanged));
  props["ModelProperty"].AddValueChanged(info, 
    new EventHandler(RaiseViewInfoChanged));
  info.IsHooked = true;
}

我们使用 TypeDescriptor 类来获取适用于我们对象的 PropertyDescriptor 对象集合。该类有许多用于处理组件的静态方法,值得一看。请参阅 MSDN 帮助以获取成员列表及其用法。

RaiseViewInfoChanged 处理程序通过使用 IDE 的另一个服务 IComponentChangeService 来通知宿主映射发生了更改。

internal void RaiseViewInfoChanged(object sender, EventArgs e)
{
  IComponentChangeService svc = (IComponentChangeService)
     GetService(typeof(IComponentChangeService));
  if (svc != null)
  {
    svc.OnComponentChanged(this, 
      TypeDescriptor.GetProperties(this)["ConfiguredViews"], 
      null, 
      this.ConfiguredViews);
  }
}

通过使用此服务,IDE 将采取适当措施再次持久化我们的组件。

第二个问题,跟踪已映射控件的命名和移除更改,是通过同一个服务实现的,但在此情况下,这是一种全局跟踪,无法像我们上面那样进行。做到这一点的方法是为我们的组件创建一个设计器来监听更改。如我们稍后将看到的,设计器允许我们为组件添加更多设计时功能。此设计器,与 RootDesigner 类似,通过属性进行关联。

[Designer(typeof(ControllerDesigner))]
public class BaseController : Component, IContainer, IExtenderProvider

我们的 ControllerDesigner 类继承自 ComponentDesigner,它是与 IComponent 接口关联的默认设计器。设计器对 IDE 有更大的控制和交互,并且在根组件(即 FormPage)被设计的整个时间内存在,并且永久地附加到它正在“设计”的组件上,在我们的例子中是具体控制器。设计器中的关键点,我们可以挂钩到 IDE 服务,是 Initialize 方法,在那里我们接收要设计的组件。

internal class ControllerDesigner : ComponentDesigner
{
  public override void Initialize(IComponent component)
  {
    //Always call base.Initialize!

    base.Initialize(component);
    IComponentChangeService ccs = (IComponentChangeService)
      GetService(typeof(IComponentChangeService));
    if (ccs != null) 
    {
      ccs.ComponentRemoving += 
        new ComponentEventHandler(OnComponentRemoving);
      ccs.ComponentRename += 
        new ComponentRenameEventHandler(OnComponentRename);
    }
  }
  
  BaseController CurrentController
  {
    get { return (BaseController) this.Component; }
  }
}

如代码注释中所述,我们在 Initialize 方法中必须做的第一件事是调用基类实现。这允许设计器与 IDE 正确挂钩。设计器包含一个 Component 属性,该属性设置为此函数传入的 IComponent 参数。处理后,我们使用通常的 GetService 方法请求服务,该方法现在直接在基类 ComponentDesigner 中实现。此实现只是将调用传递给设计器的 Component.Site 属性,这只是一个快捷方式。

有了服务后,我们可以附加到我们感兴趣的两个事件:ComponentRemovingComponentRename。让我们先看看重命名处理程序。

void OnComponentRename(object sender, ComponentRenameEventArgs e)
{
  if (CurrentController.ConfiguredViews.ContainsKey(e.OldName))
  {
    ViewInfo info = (ViewInfo) CurrentController.ConfiguredViews[e.OldName];
    info.ControlID = e.NewName;
    CurrentController.ConfiguredViews.Remove(e.OldName);
    CurrentController.ConfiguredViews.Add(e.NewName, info);
    //Notify environment of the change.

    RaiseComponentChanging(
      TypeDescriptor.GetProperties(CurrentController)["ConfiguredViews"]);
  }
}

我们只需删除映射并使用新的组件名称作为键重新添加它。最后,我们必须通知环境以触发代码生成。这可以通过基类 ComponentDesigner 的方法 RaiseComponentChanging 来实现。移除处理程序略有不同:

void OnComponentRemoving(object sender, ComponentEventArgs e)
{
  IReferenceService svc = (IReferenceService) 
    GetService(typeof(IReferenceService));
  string id = svc.GetName(sender);
  if (id != null && CurrentController.ConfiguredViews.ContainsKey(id))
  {
    CurrentController.ConfiguredViews.Remove(id);
    //Notify environment of the change.

    RaiseComponentChanging(
      TypeDescriptor.GetProperties(CurrentController)["ConfiguredViews"]);
  }
}

在这里,我们使用另一个服务 IReferenceService。它允许我们检索组件名称,根据名称获取组件(反之亦然),以及宿主内存在的特定类型组件的集合。我们使用它来知道正在移除的组件的名称,并相应地移除映射,就像我们之前所做的那样通知环境。

下一个问题,在属性浏览器中隐藏成员,取决于具体情况。隐藏公共属性通常涉及设置属性。

false)>
public ComponentCollection Components

这只会从属性浏览器中隐藏此 BaseController 成员。有时我们不仅要从属性浏览器隐藏成员,还要从 IntelliSense 隐藏,例如对于为了让生成代码可以访问而公开的成员,但我们想对用户隐藏。例如,Get/Set 方法充当属性提供程序实现。类用户根本不应该看到它们,因为它们仅与 IDE 及其 AOP 模型相关。我们可以使用另一个属性来做到这一点。

[EditorBrowsable(EditorBrowsableState.Never)]
public ViewInfo GetWinViewMapping(object target)

在某些外部因素下,要选择性地隐藏一个属性并不那么容易。我们讨论的是仅当控制器是根组件时才隐藏ConfiguredViews。我们可以利用我们为此创建了一个根设计器这一事实,并利用它来过滤属性列表。

public class ControllerRootDesigner : ComponentDocumentDesigner
{
  protected override void PreFilterProperties(IDictionary properties)
  {
    base.PreFilterProperties(properties);
    // Only show ConfiguredViews if the component is not the root component.

    IDesignerHost host = (IDesignerHost) 
      GetService(typeof(IDesignerHost));
    if (host.RootComponent == this.Component)
      properties.Remove("ConfiguredViews");
  }
}

基础ComponentDesigner中有许多可重写成员,ComponentDocumentDesigner继承自它。有一组用于属性、属性和事件的 Pre/Post 筛选方法。我们正在预先筛选属性,并与IDesignerHost.RootComponent进行检查以确定属性的移除。值得再次指出的是,GetService的实现照常将请求转发给当前组件的Site属性。

IDE 为我们提供了一个内置机制,用于在应用程序配置文件中持久化属性值。该功能可通过属性浏览器在DynamicProperties类别下获得。出现的对话框可以“绑定”一个组件属性到一个键,该键位于应用程序配置文件的appSettings节中。我们实现了一个IConnectable接口,它只有一个属性ConnectionString,控制器和模型都可以使用它来指示它们需要某种连接。

public interface IConnectable
{
  string ConnectionString { get; set; }
}

示例PublisherController实现如下:

[RecommendedAsConfigurable(true)] 
public string ConnectionString
{
  get { return _connection; }
  set 
  { 
    _connection = value; 
    //Propagate setting to connectable models.

    foreach (IComponent model in Components)
      if (model is IConnectable)
        ((IConnectable)model).ConnectionString = value;
  }
} string _connection;

它只是将设置传播给任何包含的“可连接”组件。组件中的每个公共读/写属性(如果其类型是内置类型)都会出现在当我们在DynamicProperties下的Advanced项旁边单击省略号时打开的对话框中。但是,通过应用RecommendedAsConfigurable属性,它还将直接显示在Advanced项下方,如下所示:

请注意,属性已绑定到配置文件,如ConnectionString属性旁边的小绿色图标所示。如果我们先设置属性值,然后定义DynamicProperties下的键,就会在应用程序配置文件中添加相应的条目。

<configuration>
  <appSettings>
    <add key="Pubs" 
        value="data source=.\NetSdk;initial catalog=pubs;
               persist security info=False;user id=sa;" />
  </appSettings>

要解决最后三个问题(为映射属性提供值列表、创建一个在单一位置设置所有映射的 UI 并验证已配置的映射),我们将利用 IDE 的服务化架构,并用我们自己的服务进行扩展。

扩展服务化架构

我们已经使用了 IDE 的几个服务。其中大多数由设计器主机提供。如果我们需要在多个地方访问某个功能,复制相同的代码就没有意义了。将它们实现为某个类的静态成员可能会起作用,但我们可以通过遵循 VS.NET 规则来实现相同的效果:创建自定义设计时服务。

IDesignerHost接口为我们提供了一种附加和移除我们服务的方法,即AddServiceRemoveService方法。VS.NET 附加服务的方式是定义一个接口,并通过主机方法添加具体的实例。我们的服务实现将需要以下方法:

public interface IControllerService
{
  /// <summary>

  /// Verifies that mappings for the controller are correct. 

  /// </summary>

  /// <param name="controller">Controller to check.</param>

  void VerifyMappings(BaseController controller);

  /// <summary>

  /// Lists all the properties of the control.

  /// </summary>

  string[] GetControlProperties(object control);

  /// <summary>

  /// Lists all the properties of the model.

  /// </summary>

  string[] GetModelProperties(BaseModel model);
}

服务可以附加到多个地方,但通常是在组件“可设计”后尽快附加。这发生在组件设计器的Initialize方法被调用时,我们在其中接收正在设计的组件。因此,我们将以下代码添加到我们的ControllerDesigner中:

public override void Initialize(IComponent component)
{
  ...
  //Attach the adapter services at design and initialization time. 

  SetupServices();
}

void SetupServices()
{
  service = GetService(typeof(IControllerService));
  if (service == null)
  {
    host.AddService(typeof(IControllerService), 
           new ControllerService(host), false);
  }
}

我们首先尝试检索服务来检查它是否已被添加。最后,我们添加服务,传递类型和服务实例。如果我们想按需创建服务,我们也可以传递一个执行实际对象实例化的委托。如果服务创建成本高昂,这很有用。在我们的例子中,服务实例会立即创建,并且它接收IDesignerHost的引用,以便我们可以在我们的实现中访问其他 IDE 服务。

internal class ControllerService : IControllerService
{
  IDesignerHost _host;

  public ControllerService(IDesignerHost host)
  {
    _host = host;
  }

  public string[] GetControlProperties(object control)
  {
    //This code will be reused from an editor form too.

    PropertyDescriptorCollection props = TypeDescriptor.GetProperties(
      control, new Attribute[] { new BrowsableAttribute(true) });
    
    string[] names = new string[props.Count];
    for (int i = 0; i < props.Count; i++)
      names[i] = props[i].Name;
    Array.Sort(names);

    return names;
  }

  public string[] GetModelProperties(BaseModel model)
  {
    PropertyDescriptorCollection props = TypeDescriptor.GetProperties(
      model, new Attribute[] { new BindableAttribute(true) });
    
    string[] names = new string[props.Count];
    for (int i = 0; i < props.Count; i++)
      names[i] = props[i].Name;
    Array.Sort(names);

    return names;
  }
}

GetControlProperties方法的行为与属性浏览器本身完全相同。我们使用BrowsableAttribute来过滤要返回的属性名称列表。我们对模型属性也这样做,但使用BindableAttribute,这并非严格必需,但它允许我们提供一种易于理解的隐藏属性的方法。我们可以使用任何我们想要的属性,但使用内置的属性可以让我们组件用户更容易理解。例如,BaseModel.ModelName属性不应为控制器组件用户提供,因此我们可以应用该属性,它将不再出现在我们显示可用属性的任何地方。

public class BaseModel : Component
{
  false)>
  public virtual string ModelName

相反,在PublisherModel类中,我们将属性添加到反映包含发布者数据的数据库字段的模型属性上。

false)>
true)>
public string ID
{
  get { return _id; }
  set { _id = value; }
} string _id;

false)>
true)>
public string Name
{
  get { return _name; }
  set { _name = value; }
} string _name;

请注意,同时,我们应用了BrowsableAttribute来从属性浏览器中隐藏它们,这是合乎逻辑的,因为这些属性仅在运行时才有意义,而肯定不是在我们设计模型组件本身或包含它的控制器时。

旁注:组件属性序列化

我们发现有时使用DesignerSerializationVisibilityAttribute会导致 IDE 行为异常。特别是,我们发现UndoManager(一种控制撤销/重做功能的IDesignerSerializationManager)会丢失其序列化组件以进行撤销操作的能力,有时会抛出“未知撤销/重做异常”。发生这种情况时,每当我们键入源代码时,我们都会失去所有语法着色和 Intellisense 功能。

还有另一种方法可以告知 IDE 某个属性不应被序列化,即通过一个方法(可以是私有的),名称为ShouldSerializeXX,它返回一个布尔值表示序列化支持。XX必须替换为属性名称。对于上面两个属性,我们可以添加以下方法来实现与应用DesignerSerializationVisibilityAttribute相同效果:

bool ShouldSerializeID() { return false; }
bool ShouldSerializeName() { return false; }

-- 旁注结束 ;)

现在我们可以回到控制器服务。要为视图映射在属性浏览器中提供值列表,我们必须使用TypeConverter。此类提供了一种以多种方式与属性浏览器交互的方法。转换器确定支持哪些转换(与其他类型的转换)以及哪些值是有效的,也称为标准值

internal abstract class StringListConverter : TypeConverter
{
  public override bool CanConvertFrom(ITypeDescriptorContext context, 
    Type sourceType)
  {
    if (sourceType == typeof(string))
    {
      return true;
    }
    return base.CanConvertFrom(context, sourceType);
  }
  
  public override object ConvertFrom(ITypeDescriptorContext context, 
    CultureInfo culture, object value)
  {
    return value as string;
  }

  public override bool CanConvertTo(ITypeDescriptorContext context, 
    Type destinationType)
  {
    if (destinationType == typeof(string))
    {
      return true;
    }
    return base.CanConvertTo(context, destinationType);
  }

  public override object ConvertTo(ITypeDescriptorContext context, 
    CultureInfo culture, object value, Type destinationType)
  {
    if (destinationType == typeof(string))
    {
      return value as string;
    }
    return base.ConvertTo(context, culture, value, destinationType);
  }

在此实现中,我们声明支持与string类型之间的转换。我们可以提供自定义转换,就像 Web 控件的Width属性一样。您可能已经注意到该属性的类型为Unit,因此值由一个数值和一个UnitType组成。但是我们可以在属性浏览器中直接键入一个值,例如“2px”,它将被成功转换为Unit类型的对象,并具有数值部分和相应的UnitType。这在其关联的转换器中完成,该转换器可以拆分字符串并创建具有这些部分的单元对象。

这个基类将用于在属性浏览器中提供一个独占的值列表(下拉列表)。到目前为止的所有代码对于提供值列表的所有转换器都是通用的,这就是为什么我们使用这个基类。告诉属性浏览器我们打算提供值列表并且它们是独占的方法如下:

  public override bool GetStandardValuesExclusive(
    ITypeDescriptorContext context)
  {
    return true;
  }

  public override bool GetStandardValuesSupported(
    ITypeDescriptorContext context)
  {
    return true;
  }
}

现在,列出控件属性的转换器只需继承此类并提供值列表即可。

internal class ControlPropertyConverter : StringListConverter
{
  public override StandardValuesCollection GetStandardValues(
    ITypeDescriptorContext context)
  {
    ViewInfo info = (ViewInfo) context.Instance;
    IControllerService cont = (IControllerService) 
      context.GetService(typeof(IControllerService));
    return new StandardValuesCollection(cont.GetControlProperties(ctl));
  }
}

ITypeDescriptorContext参数提供了有关转换发生上下文的重要信息。它提供了对正在更改的实例(此处为ViewInfo)、当前容器(DesignerHost)以及正在更改的属性(作为PropertyDescriptor)的访问。它还实现了IServiceProvider,因此我们可以像在设计器或组件内部一样请求服务。

值列表由我们之前添加到设计主机的控制器服务提供,我们必须返回一个可以从字符串数组构造值的StandardValuesCollection对象。请注意,出于某种(奇怪的)原因,此类不会出现在 Intellisense 中,即使没有应用EditorBrowsable属性。

现在我们只需要将转换器与ViewInfo类中的相应属性关联起来,这样就可以了。

[TypeConverter(typeof(ControlPropertyConverter))]
public string ControlProperty

这种关联的效果在属性浏览器中立即可见。

但是,如果我们想检查收到的ViewInfo.ControlID是否确实指向包含的组件(Windows 或 Web Form)中的某个对象怎么办?很自然地会想到我们可以直接从上下文(一个IServiceProvider)中获取IReferenceService并使用其GetReference方法。然而,这行不通,因为编辑发生在属性浏览器内部,并且是属性浏览器在响应GetService请求,不幸的是,它将返回null。不过,有一个简单的解决方法,它涉及到先获取IDesignerHost服务,然后向它请求所需的服务。

public override StandardValuesCollection 
                GetStandardValues(ITypeDescriptorContext context)
{
  ViewInfo info = (ViewInfo) context.Instance;
  
  IDesignerHost host = (IDesignerHost) 
    context.GetService(typeof(IDesignerHost));
  IReferenceService svc = (IReferenceService) 
    host.GetService(typeof(IReferenceService));

  if (svc == null)
    return null;

  object ctl = svc.GetReference(info.ControlID);
  if (ctl == null)
  {
    throw new ArgumentException("The control '" + info.ControlID + 
      "' wasn't found in the current container.");
  }
  else
  {
    IControllerService cont = (IControllerService) 
      context.GetService(typeof(IControllerService));
    return new StandardValuesCollection(cont.GetControlProperties(ctl));
  }
}

ModelPropertyConverter还有另一个问题。我们需要一种方法来知道当前ViewInfo链接到的控制器,以便通过其名称获取模型实例的引用。没有这个引用,就不可能反映它并显示其属性。为了解决这个问题,我们在ViewInfo中添加了一个Controllerinternal属性,该属性将在离开GetXxxViewMapping扩展器属性 getter 之前设置为指向包含的控制器。我们稍后会回到这一点,但首先让我们看看其余转换器的代码:

internal class ModelNameConverter : StringListConverter
{
  public override StandardValuesCollection GetStandardValues(
    ITypeDescriptorContext context)
  {
    ViewInfo info = (ViewInfo) context.Instance;

    ArrayList names = new ArrayList();
    foreach (Component comp in info.Controller.Components)
      if (comp is BaseModel) names.Add(((BaseModel)comp).ModelName);      
    return new StandardValuesCollection(names);
  }
}

internal class ModelPropertyConverter : StringListConverter
{
  public override StandardValuesCollection GetStandardValues(
    ITypeDescriptorContext context)
  {
    ViewInfo info = (ViewInfo) context.Instance;
    BaseModel model = info.Controller.FindModel(info.Model);
    if (model == null) return null;

    IControllerService cont = (IControllerService) 
      context.GetService(typeof(IControllerService));
    return new StandardValuesCollection(cont.GetModelProperties(model));
  }
}

我们像以前一样,通过TypeConverter将它们与它们相应的ViewInfo属性关联起来。

到目前为止,将属性枚举代码从转换器本身移出并放入IControllerService中似乎没有多大意义。毕竟,这是我们目前唯一使用它的地方。但是,我们将添加的下一个功能,即在一个集中的位置编辑表单中的所有映射,将使此设计决策变得相关。

自定义编辑器

属性浏览器通常足以设置少量属性值。然而,配置一个复杂的组件可能通过简单的属性浏览器界面非常困难。想想 Windows 窗体字体编辑器对话框如何简化对几个与字体相关的属性的选择。或者 CSS 文件和 HTML 样式属性真正有用的 Style Builder。

好消息是我们自己也可以有一个这样的编辑器。它基本上是我们像其他任何窗体一样构建的自定义 Windows 窗体。在窗体内部,我们可以通过在窗体设置的基础上为用户设置多个属性来配置组件。我们创建了以下窗体来编辑应用于控制器的所有映射:

它远非 Style Builder,但它是我们所需的。我们只需在左侧的列表框中填充控制器正在配置的视图信息(ViewInfo)实例,并在组合框中填充相关值,将选定项定位到与所选ViewInfo匹配的位置。现在的问题是如何启动对此窗体的编辑。一种方法是将自定义类型编辑器附加到属性,在本例中是控制器ConfiguredViews

[Editor(typeof(ViewMappingsEditor), typeof(UITypeEditor))]
public Hashtable ConfiguredViews

EditorAttribute的第二个参数将始终是System.Drawing.Design.UITypeEditor类的类型,我们的自定义编辑器必须从此类派生。

internal class ViewMappingsEditor : UITypeEditor

我们必须重写GetEditStyle方法来告知属性浏览器我们将向用户显示哪种 UI。

public override UITypeEditorEditStyle GetEditStyle(
  ITypeDescriptorContext context)
{
  return UITypeEditorEditStyle.Modal;
}

在我们的例子中,我们将显示一个完整的模态 Windows 窗体。我们拥有的另一种可能性是UITypeEditorEditStyle.DropDown,在这种情况下,我们可以显示一个就地 Windows 窗体用户控件,就像 Windows 窗体AnchorDock属性编辑器一样,以及 Web 窗体编辑器用于BackColorForeColor控件属性。

有了我们目前所拥有的,我们将在控制器ConfiguredViews旁边看到省略号,但单击时什么都不会发生。

由于我们在GetEditStyle重写中返回的值,该属性知道它必须显示省略号。为了显示窗体并开始编辑,我们必须重写EditValue方法。

public override object EditValue(ITypeDescriptorContext context, 
  IServiceProvider provider, object value)
{
  //Always return a valid value.

  object retvalue = value;

  try
  {      
    IWindowsFormsEditorService srv = null;
    IDesignerHost host = (IDesignerHost)
      context.GetService(typeof(IDesignerHost));

    //Get the forms editor service from the provider, to display the form.

    if (provider != null)
      srv = (IWindowsFormsEditorService)
        context.GetService(typeof(IWindowsFormsEditorService));

尽管我们可以简单地创建新窗体实例并调用其ShowDialog方法,但正确的方式是在属性浏览器内部通过IWindowsFormsEditorService,这是属性浏览器本身提供的一项服务。

    if (srv != null && host != null)
    {
      BaseController controller = (BaseController) context.Instance;

      //Get the designer so we can pass it to the form.

      ViewMappingsEditorForm form = new ViewMappingsEditorForm(
            host, (BaseController) context.Instance);

我们稍后会看到窗体代码,但请注意,我们将设计器主机的引用传递给它。我们这样做是因为窗体与我们迄今为止使用的类型转换器不同,它不了解环境,如果它需要从 IDE 请求服务,它将没有任何上下文来请求它。因此,它将保留我们传递给构造函数的引用的主机,用于此目的。

      //Show form.

      if (srv.ShowDialog(form) == DialogResult.OK)
      {
        //Set the value through the descriptor as usual.

        context.PropertyDescriptor.SetValue(
          context.Instance, form.ConfiguredMappings);
      }
    }
  }
  catch (Exception ex)
  {
    System.Windows.Forms.MessageBox.Show(
      "An exception occurred during edition: " + ex.ToString());
  }
  
  return retvalue;
}

请注意,在显示窗体后,我们使用PropertyDescriptor将新值设置在控制器上。这会通知 IDE 更改并再次触发代码生成过程。窗体为我们提供了用户通过窗体选择的新设置的属性(ConfiguredMappings)。

我们不会显示所有窗体代码,因为它主要是 Windows 窗体(无聊) UI 交互代码。但有一些重要要点我们将重点介绍。

首先,每当我们需要处理一个对象的实例而我们只有其 ID 时,我们都应该像以前一样通过IReferenceService。除了从GetInstance方法检索实例外,此服务还允许我们避免 Windows 和 Web 窗体在命名其控件的方式上的一些“不一致”。前者使用控件的Name属性,后者使用控件的ID。我们可以使用该服务的GetName方法来避免硬编码这种差异。我们在InitControls方法中这样做,在其中我们将可用控件加载到相应的下拉列表中。

void InitControls(object state)
{
  ...
    cbControl.Items.Add(
      new ControlEntry(control, _reference.GetName(control)));

ControlEntry是一个帮助结构,它只是提供了一个自定义的ToString重写,该重写会显示在下拉列表中。这样,我们就可以集中填充,并使其更容易以后找到一个项目。

struct ControlEntry
{
  public object Control;
  public string Name;

  public ControlEntry(object control, string name)
  {
    this.Control = control;
    this.Name = name;
  }
  public override string ToString()
  {
    string id = this.Name.PadRight(20, ' ');
    return id + " [" + Control.GetType().Name + "]";
  }
}

另一个重要问题与多线程有关。如果窗体初始化成本高昂,您可能会倾向于启动一个新线程来执行它。我们可以使用以下代码在新线程中调用InitControls方法:

ThreadPool.QueueUserWorkItem(new WaitCallback(InitControls));

但是,如果您的初始化代码必须通过主机请求服务,这将不起作用。会抛出异常。这是因为服务预计是从主应用程序(VS.NET)线程中获取的。如果您不需要检索服务,或者例如,您是从Load事件处理程序内部检索的,那么就没有问题。

根据具体的应用程序和服务,我们的组件可能使用我们用于实例化具体对象的限定类型名称。例如,您的应用程序可能允许用户通过下拉列表定义将处理某个请求或应用程序功能的类型。您甚至可以从数据库获取列表。加载类型并反射它以显示其成员(例如,在窗体中)的常用方法是通过Type.GetType

Type t = Type.GetType("Mvc.Components.Controller.ViewInfo, Mvc.Components");

这在设计时仅对 GAC 中的程序集有效。这是因为 IDE 不是从项目存储所在的位置运行的。因此,运行 IDE 的进程无法找到程序集,即使它们被项目引用。还有另一项服务为我们提供了类型加载功能,即ITypeResolutionService。此服务提供了一个GetType方法,该方法通过考虑当前项目中引用的程序集来正确查找程序集。

在这个编辑器中,我们可以利用IControllerService来填充下拉列表中的值。

private void cbControlProperty_DropDown(object sender, System.EventArgs e)
{
  cbControlProperty.Items.Clear();

  try
  {
    if (lstMappings.SelectedItem == null || cbControl.SelectedItem == null)
      return;
    
    IControllerService svc = (IControllerService)
      _host.GetService(typeof(IControllerService));
    object control = ((ControlEntry) cbControl.SelectedItem).Control;
    cbControlProperty.Items.Clear();
    cbControlProperty.Text = String.Empty;
    cbControlProperty.Items.AddRange(svc.GetControlProperties(control));
    ...

在这里,我们可以立即体会到拥有全局可用的服务以避免代码重复(主要是与类型转换器)的好处。

自定义和全局命令

待解决的问题是如何提供一种简单的方法来一步检查所有已配置的映射。IControllerService有一个名为VerifyMappings的方法,用于此目的,其实现如下:

public void VerifyMappings(BaseController controller)
{
  string result = VerifyOne(controller);
  if (result == String.Empty)
    System.Windows.Forms.MessageBox.Show("Verification succeeded.");
  else
    System.Windows.Forms.MessageBox.Show(result);
}

VerifyOne检查控制器中每个ViewInfo中模型和控件值的引用。

string VerifyOne(BaseController controller)
{
  //Use reference service as always.

  IReferenceService svc = 
    (IReferenceService) _host.GetService(typeof(IReferenceService));
  StringWriter w = new StringWriter();

  Hashtable models = new Hashtable(controller.Components.Count);
  ArrayList names = new ArrayList(controller.Components.Count);

  foreach (IComponent comp in controller.Components)
  {
    if (comp is BaseModel)
    {
      BaseModel model = (BaseModel) comp;
      if (names.Contains(model.ModelName))
      {
        w.WriteLine("The model name '{0}' is" + 
            " duplicated in the controller.", model.ModelName);
      }
      else
      {
        models.Add(model.ModelName, model);
      }
    }
  }

  foreach (DictionaryEntry entry in controller.ConfiguredViews)
  {
    ViewInfo info = (ViewInfo) entry.Value;
    object ctl = svc.GetReference(info.ControlID);
    if (ctl == null)
    {
      w.WriteLine("Control '{0}' associated with the view mapping " +
              "in controller '{1}' doesn't exist in the form.", 
              info.ControlID, svc.GetName(controller));
    }
    else
    {
      if (ctl.GetType().GetProperty(info.ControlProperty) == null)
        w.WriteLine("Control property '{0}' can't be found in " + 
                "control '{1}' in controller '{2}'.", 
                info.ControlProperty, info.ControlID, svc.GetName(controller));
    }
  }

  return w.ToString();
}

现在我们需要一种执行此命令的方法。我们可以通过重写ControllerDesignerVerbs属性来为我们的控制器添加项到上下文菜单。

public override DesignerVerbCollection Verbs
{
  get 
  {
    DesignerVerb[] verbs = new DesignerVerb[] { 
      new DesignerVerb("Verify this controller mappings ...", 
        new EventHandler(OnVerifyOne)) };
    return new DesignerVerbCollection(verbs); }
}

当选择菜单项时,将调用我们的处理程序。

void OnVerifyMappings(object sender, EventArgs e)
{
  IControllerService svc = (IControllerService) 
    GetService(typeof(IControllerService));
  svc.VerifyMappings(CurrentController);
}

请注意,当公共功能被移至一等 IDE 服务时,代码变得非常简单。有了动词,我们将看到修改后的上下文菜单。

另一种常见的做法是将最常用的编辑器添加到此上下文菜单中。例如,我们可以为“编辑映射”添加一个菜单项。这似乎是显而易见的,但也有细微之处。第一步是添加另一个动词,这很容易。

public override DesignerVerbCollection Verbs
{
  get 
  {
    DesignerVerb[] verbs = new DesignerVerb[] { 
      new DesignerVerb("Verify this controller mappings ...", 
        new EventHandler(OnVerifyOne)),
      new DesignerVerb("Edit View mappings ...", 
        new EventHandler(OnEditMappings)) };
    return new DesignerVerbCollection(verbs); }
}

回想一下,我们使用了类型Editor来实现ConfiguredViews属性编辑。现在我们可以直接调用ViewMappingsEditor.EditValue方法,这将是很好的。

public override object EditValue(
  ITypeDescriptorContext context, IServiceProvider provider, object value)

但看看我们必须传递给它的参数,我们从哪里获取ITypeDescriptorContext呢?好吧,我们可以实现自己的,界面毕竟并不那么复杂。

public class DesignerContext : ITypeDescriptorContext
{
  public void OnComponentChanged() {} 
  public bool OnComponentChanging() { return true; }
  
  public IContainer Container
  {
    get { return _container; }
  } IContainer _container;
  
  public object Instance
  {
    get { return _instance;  }
  } object _instance;

  public PropertyDescriptor PropertyDescriptor
  {
    get { return _property; }
  } PropertyDescriptor _property;

  public object GetService(System.Type serviceType)
  {
    return _host.GetService(serviceType);
  }

  IDesignerHost _host;

  public DesignerContext(IDesignerHost host, 
    IContainer container, object instance, string property)
  {
    _host = host;
    _container = container;
    _instance = instance;
        _property = TypeDescriptor.GetProperties(instance)[property];      
  }
}

它基本上是属性的一个占位符,并将任何GetService请求传递给设计器主机。我们现在可以尝试直接调用编辑器。

void OnEditMappings(object sender, EventArgs e)
{
  UITypeEditor editor = new ViewMappingsEditor();

  ITypeDescriptorContext ctx = new DesignerContext(
    (IDesignerHost) GetService(typeof(IDesignerHost)), 
    this.Component.Site.Container,
    this.Component, 
    "ConfiguredViews");
  
  editor.EditValue(ctx, this.Component.Site, 
    CurrentController.ConfiguredViews);

不幸的是,这行不通。失败点将在我们检索IWindowsFormsEditorService时,在EditValue内部。

srv = (IWindowsFormsEditorService)
  context.GetService(typeof(IWindowsFormsEditorService));

请注意,我们直接向主机请求服务(因为我们实现了ITypeDescriptorContext)。无论如何,srv变量将始终为null。为什么?答案是这项服务不是由 IDE 本身提供的,而是由属性网格提供的,更确切地说,是内部类System.Windows.Forms.PropertyGridInternal.PropertyGridView,它通过返回自身来响应对此服务的请求(因为它实现了服务接口)。查看此类的ShowDialog实现的 IL 代码会揭示该过程的一些复杂性,其中包括计算对话框位置、设置焦点并将调用传递给另一个服务IUIService。这就是为什么我们一开始在编辑器中没有直接实例化一个窗体并调用ShowDialog的原因!

我们不会复制所有这些代码,所以我们将直接实例化窗体,因为在设计器动词内部这样做没有限制。

void OnEditMappings(object sender, EventArgs e)
{
  try
  {
    //Inevitably, code is almost duplicated compared with the editor...

    ViewMappingsEditorForm form = new ViewMappingsEditorForm(
      (IDesignerHost) GetService(typeof(IDesignerHost)), 
      CurrentController);

    if (form.ShowDialog() == System.Windows.Forms.DialogResult.OK)
    {
      PropertyDescriptor prop = 
        TypeDescriptor.GetProperties(Component)["ConfiguredViews"];
      prop.SetValue(Component, form.ConfiguredMappings);
    }
  }
  catch (Exception ex)
  {
    System.Windows.Forms.MessageBox.Show(
      "An exception occurred during edition: " + ex.ToString());
  }
}

代码与编辑器本身使用的代码几乎相同,除了调用IWindowsFormsEditorService

如果窗体开发人员需要同时使用多个组件,逐个检查设置可能会很繁琐。我们可以使用另一项 IDE 服务来添加所谓的全局命令。它是IMenuCommandService。由于检查映射是在控制器服务中执行的任务,因此将此新行为放在其中似乎是合乎逻辑的。回想一下,我们的服务是在Initialize方法调用中由控制器设计器实例化的。在服务构造函数中,我们可以挂钩我们的全局命令。

public ControllerService(IDesignerHost host)
{
  _host = host;
    
  IMenuCommandService mcs = (IMenuCommandService) 
    host.GetService(typeof(IMenuCommandService));
  if (mcs != null)
  {
    mcs.AddCommand(new DesignerVerb("Verify all controllers mappings ...", 
      new EventHandler(OnVerifyAll)));
  }
}

我们只能添加设计器动词,它们继承自AddCommand方法接收的MenuCommand类。每当用户单击表单的组件区域中的任何位置时,都会显示此新的上下文菜单项。

请注意,控制器未被选中。我们单击了它旁边的组件表面。处理程序现在逐个迭代所有组件。

void OnVerifyAll(object sender, EventArgs e)
{
  StringBuilder sb = new StringBuilder();
  
  foreach (IComponent component in _host.Container.Components)
  {
    if (component is BaseController)
    {
      sb.Append(VerifyOne((BaseController) component));
    }
  }

  string result = sb.ToString();
  if (result == String.Empty)
    System.Windows.Forms.MessageBox.Show("Verification succeeded.");
  else
    System.Windows.Forms.MessageBox.Show(result);
}

它只是将球传给每个找到的控制器的VerifyOne方法。IDesignerHost.Container.Components属性包含当前存在的所有引用。

如果这有那么容易就好了……不幸的是,如果我们现在右键单击控制器,我们将遗憾地看到全局命令替换了第一个控制器设计器动词。

Verify this controller …项已消失!发生这种奇怪行为的原因是 IDE 首先请求组件动词,然后(非常奇怪地)将所有全局动词放在顶部。我发现的唯一解决方法是在控制器设计器中添加一种“占位符”动词。

public override DesignerVerbCollection Verbs
{
  get 
  {
    DesignerVerb[] verbs = new DesignerVerb[] { 
      //Placeholder for first global command to avoid collisions.

      new DesignerVerb(String.Empty, null), 
      new DesignerVerb("Verify this controller mappings ...", 
        new EventHandler(OnVerifyMappings)), 
      new DesignerVerb("Edit View mappings ...", 
        new EventHandler(OnEditMappings)) 
      };
    return new DesignerVerbCollection(verbs); }
}

所有设计器动词也显示在属性浏览器的底部,方便访问。但不幸的是,我们添加的新空动词也出现在那里!请注意开头的第一个冒号:(

现在,我们只能忍受这种情况,或者放弃全局命令。

现在我们已经具备了所有的 IDE 集成管道,我们开始实际反映模型数据到映射的 UI 小部件。正如我们在分析框架架构时上面所说,控制器将设置/获取 UI 中的值这一责任委托给知道如何处理 Web 和 Windows 窗体的适配器类。

处理不同的视图技术

我们知道我们将支持 Windows 和 Web 窗体。我们将通过在通用的基础抽象类中定义我们需要的来自这两种技术的共同成员来实现这一点,并让每个“适配器”具体实现来执行特定技术所需的必要操作。

Web 和 Windows 窗体在访问和设置子控件值的方式上存在根本差异。我们已经提到了命名差异。这些细节将被隔离到一个我们将提供的新服务中,即IAdapterService。其接口包含我们需要的基本方法。

public interface IAdapterService
{
  object FindControl(string controlId);
  string GetControlID(object control);
  object[] GetControls();
  ComponentCollection GetComponents();
  void RefreshView(BaseController controller);
  void RefreshModels(BaseController controller);
}

两个类实现了这个接口:WebFormsAdapterServiceWindowsFormsAdapterService。两者都非常相似,所以我们将看前者。两个适配器都在构造函数中接收用于解析其他方法的参数。

internal class WebFormsAdapterService : IAdapterService
{
  Page _container;
  IContainer _components;

  internal WebFormsAdapterService(object controlsContainer, 
    IContainer componentsContainer)
  {
    _container = (Page) controlsContainer;
    _components = componentsContainer;
  }

Windows 窗体版本类似,但将controlsContainer参数转换为Form。前四个方法非常简单。您可以查看下载的代码以了解它们的实现。有趣的部分是RefreshViewRefreshModels方法。前者会迭代接收到的控制器中所有已配置的映射,并将相应的控件属性设置为从模型中找到的值。后者则相反。

public void RefreshView(BaseController controller)
{
  //Build a keyed list of models in the controller. 

  Hashtable models = new Hashtable(controller.Components.Count);
  foreach (BaseModel model in controller.Components)
    models.Add(model.ModelName, model);

  //We make extensive use of reflection here. This can be improved.

  foreach (DictionaryEntry entry in controller.ConfiguredViews)
  {
    ViewInfo info = (ViewInfo) entry.Value;
    //Retrieve model object and property.

    object model = models[info.Model];
    PropertyInfo modelprop = model.GetType().GetProperty(info.ModelProperty);
    //Locate control and property.

    Control ctl = (Control) FindControl((string)entry.Key);
    if (ctl == null)
    {
      throw new ArgumentException("The control '" + info.ControlID + 
          "' wasn't found in the current container.");
    }
    else
    {
      PropertyInfo ctlprop = ctl.GetType().GetProperty(info.ControlProperty);
      if (ctlprop == null)
      {
        throw new ArgumentException("The property '" + info.ControlProperty 
          + "' wasn't found in the control '" + info.ControlID + "'.");
      }
      else
      {
        object newvalue = modelprop.GetValue(model, new object[0]);
        ctlprop.SetValue(ctl, newvalue, new object[0]);
      }
    }
  }    
}

我们只是使用反射来加载类型和属性并设置值。反向过程(RefreshModels)是相同的,但不是在控件属性上调用SetValue,而是在模型属性上调用。

我们已经知道如何将新服务挂钩到架构中,所以让我们简要看一下ControllerDesigner.SetupServices方法中执行此任务的代码。

void SetupServices()
{
  //Attach the adapter services at design-time. 

  object service = GetService(typeof(IAdapterService));
  IDesignerHost host = (IDesignerHost) GetService(typeof(IDesignerHost));
  if (host.RootComponent as System.Windows.Forms.Form != null)
  {
    if (service == null)
    {
      host.AddService(
        typeof(IAdapterService), 
        new WindowsFormsAdapterService(host.RootComponent, 
          host.RootComponent.Site.Container),
        false);
    }
  }
  else if (host.RootComponent as System.Web.UI.Page != null)
  {
    if (service == null)
    {
      host.AddService(typeof(IAdapterService), 
        new WebFormsAdapterService(host.RootComponent, 
          host.RootComponent.Site.Container), false);
    }
  }

  //Setup the controller service.

  service = GetService(typeof(IControllerService));
  if (service == null)
  {
    host.AddService(typeof(IControllerService), 
      new ControllerService(host), false);
  }
}

您可能已经注意到,当我们向主机添加任何服务时,我们将false作为最后一个参数传递。该参数指定我们是否要将服务提升到 IDE 架构的更高层。除非您确定该服务是唯一的且永远不需要动态更改,否则不建议这样做。在我们的例子中,我们根据根组件类型切换服务对象,因此我们需要它保留在IDesignerHost中并且易于替换。通过向第三个参数传递false,我们表示该服务仅在当前根设计器的生命周期内有效。

现在我们可以利用这项服务来访问控制器扩展器属性中的映射(我们删除了属性以提高可读性)。

public ViewInfo GetWinViewMapping(object target)
{
  return GetViewMapping(target);
}

public void SetWinViewMapping(object target, ViewInfo value)
{
  SetViewMapping(target, value);
}

public ViewInfo GetWebViewMapping(object target)
{
  return GetViewMapping(target);
}

public void SetWebViewMapping(object target, ViewInfo value)
{
  SetViewMapping(target, value);
}

现在两个属性(Win 和 Web)可以共享一个通用实现,因为适配器服务解决了不一致之处。

public ViewInfo GetViewMapping(object target)
{
  IAdapterService svc = (IAdapterService) GetService(typeof(IAdapterService));
  string id = svc.GetControlID(target);
  ViewInfo info = _views[id] as ViewInfo;
  if (info == null)
  {
    info = new ViewInfo(this, id);
    _views[id] = info;
  }

  info.Controller = this;

  //Verify that we are trapping the changes. 

  if (!info.IsHooked)
  {
    PropertyDescriptorCollection props = TypeDescriptor.GetProperties(info);
      props["ControlProperty"].AddValueChanged(info, 
        new EventHandler(RaiseViewInfoChanged));
      props["Model"].AddValueChanged(info, 
        new EventHandler(RaiseViewInfoChanged));
      props["ModelProperty"].AddValueChanged(info, 
        new EventHandler(RaiseViewInfoChanged));
    info.IsHooked = true;
  }

  return info;
}

public void SetViewMapping(object target, ViewInfo value)
{
  IAdapterService svc = (IAdapterService) GetService(typeof(IAdapterService));
  _views[svc.GetControlID(target)] = value;
}

最后,基础控制器公开内部方法来同步视图和模型。

internal event EventHandler ModelChanged;

protected virtual void InitContext(object sender, EventArgs e)
{
}

internal void Init(object sender, EventArgs e)
{
  InitContext(sender, e);
}

internal void RefreshModels(object sender, EventArgs e)
{
  IAdapterService svc = (IAdapterService) 
    this.Site.GetService(typeof(IAdapterService));
  svc.RefreshModels(this);
}

internal void RefreshView(object sender, EventArgs e)
{
  IAdapterService svc = (IAdapterService)
    this.Site.GetService(typeof(IAdapterService));
  svc.RefreshView(this);
}

protected void RaiseModelChanged(BaseModel model)
{
  if (ModelChanged != null)
    ModelChanged(model, EventArgs.Empty);
}

请注意,这些方法现在非常简单,因为适配器负责与视图的交互。这与原始 MVC 模式的模型略有不同,在原始 MVC 模式中,每个视图技术都有自己的控制器。

但在我们继续进行视图同步之前,我们需要知道视图如何请求控制器加载某个模型。

模型行为,MVC 方式

由于视图不允许直接在模型上执行操作,因此保留了隔离性。因此,控制器公开了导致实际模型方法执行的方法。模型包含行为以及数据,并使用这些数据来保存方法执行的结果或作为某些操作的输入。

一个简单的PublisherModel组件可能除了映射到示例 Pubs 数据库中publishers表字段的属性外,还公开了对实体的三个基本操作:LoadSaveDelete

/// <summary>

/// Loads the current model with data from the database matching the 

/// current ID.

/// </summary>

public void Load()
{
  SqlConnection cn = new SqlConnection(ConnectionString);
  SqlCommand cmd = new SqlCommand(
    "SELECT * FROM publishers WHERE pub_id = '" + this.ID + "'", cn);
  try
  {
    cn.Open();
    SqlDataReader reader =
      cmd.ExecuteReader(CommandBehavior.CloseConnection);
    if (reader.Read())
    {
      this.ID = reader["pub_id"] as string;
      this.Name = reader["pub_name"] as string;
      this.City = reader["city"] as string;
      this.State = reader["state"] as string;
      this.Country = reader["country"] as string;
    }
    reader.Close();
  }
  finally
  {
    if (cn.State != ConnectionState.Closed)
      cn.Close();
  }
}

请注意,该方法使用发布者 ID 从其自身的属性加载。同样,无论它加载什么都会放回模型属性中。其他两个方法的实现也类似。

正如我们所说,为了访问这些模型行为,必须通过控制器公开。

public class PublisherController : BaseController
{
  public void DeletePublisher()
  {
    model.Delete();
    RaiseModelChanged(model);
  }

  public void LoadPublisher()
  {
    model.Load();
    RaiseModelChanged(model);
  }

  public void SavePublisher()
  {
    model.Save();
    RaiseModelChanged(model);
  }

基本上,它们将方法调用传播到包含的模型,并最终引发一个视图可以使用来更新其控件值的事件。

在 Windows 窗体按钮单击和 Web 窗体按钮单击中,我们都可以放置以下代码来在模型中执行操作:

private void btnLoad_Click(object sender, System.EventArgs e)
{
  controller.LoadPublisher();
}

private void btnSave_Click(object sender, System.EventArgs e)
{
  controller.SavePublisher();
}

private void btnDelete_Click(object sender, System.EventArgs e)
{
  controller.DeletePublisher();
}

请注意,没有“平台”特定的代码。没有一行。但是控制器是如何以及何时更新 UI 的?Web 和 Windows 环境中的这种时序存在根本差异。在前者中,UI 通常在Load事件上加载,因为回发会导致新的Load事件被触发。然而,Windows 窗体需要在Load之外的其他时间刷新视图,因为进一步的操作不会导致新的Load事件。为了提供这种包含窗体、其事件和控制器操作(即RefreshViewRefreshModels)之间的连接性,我们使用连接器

连接视图

连接器的责任是在适当的时间调用控制器方法来同步模型和视图。基本上,UI 容器(Web 或 Windows 窗体)应该实例化相应的连接器并调用Connect,传递相关的参数来执行布线。我们像往常一样提供一个基类,两个连接器都将从它继承。

public abstract class BaseConnector
{
  public abstract void Connect(BaseController controller, 
    object controlsContainer, IContainer componentsContainer);
}

为了使过程自动化,我们将从控制器代码生成内部发出具体的连接代码,在检测到托管技术后。这是ControllerCodeDomSerializer方法中的相关代码。

public override object 
       Serialize(IDesignerSerializationManager manager, object value)
{
  ... 
  
  //Emit code for the appropriate adapter.

  //new WebFormsAdapter.Connect(controller, this, this.components);

  if (host.RootComponent as System.Windows.Forms.Form != null)
  {
    CodeObjectCreateExpression adapter = new CodeObjectCreateExpression(
      typeof(Connector.WindowsFormsConnector), new CodeExpression[0]);
    CodeExpression connect = new CodeMethodInvokeExpression(
      adapter, "Connect", 
      new CodeExpression[] {
        cnref, 
        new CodeThisReferenceExpression(), 
        new CodeFieldReferenceExpression(
          new CodeThisReferenceExpression(), 
          "components") 
      });
    statements.Add(connect);
  }
  else if (host.RootComponent as System.Web.UI.Page != null)
  {
    CodeObjectCreateExpression adapter = new CodeObjectCreateExpression(
      typeof(Connector.WebFormsConnector), new CodeExpression[0]);
    CodeExpression connect = new CodeMethodInvokeExpression(
      adapter, "Connect", 
      new CodeExpression[] {
        cnref, 
        new CodeThisReferenceExpression(),
        new CodeFieldReferenceExpression(
          new CodeThisReferenceExpression(), "components") 
      });
    statements.Add(connect);
  }

放置在 Web 窗体中的控制器会生成以下代码:

private void InitializeComponent()
{    
  ...
  // 

  // publisherController

  // 

  this.publisherController.ConnectionString = 
    ((string)(configurationAppSettings.GetValue("Pubs", typeof(string))));
  // 

  // ------------- ClariuS Custom Code -------------

  // Controller code

  // 

  this.publisherController.ConfiguredViews.Add("txtID", 
       new Mvc.Components.Controller.ViewInfo("txtID", 
       "Text", "Publisher", "ID"));
  // Connect the controller with the hosting environment.

  new Mvc.Components.Connector.WebFormsConnector().Connect(
    this.publisherController, this, this.components);
  ...
}

放置在 Windows 窗体中的同一个组件会生成:

private void InitializeComponent()
{    
  ...
  // 

  // publisherController

  // 

  this.publisherController.ConnectionString = 
    ((string)(configurationAppSettings.GetValue("Pubs", typeof(string))));
  // 

  // ------------- ClariuS Custom Code -------------

  // Controller code

  // 

  this.publisherController.ConfiguredViews.Add("txtID", 
       new Mvc.Components.Controller.ViewInfo("txtID", 
       "Text", "Publisher", "ID"));
  // Connect the controller with the hosting environment.

  new Mvc.Components.Connector. WindowsFormsConnector().Connect(
    this.publisherController, this, this.components);
  ...
}

请注意,唯一的区别是实例化的具体连接器。一个重要的注意点是,此方法永远不会在设计时调用。IDE 只加载组件上的属性,但不会调用方法。

Web 窗体连接器相当简单:

public class WebFormsConnector : BaseConnector
{
  public override void Connect(BaseController controller, 
    object controlsContainer, IContainer componentsContainer)
  {
    //Connect lifecycle events.

    Page page = (Page) controlsContainer;
    page.Init += new EventHandler(controller.Init);
    page.Load += new EventHandler(controller.RefreshModels);
    page.PreRender += new EventHandler(controller.RefreshView);
  }
}

请注意,这将控制器挂钩到页面事件,导致它们在适当的时间被调用。

让我们总结一下在Load事件中将发生的交互:

这里的关键点是Site.GetService()方法调用,它必须为控制器提供一个适当的适配器实例。我们已经测试过在设计时附加服务时服务就在那里。但是设计器只在设计时调用,那么运行时会发生什么?

回到开头,当我们介绍这个 .NET 组件面向对象架构时,我们说过 UI 控件和非可视化组件之间存在差异。现在我们能够解释这种差异是什么。在设计时,两种组件都位于DesignSite中并包含在DesignHost中,正如当时所示。但在运行时,组件会将this.components 字段作为容器传递,它们在该容器上被定位,该容器只是一个类型为Container的类。相应的站点类型为Container.Site。问题是GetService方法在Container类内部实现,它只返回它本身提供的IContainer类型的服务。所有其他服务都丢失了。更糟的是,可视化控件根本没有Site,这就是区别。

我们可以做的是执行一些运行时初始化,并提供一个自定义站点,该站点可以响应我们IAdapterService的请求。请记住,服务实例必须始终保存在某个地方,就像 VS.NET 在设计时保存它一样。我们将创建一个实现ISiteRuntimeSite类,我们可以用它来“定位”组件,并使用我们自己的基础设施。

/// <summary>

/// Provides a custom site to be used at run-time, so that components can 

/// request our custom services just as if they were at design-time.

/// </summary>

public class RuntimeSite : ISite
{
  public RuntimeSite(IContainer container, 
    IComponent component, GetServiceEventHandler getServiceCallback)
  {
    _container = container;
    _component = component;
    _callback = getServiceCallback;
  }

  public IComponent Component
  {
    get {  return _component; }
  } IComponent _component;

  public IContainer Container
  {
    get { return _container; }
  } IContainer _container;

  public bool DesignMode
  {
    get { return false; }
  }

  public string Name
  {
    get { return String.Empty; }
    set { }
  }

  /// <summary>

  /// Passes the call for a service to the handler received 

  /// in the constructor.

  /// </summary>

  public object GetService(Type serviceType)
  {
    return _callback(this, new GetServiceEventArgs(serviceType));
  } GetServiceEventHandler _callback;
}

我们的构造函数除了接收当前容器和组件外,还接收一个委托。我们说服务实例在运行时必须保存在某个地方。提供了此回调,以便当组件请求服务时,调用将被传递到此委托,该委托可以根据所用技术的需要进行实现。例如,Web 版本可以使用HttpContext来保存对象,而 Windows 窗体版本则不能。委托让两者都有机会以任何他们想要的方式回答GetService请求。委托类及其参数是:

public delegate object GetServiceEventHandler(object sender, 
                                     GetServiceEventArgs e);

public class GetServiceEventArgs
{
  public GetServiceEventArgs(Type serviceType)
  {
    _service = serviceType;
  }

  public Type ServiceType
  {
    get { return _service; }
  } Type _service;
}

让我们看一下完整的 Web 连接器构造函数,它还初始化运行时站点。

public override void Connect(BaseController controller, 
  object controlsContainer, IContainer componentsContainer)
{
  //Retrieve or hook the service as needed.

  WebFormsAdapterService service;

  if (!HttpContext.Current.Items.Contains(typeof(IAdapterService).FullName))
  {
    service = new WebFormsAdapterService(controlsContainer, 
      componentsContainer);
    HttpContext.Current.Items.Add(typeof(IAdapterService).FullName, service);
  }  
  else
  {
    service = (WebFormsAdapterService) 
      HttpContext.Current.Items[typeof(IAdapterService).FullName];
  }

  //The handler for GetService is implemented in the service itself.

  controller.Site = new RuntimeSite(componentsContainer, controller, 
    new GetServiceEventHandler(service.GetServiceHandler));

  //Set new site on each component.

  foreach (IComponent component in controller.Components)
  {
    component.Site = new RuntimeSite(controller, component, 
      new GetServiceEventHandler(service.GetServiceHandler));
  }

    //Connect lifecycle events.

    Page page = (Page) controlsContainer;
    page.Init += new EventHandler(controller.Init);
    page.Load += new EventHandler(controller.RefreshModels);
    page.PreRender += new EventHandler(controller.RefreshView);
  }
}

WebFormsAdapterService.GetServiceHandler方法负责响应来自组件的请求。

internal object GetServiceHandler(object sender, GetServiceEventArgs e)
{
  if (e.ServiceType == typeof(IAdapterService))
  {
    return this;
  }
  else
  {
    return null;
  }
}

很简单,连接器的 Web 版本首先将服务存储在HttpContext中,然后让服务本身响应请求服务的组件。现在,控制器在其RefreshModels方法中将被调用在Page.Load时间,将成功检索服务。

public class BaseController : Component, IContainer, IExtenderProvider
{
  internal void RefreshModels(object sender, EventArgs e)
    {
      IAdapterService svc = (IAdapterService) 
        this.Site.GetService(typeof(IAdapterService));
      svc.RefreshModels(this);
    }

该框架的 Web 版本现在已完成。当页面加载时,模型会根据用户在表单上发布的(或最初加载的)任何值进行更新。在渲染到客户端之前,通常在所有按钮单击、文本框更改等事件处理程序都被调用之后,控制器会使用模型中最终的值刷新视图。显示发布者数据并具有正确映射的 Web 窗体,可以通过我们每个按钮处理程序中看到的单行代码提供加载/保存/删除功能。

private void btnLoad_Click(object sender, System.EventArgs e)
{
  controller.LoadPublisher();
}

现在,假设我们在相应的字段中输入一个ID并单击Load,这就是页回到我们之前的操作序列:

  • Load时,适配器服务被挂钩,组件被定位。
  • 模型被刷新,因此txtID中的值被放置在PublisherModel.ID属性中。
  • 事件处理程序调用控制器方法LoadPublisher()
  • 模型转到数据库并使用返回的行加载自身。
  • 在渲染之前,控件值会根据模型中的新值进行更新(完整的发布者数据)。
  • 页面以值呈现并发送到浏览器。

页面设计者只需使用我们的 IDE 集成功能配置映射,并在适当的时候调用相应的控制器方法即可!

更重要的是,如果我们以后决定(或同时)移动到 Windows 窗体客户端,相同的控制器和映射将完成工作。甚至 UI 事件的事件处理程序也会看起来相同!

由于 Windows 窗体支持有状态应用程序,我们可以简单地将相关变量保留在连接器本身中。但是,在 Web 中,初始化时,我们知道所有控件都已创建。Windows 窗体的情况则不是这样,控件只是类级别的变量,必须在InitializeComponent方法中初始化,就像我们的控制器一样。因此,不能保证我们的连接器会在控件初始化后被调用。

为什么我们需要在初始化时访问控件?因为与 Web 中的模型刷新只有一个点(Load事件)不同,在 Windows 窗体中,我们必须在控件修改后立即刷新模型,并且我们必须挂钩到该事件并触发模型刷新。

所以我们必须求助于一些窗体事件来执行实际的挂钩。

public class WindowsFormsConnector : BaseConnector
{
  IAdapterService _service;
  BaseController _controller;

  public override void Connect(BaseController controller, object 
    controlsContainer, IContainer componentsContainer)
  {
    _service = new WindowsFormsAdapterService(controlsContainer, 
        componentsContainer);
    _controller = controller;

    //Connect components to a run-time site.

    controller.Site = new RuntimeSite(componentsContainer, controller, 
        new GetServiceEventHandler(GetServiceHandler));
    foreach (IComponent component in controller.Components)
    {
      component.Site = new RuntimeSite(controller, component, 
        new GetServiceEventHandler(GetServiceHandler));
    }

    //Connect lifecycle events.

    Form form = (Form) controlsContainer;
    //First connect control events.

    form.Activated += new EventHandler(OnActivated);
    //Initialize controller context next.

    form.Activated += new EventHandler(controller.Init);
    //Refresh control values at loading time.

    form.Load += new EventHandler(controller.RefreshView);
    //Persist in model at deactivation time.

    form.Deactivate += new EventHandler(controller.RefreshModels);
  }

如您所见,每个连接器(每个控制器一个)将拥有服务和相应的连接器。OnActivated处理程序负责将RefreshModels方法挂钩到每个控件的Leave事件。

  void OnActivated(object sender, EventArgs e)
  {
    //Just a simple implementation based on focus changes.

    foreach (DictionaryEntry entry in _controller.ConfiguredViews)
    {
      Control ctl = (Control) 
        _service.FindControl(((ViewInfo)entry.Value).ControlID);
      ctl.Leave += new EventHandler(_controller.RefreshModels);
    }
    //Refresh the form view.

    _controller.RefreshView(sender, e);
    //Attach the model changed event to the controller refresh method, 

    //so that the view is automatically updated.

    _controller.ModelChanged += new EventHandler(_controller.RefreshView);
  }

我们在最后做的另一个技巧是将控制器触发的ModelChanged事件挂钩到同一个控制器的RefreshView方法。这样,我们就可以实现自动 UI 更新,而无需编写任何代码。

最后,处理GetService请求的委托(我们为此传递给每个组件的新RuntimeSite)只需返回它保留的服务引用。

  object GetServiceHandler(object sender, GetServiceEventArgs e)
  {
    if (e.ServiceType == typeof(IAdapterService))
    {
      return _service;
    }
    else
    {
      return null;
    }
  }
}

现在,这两个(除了连接器实例)完全相同的代码可以存在于 Windows 和 Web 窗体应用程序中,并且行为完全相同,甚至在配置级别也是如此。

超越您自己的组件

通过代码在设计时持久化肯定比运行时检测和发出具有优势:它是编译的,并且速度总是更快。例如,我们可能希望在渲染的 Web 窗体控件上发出一些属性,我们可以在客户端 JavaScript 中使用这些属性来了解映射的详细信息。同样,有一种自然的方法似乎很明显:挂钩到PreRender并再次迭代配置的映射,将相关属性添加到控件。

如果映射和控件在设计时已知,为什么我们必须在每次页面交互时浪费宝贵的运行时处理时间进行这种迭代?答案在于一种更好的利用方式。它被称为IDesignerSerializationProvider

实现此接口的对象可以注册到IDesignerSerializationManager(我们在控制器序列化器中接收到的)。

internal class ControllerCodeDomSerializer : BaseCodeDomSerializer
{
  public override object 
         Serialize(IDesignerSerializationManager manager, object value)
  {
    ...

    //Add a sample serializer for web controls.

    if (!(manager.GetSerializer(typeof(System.Web.UI.Control), 
        typeof(CodeDomSerializer)) is WebControlSerializer))
    {
      manager.AddSerializationProvider(
        new WebControlSerializationProvider());
    }

我们首先检查提供程序是否已被添加。在序列化时,管理器将调用每个配置的提供程序,并给它一个机会为某个类型提供自定义序列化器。这是在GetSerializer方法中完成的。

internal class WebControlSerializationProvider : 
    IDesignerSerializationProvider
{
  /// <summary>

  /// Returns our custom serializer only if the type received implements 

  /// <see cref="IAttributeAccessor"/>, the only common ground 

  /// (for attribute settings) for HtmlControls and WebControls.

  /// </summary>

  public object GetSerializer(IDesignerSerializationManager manager, 
    object currentSerializer, Type objectType, Type serializerType)
  {
    if (typeof(IAttributeAccessor).IsAssignableFrom(objectType) && 
        serializerType == typeof(CodeDomSerializer))
      return new WebControlSerializer();
    else
      return null;
  }
}

如果我们找到一个我们可以附加属性的控件(实现了IAttributeAccessor),我们就返回我们的新序列化器。从现在开始,每当 IDE 需要序列化 Web 控件(无论是HtmlControl还是WebControl)时,我们的序列化器都将被调用,并接收正在被序列化的控件。

internal class WebControlSerializer : BaseCodeDomSerializer
{
  public override object Serialize(
    IDesignerSerializationManager manager, object value)
  {
    ...
  }

为了避免干扰正常的序列化过程,我们将首先检索接收到的控件的常规序列化器,并从中获取代码语句。我们在基类序列化器中定义了一个帮助方法来获取适当的类型。

protected CodeDomSerializer GetConfiguredSerializer(
    IDesignerSerializationManager manager, object value)
{
  //Get the attribute anywhere in the inheritance chain.

  object[] attrs = value.GetType().GetCustomAttributes(
    typeof(DesignerSerializerAttribute), true);
  if (attrs.Length == 0) return null;
  
  DesignerSerializerAttribute serializer = 
    (DesignerSerializerAttribute) attrs[0];

  ITypeResolutionService svc = (ITypeResolutionService)
    manager.GetService(typeof(ITypeResolutionService));
  Type t = svc.GetType(serializer.SerializerTypeName);

  return (CodeDomSerializer) Activator.CreateInstance(t);
}

这里我们看到ITypeResolutionService在起作用。在我们检索到相关的属性后,我们所拥有的只是限定的类型名称,我们必须通过该服务加载它,以确保它所在的程序集被找到并成功加载。之后,我们只需使用Activator.CreateInstance方法创建序列化器并返回它。

让我们回到WebControlSerializationProvider.Serialize方法。

public override object Serialize(
  IDesignerSerializationManager manager, object value)
{
  CodeDomSerializer serial = GetConfiguredSerializer(manager, value);
  if (serial == null) 
    return null;

  CodeStatementCollection statements = (CodeStatementCollection)
    serial.Serialize(manager, value);

当属性被扩展时,如果使用TypeDescriptor,它将作为对象本身的属性出现。

  PropertyDescriptor prop = 
    TypeDescriptor.GetProperties(value)["WebViewMapping"];
  ViewInfo info = (ViewInfo) prop.GetValue(value);

现在我们需要生成以下代码来添加一个属性来存储控制器名称,例如。

((IAttributeAccessor)control).SetAttribute("MVC_Controller", "controller");

我们还将对其他四个ViewInfo属性执行此操作。

  //Attach the view mappings to the control attributes.

  if (info.ControlProperty != String.Empty && 
    info.Model != String.Empty && info.ModelProperty != String.Empty)
  {
    //Temp variables

    CodeExpression ctlref = SerializeToReferenceExpression(
      manager, value);
    CodeCastExpression cast = 
      new CodeCastExpression(typeof(IAttributeAccessor), ctlref);

    //Emits:

    //((IAttributeAccessor)control).SetAttribute("MVC_" + [key], [value]);

    statements.Add(new CodeMethodInvokeExpression(
        cast, "SetAttribute", new CodeExpression[] {
          new CodePrimitiveExpression("MVC_Controller"), 
          new CodePrimitiveExpression(manager.GetName(info.Controller)) 
                          }));
    statements.Add(new CodeMethodInvokeExpression(
        cast, "SetAttribute", new CodeExpression[] {
          new CodePrimitiveExpression("MVC_Model"), 
          new CodePrimitiveExpression(info.Model) 
                          }));
    //The other two are the same.

    ...
    return statements;
  }
}

我们定义了我们反复使用的表达式,例如转换为IAttributeAccessor和控件引用。

现在我们可以导致页面上的代码生成过程,例如,通过更改视图映射,然后更改 Web 控件属性。新序列化的代码如下所示:

private void InitializeComponent()
{    
  this.components = new System.ComponentModel.Container();
  System.Configuration.AppSettingsReader configurationAppSettings = 
      new System.Configuration.AppSettingsReader();
  this.publisherController = new PubsMVC.PublisherController(this.components);
  ((System.Web.UI.IAttributeAccessor)(this.txtID)).SetAttribute("MVC_Controller", 
                                                          "publisherController");
  ((System.Web.UI.IAttributeAccessor)(this.txtID)).SetAttribute("MVC_Model", 
                                                               "Publisher");
  ...

现在呈现的页面带有属性,而无需在运行时动态添加它们。

<table id="Table1" cellspacing="5" 
                   cellpadding="1" width="217" border="0"   style="...">
  <tr>
    <td>ID:</td>
    <td>
      <input name="txtID" type="text" 
        value="0736" id="txtID" 
        MVC_Controller="publisherController" 
        MVC_Model="Publisher" 
        MVC_ModelProperty="ID" 
        MVC_ControlProperty="Text" />
    </td>
  </tr>
  <tr>
    <td>Name:</td>
    <td>
      <input name="txtName" type="text" 
        value="New Moon Book" id="txtName" 
        MVC_Controller="publisherController" 
        MVC_Model="Publisher" 
        MVC_ModelProperty="Name" 
        MVC_ControlProperty="Text" />
    </td>
  </tr>
...

作为旁注,我们必须指出ViewInfo对象包含一个指向创建它的控制器的Controller属性。这就是我们将它的名称发出到页面的方式。这是我们特有的实现细节,但如果该属性不存在怎么办?我们如何检索实际的属性提供程序,备份控件上一个“虚假”的属性?

答案并不容易,不幸的是。当我们向TypeDescriptor请求属性时,我们期望得到一个PropertyDescriptor。但是,当属性被扩展时,在一个派生自它的类中,会返回ExtendedPropertyDescriptor。这个对象有一个Provider属性,它引用提供此属性的对象。太棒了!还有什么可以问的?嗯,我们可以要求类不要是私有的

所以,既然我们运气不好,我们就必须找到一个解决方法,像往常一样。技巧来自反射。请注意,我们将要使用的技术风险很高。如果 MS 决定更改类名或属性名,甚至删除它,您现在可以工作的代码将不再起作用。而且他们不会做任何错误的事情,因为界面是私有的。现在您已意识到,让我们开始吧!

为了实现这一点,我们必须首先加载类型。通过Assembly.GetType方法,我们无法访问private类型。我们必须使用GetTypes并迭代所有类型并找到匹配的类型。我们创建了一个实用类DesignUtils,其中有一个LoadPrivateType可以做到这一点。它的用法很简单:

Type t = DesignUtils.LoadPrivateType(
  "System.ComponentModel.ExtendedPropertyDescriptor,System");

请注意,我们避免了传递完全限定的程序集名称,方法代码如下:

internal static Type LoadPrivateType(string className)
{
  string[] names = className.Split(',');
  if (names.Length < 2) 
    throw new ReflectionTypeLoadException(null, null,
      "Invalid class name: " + className);

  //Try to load className from the referenced assemblies. 

  //If it is not referenced, throw.

  AssemblyName[] asmNames = 
    Assembly.GetExecutingAssembly().GetReferencedAssemblies();
  Assembly asm = null;

  foreach (AssemblyName name in asmNames)
  {
    if (String.CompareOrdinal(name.Name, names[1]) == 0)
    {
      asm = Assembly.Load(name);
      break;
    }
  }
  if (asm == null) 
    throw new ReflectionTypeLoadException(null, null, 
      names[1] + " assembly couldn't be loaded.");

  //Locate the type. Iteration is required because it's not public.

  Type type = null;
  foreach (Type t in asm.GetTypes())
  {
    if (String.CompareOrdinal(t.FullName,names[0]) == 0)
    {
      type = t;
      break;
    }
  }

  if (type == null) 
    throw new ReflectionTypeLoadException(null, null, 
      className + " wasn't found.");

  return type;
}

现在我们有了私有的反射类型,我们可以像往常一样通过以下方式获取属性:

PropertyInfo provider = t.GetProperty("Provider");

最后,我们可以通过使用属性信息GetValue方法来获取实际的提供程序实例。

object instance = provider.GetValue(prop, new object[0]);

我们将此行为封装在DesignUtils的静态字段中,以便此过程只发生一次。从 Web 控件自定义序列化器内部,我们可以通过以下代码获取对控制器的引用:

PropertyDescriptor prop = TypeDescriptor.GetProperties(value)["WebViewMapping"];
ViewInfo info = (ViewInfo) prop.GetValue(value);

BaseController controller = (BaseController)
  DesignUtils.ProviderProperty.GetValue(prop, new object[0]);

最终信息

这个 MVC 风格框架的原型实现远非完整或达到生产质量。但是,我们已经探索了 IDE 设计时架构的几乎所有方面,甚至将其概念扩展到运行时,并利用了整个 .NET 框架的内在组件面向对象特性。因此,即使 Web 窗体实现特别薄弱(因为我们在每次更改时都会导致完整的视图和模型刷新),它确实证明了该概念。更精细化的控制,以及更丰富的控制器事件模型,将极大地有益于架构。

与 VS.NET 的深度集成可以极大地提高开发人员的生产力,通过使用自定义代码生成技术,我们可以实现相当复杂的组件持久化,甚至强制执行某些架构决策,例如我们为模型访问所做的决策。

对于研究组件持久化其他领域的人来说,您应该知道组件也可以存储在资源文件中。更重要的是,我们可以附加我们自己的序列化提供程序,就像我们为 Web 窗体控件所做的那样,以序列化到其他媒介,例如 XML、数据库或任何其他媒介。

还可以创建自定义根设计器,具有自定义绘图、工具箱中的元素等。就像 DataSet 或 XmlSchema 设计器一样。请参阅此处的文章以获取一个很好的例子。

提示 1:调试这些设计器很困难。我们必须启动一个 Web 客户端(以便在其中工作)或启动一个 Windows 客户端。尝试同时调试两者非常困难,因为进程似乎被加载了两次,所以您实际上停止了调试。

提示 2:请注意,使用ExpandableObjectConverter可能会导致子组件(可以通过ITypeDescriptorContext.Instance属性在转换器中访问)停止被定位,我们可能会在检索服务时遇到麻烦。

提示 3:如果您使用 DropDown 类型的编辑器,如果您打开新的对话框窗体,属性浏览器会立即失去焦点,编辑器将不再控制打开的窗体。

提示 4:当我们在提供标准值列表时,使用转换器的IsValid方法来确定特定值的有效性是很诱人的。然而,验证不是自动的。我们必须重写此方法并检查接收到的值。但是,此方法重写可能需要TypeDescriptorContext。由于没有必要使用IWindowsFormsEditorService,我们可以安全地在此处传递我们的自定义上下文实现。

摘要

在本文中,我们探讨了 .NET 和 VS.NET 在组件化开发方面提供的最先进的功能。我们提供了与 IDE 的深度集成,甚至将模型扩展到了运行时。

我们讨论了 MVC 设计模式,并创建了一个具体的实现,该实现可以使应用程序开发速度大大加快。不仅如此,我们还能够创建一个可以在 Windows 和 Web 窗体之间共享相同代码库的实现。

使用代码

  1. 下载并解压。
  2. 在 IIS 中创建一个指向WebComponents文件夹的虚拟文件夹,名称相同。
  3. 打开解决方案,然后运行 Windows 和 Web 应用程序。
  4. 该代码假定您已在本地安装了 SQL,并带有示例 Pubs 数据库。文件WebComponents\Web.configWinComponents\App.config包含指向它的连接字符串。您必须确保这些文件中的用户和密码对于您安装的数据库是有效的。

历史

DE>GetName 方法。我们在InitControls方法中这样做,在其中我们将可用控件加载到相应的下拉列表中。

void InitControls(object state)
{
  ...
    cbControl.Items.Add(
      new ControlEntry(control, _reference.GetName(control)));

ControlEntry是一个帮助结构,它只是提供了一个自定义的ToString重写,该重写会显示在下拉列表中。这样,我们就可以集中填充,并使其更容易以后找到一个项目。

struct ControlEntry
{
  public object Control;
  public string Name;

  public ControlEntry(object control, string name)
  {
    this.Control = control;
    this.Name = name;
  }
  public override string ToString()
  {
    string id = this.Name.PadRight(20, ' ');
    return id + " [" + Control.GetType().Name + "]";
  }
}

另一个重要问题与多线程有关。如果窗体初始化成本高昂,您可能会倾向于启动一个新线程来执行它。我们可以使用以下代码在新线程中调用InitControls方法:

ThreadPool.QueueUserWorkItem(new WaitCallback(InitControls));

但是,如果您的初始化代码必须通过主机请求服务,这将不起作用。会抛出异常。这是因为服务预计是从主应用程序(VS.NET)线程中获取的。如果您不需要检索服务,或者例如,您是从Load事件处理程序内部检索的,那么就没有问题。

根据具体的应用程序和服务,我们的组件可能使用我们用于实例化具体对象的限定类型名称。例如,您的应用程序可能允许用户通过下拉列表定义将处理某个请求或应用程序功能的类型。您甚至可以从数据库获取列表。加载类型并反射它以显示其成员(例如,在窗体中)的常用方法是通过Type.GetType

Type t = Type.GetType("Mvc.Components.Controller.ViewInfo, Mvc.Components");

这在设计时仅对 GAC 中的程序集有效。这是因为 IDE 不是从项目存储所在的位置运行的。因此,运行 IDE 的进程无法找到程序集,即使它们被项目引用。还有另一项服务为我们提供了类型加载功能,即ITypeResolutionService。此服务提供了一个GetType方法,该方法通过考虑当前项目中引用的程序集来正确查找程序集。

在这个编辑器中,我们可以利用IControllerService来填充下拉列表中的值。

private void cbControlProperty_DropDown(object sender, System.EventArgs e)
{
  cbControlProperty.Items.Clear();

  try
  {
    if (lstMappings.SelectedItem == null || cbControl.SelectedItem == null)
      return;
    
    IControllerService svc = (IControllerService)
      _host.GetService(typeof(IControllerService));
    object control = ((ControlEntry) cbControl.SelectedItem).Control;
    cbControlProperty.Items.Clear();
    cbControlProperty.Text = String.Empty;
    cbControlProperty.Items.AddRange(svc.GetControlProperties(control));
    ...

在这里,我们可以立即体会到拥有全局可用的服务以避免代码重复(主要是与类型转换器)的好处。

自定义和全局命令

待解决的问题是如何提供一种简单的方法来一步检查所有已配置的映射。IControllerService有一个名为VerifyMappings的方法,用于此目的,其实现如下:

public void VerifyMappings(BaseController controller)
{
  string result = VerifyOne(controller);
  if (result == String.Empty)
    System.Windows.Forms.MessageBox.Show("Verification succeeded.");
  else
    System.Windows.Forms.MessageBox.Show(result);
}

VerifyOne检查控制器中每个ViewInfo中模型和控件值的引用。

string VerifyOne(BaseController controller)
{
  //Use reference service as always.

  IReferenceService svc = 
    (IReferenceService) _host.GetService(typeof(IReferenceService));
  StringWriter w = new StringWriter();

  Hashtable models = new Hashtable(controller.Components.Count);
  ArrayList names = new ArrayList(controller.Components.Count);

  foreach (IComponent comp in controller.Components)
  {
    if (comp is BaseModel)
    {
      BaseModel model = (BaseModel) comp;
      if (names.Contains(model.ModelName))
      {
        w.WriteLine("The model name '{0}' is" + 
            " duplicated in the controller.", model.ModelName);
      }
      else
      {
        models.Add(model.ModelName, model);
      }
    }
  }

  foreach (DictionaryEntry entry in controller.ConfiguredViews)
  {
    ViewInfo info = (ViewInfo) entry.Value;
    object ctl = svc.GetReference(info.ControlID);
    if (ctl == null)
    {
      w.WriteLine("Control '{0}' associated with the view mapping " +
              "in controller '{1}' doesn't exist in the form.", 
              info.ControlID, svc.GetName(controller));
    }
    else
    {
      if (ctl.GetType().GetProperty(info.ControlProperty) == null)
        w.WriteLine("Control property '{0}' can't be found in " + 
                "control '{1}' in controller '{2}'.", 
                info.ControlProperty, info.ControlID, svc.GetName(controller));
    }
  }

  return w.ToString();
}

现在我们需要一种执行此命令的方法。我们可以通过重写ControllerDesignerVerbs属性来为我们的控制器添加项到上下文菜单。

public override DesignerVerbCollection Verbs
{
  get 
  {
    DesignerVerb[] verbs = new DesignerVerb[] { 
      new DesignerVerb("Verify this controller mappings ...", 
        new EventHandler(OnVerifyOne)) };
    return new DesignerVerbCollection(verbs); }
}

当选择菜单项时,将调用我们的处理程序。

void OnVerifyMappings(object sender, EventArgs e)
{
  IControllerService svc = (IControllerService) 
    GetService(typeof(IControllerService));
  svc.VerifyMappings(CurrentController);
}

请注意,当公共功能被移至一等 IDE 服务时,代码变得非常简单。有了动词,我们将看到修改后的上下文菜单。

另一种常见的做法是将最常用的编辑器添加到此上下文菜单中。例如,我们可以为“编辑映射”添加一个菜单项。这似乎是显而易见的,但也有细微之处。第一步是添加另一个动词,这很容易。

public override DesignerVerbCollection Verbs
{
  get 
  {
    DesignerVerb[] verbs = new DesignerVerb[] { 
      new DesignerVerb("Verify this controller mappings ...", 
        new EventHandler(OnVerifyOne)),
      new DesignerVerb("Edit View mappings ...", 
        new EventHandler(OnEditMappings)) };
    return new DesignerVerbCollection(verbs); }
}

回想一下,我们使用了类型Editor来实现ConfiguredViews属性编辑。现在我们可以直接调用ViewMappingsEditor.EditValue方法,这将是很好的。

public override object EditValue(
  ITypeDescriptorContext context, IServiceProvider provider, object value)

但看看我们必须传递给它的参数,我们从哪里获取ITypeDescriptorContext呢?好吧,我们可以实现自己的,界面毕竟并不那么复杂。

public class DesignerContext : ITypeDescriptorContext
{
  public void OnComponentChanged() {} 
  public bool OnComponentChanging() { return true; }
  
  public IContainer Container
  {
    get { return _container; }
  } IContainer _container;
  
  public object Instance
  {
    get { return _instance;  }
  } object _instance;

  public PropertyDescriptor PropertyDescriptor
  {
    get { return _property; }
  } PropertyDescriptor _property;

  public object GetService(System.Type serviceType)
  {
    return _host.GetService(serviceType);
  }

  IDesignerHost _host;

  public DesignerContext(IDesignerHost host, 
    IContainer container, object instance, string property)
  {
    _host = host;
    _container = container;
    _instance = instance;
        _property = TypeDescriptor.GetProperties(instance)[property];      
  }
}

它基本上是属性的一个占位符,并将任何GetService请求传递给设计器主机。我们现在可以尝试直接调用编辑器。

void OnEditMappings(object sender, EventArgs e)
{
  UITypeEditor editor = new ViewMappingsEditor();

  ITypeDescriptorContext ctx = new DesignerContext(
    (IDesignerHost) GetService(typeof(IDesignerHost)), 
    this.Component.Site.Container,
    this.Component, 
    "ConfiguredViews");
  
  editor.EditValue(ctx, this.Component.Site, 
    CurrentController.ConfiguredViews);

不幸的是,这行不通。失败点将在我们检索IWindowsFormsEditorService时,在EditValue内部。

srv = (IWindowsFormsEditorService)
  context.GetService(typeof(IWindowsFormsEditorService));

请注意,我们直接向主机请求服务(因为我们实现了ITypeDescriptorContext)。无论如何,srv变量将始终为null。为什么?答案是这项服务不是由 IDE 本身提供的,而是由属性网格提供的,更确切地说,是内部类System.Windows.Forms.PropertyGridInternal.PropertyGridView,它通过返回自身来响应对此服务的请求(因为它实现了服务接口)。查看此类的ShowDialog实现的 IL 代码会揭示该过程的一些复杂性,其中包括计算对话框位置、设置焦点并将调用传递给另一个服务IUIService。这就是为什么我们一开始在编辑器中没有直接实例化一个窗体并调用ShowDialog的原因!

我们不会复制所有这些代码,所以我们将直接实例化窗体,因为在设计器动词内部这样做没有限制。

void OnEditMappings(object sender, EventArgs e)
{
  try
  {
    //Inevitably, code is almost duplicated compared with the editor...

    ViewMappingsEditorForm form = new ViewMappingsEditorForm(
      (IDesignerHost) GetService(typeof(IDesignerHost)), 
      CurrentController);

    if (form.ShowDialog() == System.Windows.Forms.DialogResult.OK)
    {
      PropertyDescriptor prop = 
        TypeDescriptor.GetProperties(Component)["ConfiguredViews"];
      prop.SetValue(Component, form.ConfiguredMappings);
    }
  }
  catch (Exception ex)
  {
    System.Windows.Forms.MessageBox.Show(
      "An exception occurred during edition: " + ex.ToString());
  }
}

代码与编辑器本身使用的代码几乎相同,除了调用IWindowsFormsEditorService

如果窗体开发人员需要同时使用多个组件,逐个检查设置可能会很繁琐。我们可以使用另一项 IDE 服务来添加所谓的全局命令。它是IMenuCommandService。由于检查映射是在控制器服务中执行的任务,因此将此新行为放在其中似乎是合乎逻辑的。回想一下,我们的服务是在Initialize方法调用中由控制器设计器实例化的。在服务构造函数中,我们可以挂钩我们的全局命令。

public ControllerService(IDesignerHost host)
{
  _host = host;
    
  IMenuCommandService mcs = (IMenuCommandService) 
    host.GetService(typeof(IMenuCommandService));
  if (mcs != null)
  {
    mcs.AddCommand(new DesignerVerb("Verify all controllers mappings ...", 
      new EventHandler(OnVerifyAll)));
  }
}

我们只能添加设计器动词,它们继承自AddCommand方法接收的MenuCommand类。每当用户单击表单的组件区域中的任何位置时,都会显示此新的上下文菜单项。

请注意,控制器未被选中。我们单击了它旁边的组件表面。处理程序现在逐个迭代所有组件。

void OnVerifyAll(object sender, EventArgs e)
{
  StringBuilder sb = new StringBuilder();
  
  foreach (IComponent component in _host.Container.Components)
  {
    if (component is BaseController)
    {
      sb.Append(VerifyOne((BaseController) component));
    }
  }

  string result = sb.ToString();
  if (result == String.Empty)
    System.Windows.Forms.MessageBox.Show("Verification succeeded.");
  else
    System.Windows.Forms.MessageBox.Show(result);
}

它只是将球传给每个找到的控制器的VerifyOne方法。IDesignerHost.Container.Components属性包含当前存在的所有引用。

如果这有那么容易就好了……不幸的是,如果我们现在右键单击控制器,我们将遗憾地看到全局命令替换了第一个控制器设计器动词。

Verify this controller …项已消失!发生这种奇怪行为的原因是 IDE 首先请求组件动词,然后(非常奇怪地)将所有全局动词放在顶部。我发现的唯一解决方法是在控制器设计器中添加一种“占位符”动词。

public override DesignerVerbCollection Verbs
{
  get 
  {
    DesignerVerb[] verbs = new DesignerVerb[] { 
      //Placeholder for first global command to avoid collisions.

      new DesignerVerb(String.Empty, null), 
      new DesignerVerb("Verify this controller mappings ...", 
        new EventHandler(OnVerifyMappings)), 
      new DesignerVerb("Edit View mappings ...", 
        new EventHandler(OnEditMappings)) 
      };
    return new DesignerVerbCollection(verbs); }
}

所有设计器动词也显示在属性浏览器的底部,方便访问。但不幸的是,我们添加的新空动词也出现在那里!请注意开头的第一个冒号:(

现在,我们只能忍受这种情况,或者放弃全局命令。

现在我们已经具备了所有的 IDE 集成管道,我们开始实际反映模型数据到映射的 UI 小部件。正如我们在分析框架架构时上面所说,控制器将设置/获取 UI 中的值这一责任委托给知道如何处理 Web 和 Windows 窗体的适配器类。

处理不同的视图技术

我们知道我们将支持 Windows 和 Web 窗体。我们将通过在通用的基础抽象类中定义我们需要的来自这两种技术的共同成员来实现这一点,并让每个“适配器”具体实现来执行特定技术所需的必要操作。

Web 和 Windows 窗体在访问和设置子控件值的方式上存在根本差异。我们已经提到了命名差异。这些细节将被隔离到一个我们将提供的新服务中,即IAdapterService。其接口包含我们需要的基本方法。

public interface IAdapterService
{
  object FindControl(string controlId);
  string GetControlID(object control);
  object[] GetControls();
  ComponentCollection GetComponents();
  void RefreshView(BaseController controller);
  void RefreshModels(BaseController controller);
}

两个类实现了这个接口:WebFormsAdapterServiceWindowsFormsAdapterService。两者都非常相似,所以我们将看前者。两个适配器都在构造函数中接收用于解析其他方法的参数。

internal class WebFormsAdapterService : IAdapterService
{
  Page _container;
  IContainer _components;

  internal WebFormsAdapterService(object controlsContainer, 
    IContainer componentsContainer)
  {
    _container = (Page) controlsContainer;
    _components = componentsContainer;
  }

Windows 窗体版本类似,但将controlsContainer参数转换为Form。前四个方法非常简单。您可以查看下载的代码以了解它们的实现。有趣的部分是RefreshViewRefreshModels方法。前者会迭代接收到的控制器中所有已配置的映射,并将相应的控件属性设置为从模型中找到的值。后者则相反。

public void RefreshView(BaseController controller)
{
  //Build a keyed list of models in the controller. 

  Hashtable models = new Hashtable(controller.Components.Count);
  foreach (BaseModel model in controller.Components)
    models.Add(model.ModelName, model);

  //We make extensive use of reflection here. This can be improved.

  foreach (DictionaryEntry entry in controller.ConfiguredViews)
  {
    ViewInfo info = (ViewInfo) entry.Value;
    //Retrieve model object and property.

    object model = models[info.Model];
    PropertyInfo modelprop = model.GetType().GetProperty(info.ModelProperty);
    //Locate control and property.

    Control ctl = (Control) FindControl((string)entry.Key);
    if (ctl == null)
    {
      throw new ArgumentException("The control '" + info.ControlID + 
          "' wasn't found in the current container.");
    }
    else
    {
      PropertyInfo ctlprop = ctl.GetType().GetProperty(info.ControlProperty);
      if (ctlprop == null)
      {
        throw new ArgumentException("The property '" + info.ControlProperty 
          + "' wasn't found in the control '" + info.ControlID + "'.");
      }
      else
      {
        object newvalue = modelprop.GetValue(model, new object[0]);
        ctlprop.SetValue(ctl, newvalue, new object[0]);
      }
    }
  }    
}

我们只是使用反射来加载类型和属性并设置值。反向过程(RefreshModels)是相同的,但不是在控件属性上调用SetValue,而是在模型属性上调用。

我们已经知道如何将新服务挂钩到架构中,所以让我们简要看一下ControllerDesigner.SetupServices方法中执行此任务的代码。

void SetupServices()
{
  //Attach the adapter services at design-time. 

  object service = GetService(typeof(IAdapterService));
  IDesignerHost host = (IDesignerHost) GetService(typeof(IDesignerHost));
  if (host.RootComponent as System.Windows.Forms.Form != null)
  {
    if (service == null)
    {
      host.AddService(
        typeof(IAdapterService), 
        new WindowsFormsAdapterService(host.RootComponent, 
          host.RootComponent.Site.Container),
        false);
    }
  }
  else if (host.RootComponent as System.Web.UI.Page != null)
  {
    if (service == null)
    {
      host.AddService(typeof(IAdapterService), 
        new WebFormsAdapterService(host.RootComponent, 
          host.RootComponent.Site.Container), false);
    }
  }

  //Setup the controller service.

  service = GetService(typeof(IControllerService));
  if (service == null)
  {
    host.AddService(typeof(IControllerService), 
      new ControllerService(host), false);
  }
}

您可能已经注意到,当我们向主机添加任何服务时,我们将false作为最后一个参数传递。该参数指定我们是否要将服务提升到 IDE 架构的更高层。除非您确定该服务是唯一的且永远不需要动态更改,否则不建议这样做。在我们的例子中,我们根据根组件类型切换服务对象,因此我们需要它保留在IDesignerHost中并且易于替换。通过向第三个参数传递false,我们表示该服务仅在当前根设计器的生命周期内有效。

现在我们可以利用这项服务来访问控制器扩展器属性中的映射(我们删除了属性以提高可读性)。

public ViewInfo GetWinViewMapping(object target)
{
  return GetViewMapping(target);
}

public void SetWinViewMapping(object target, ViewInfo value)
{
  SetViewMapping(target, value);
}

public ViewInfo GetWebViewMapping(object target)
{
  return GetViewMapping(target);
}

public void SetWebViewMapping(object target, ViewInfo value)
{
  SetViewMapping(target, value);
}

现在两个属性(Win 和 Web)可以共享一个通用实现,因为适配器服务解决了不一致之处。

public ViewInfo GetViewMapping(object target)
{
  IAdapterService svc = (IAdapterService) GetService(typeof(IAdapterService));
  string id = svc.GetControlID(target);
  ViewInfo info = _views[id] as ViewInfo;
  if (info == null)
  {
    info = new ViewInfo(this, id);
    _views[id] = info;
  }

  info.Controller = this;

  //Verify that we are trapping the changes. 

  if (!info.IsHooked)
  {
    PropertyDescriptorCollection props = TypeDescriptor.GetProperties(info);
      props["ControlProperty"].AddValueChanged(info, 
        new EventHandler(RaiseViewInfoChanged));
      props["Model"].AddValueChanged(info, 
        new EventHandler(RaiseViewInfoChanged));
      props["ModelProperty"].AddValueChanged(info, 
        new EventHandler(RaiseViewInfoChanged));
    info.IsHooked = true;
  }

  return info;
}

public void SetViewMapping(object target, ViewInfo value)
{
  IAdapterService svc = (IAdapterService) GetService(typeof(IAdapterService));
  _views[svc.GetControlID(target)] = value;
}

最后,基础控制器公开内部方法来同步视图和模型。

internal event EventHandler ModelChanged;

protected virtual void InitContext(object sender, EventArgs e)
{
}

internal void Init(object sender, EventArgs e)
{
  InitContext(sender, e);
}

internal void RefreshModels(object sender, EventArgs e)
{
  IAdapterService svc = (IAdapterService) 
    this.Site.GetService(typeof(IAdapterService));
  svc.RefreshModels(this);
}

internal void RefreshView(object sender, EventArgs e)
{
  IAdapterService svc = (IAdapterService)
    this.Site.GetService(typeof(IAdapterService));
  svc.RefreshView(this);
}

protected void RaiseModelChanged(BaseModel model)
{
  if (ModelChanged != null)
    ModelChanged(model, EventArgs.Empty);
}

请注意,这些方法现在非常简单,因为适配器负责与视图的交互。这与原始 MVC 模式的模型略有不同,在原始 MVC 模式中,每个视图技术都有自己的控制器。

但在我们继续进行视图同步之前,我们需要知道视图如何请求控制器加载某个模型。

模型行为,MVC 方式

由于视图不允许直接在模型上执行操作,因此保留了隔离性。因此,控制器公开了导致实际模型方法执行的方法。模型包含行为以及数据,并使用这些数据来保存方法执行的结果或作为某些操作的输入。

一个简单的PublisherModel组件可能除了映射到示例 Pubs 数据库中publishers表字段的属性外,还公开了对实体的三个基本操作:LoadSaveDelete

/// <summary>

/// Loads the current model with data from the database matching the 

/// current ID.

/// </summary>

public void Load()
{
  SqlConnection cn = new SqlConnection(ConnectionString);
  SqlCommand cmd = new SqlCommand(
    "SELECT * FROM publishers WHERE pub_id = '" + this.ID + "'", cn);
  try
  {
    cn.Open();
    SqlDataReader reader =
      cmd.ExecuteReader(CommandBehavior.CloseConnection);
    if (reader.Read())
    {
      this.ID = reader["pub_id"] as string;
      this.Name = reader["pub_name"] as string;
      this.City = reader["city"] as string;
      this.State = reader["state"] as string;
      this.Country = reader["country"] as string;
    }
    reader.Close();
  }
  finally
  {
    if (cn.State != ConnectionState.Closed)
      cn.Close();
  }
}

请注意,该方法使用发布者 ID 从其自身的属性加载。同样,无论它加载什么都会放回模型属性中。其他两个方法的实现也类似。

正如我们所说,为了访问这些模型行为,必须通过控制器公开。

public class PublisherController : BaseController
{
  public void DeletePublisher()
  {
    model.Delete();
    RaiseModelChanged(model);
  }

  public void LoadPublisher()
  {
    model.Load();
    RaiseModelChanged(model);
  }

  public void SavePublisher()
  {
    model.Save();
    RaiseModelChanged(model);
  }

基本上,它们将方法调用传播到包含的模型,并最终引发一个视图可以使用来更新其控件值的事件。

在 Windows 窗体按钮单击和 Web 窗体按钮单击中,我们都可以放置以下代码来在模型中执行操作:

private void btnLoad_Click(object sender, System.EventArgs e)
{
  controller.LoadPublisher();
}

private void btnSave_Click(object sender, System.EventArgs e)
{
  controller.SavePublisher();
}

private void btnDelete_Click(object sender, System.EventArgs e)
{
  controller.DeletePublisher();
}

请注意,没有“平台”特定的代码。没有一行。但是控制器是如何以及何时更新 UI 的?Web 和 Windows 环境中的这种时序存在根本差异。在前者中,UI 通常在Load事件上加载,因为回发会导致新的Load事件被触发。然而,Windows 窗体需要在Load之外的其他时间刷新视图,因为进一步的操作不会导致新的Load事件。为了提供这种包含窗体、其事件和控制器操作(即RefreshViewRefreshModels)之间的连接性,我们使用连接器

连接视图

连接器的责任是在适当的时间调用控制器方法来同步模型和视图。基本上,UI 容器(Web 或 Windows 窗体)应该实例化相应的连接器并调用Connect,传递相关的参数来执行布线。我们像往常一样提供一个基类,两个连接器都将从它继承。

public abstract class BaseConnector
{
  public abstract void Connect(BaseController controller, 
    object controlsContainer, IContainer componentsContainer);
}

为了使过程自动化,我们将从控制器代码生成内部发出具体的连接代码,在检测到托管技术后。这是ControllerCodeDomSerializer方法中的相关代码。

public override object 
       Serialize(IDesignerSerializationManager manager, object value)
{
  ... 
  
  //Emit code for the appropriate adapter.

  //new WebFormsAdapter.Connect(controller, this, this.components);

  if (host.RootComponent as System.Windows.Forms.Form != null)
  {
    CodeObjectCreateExpression adapter = new CodeObjectCreateExpression(
      typeof(Connector.WindowsFormsConnector), new CodeExpression[0]);
    CodeExpression connect = new CodeMethodInvokeExpression(
      adapter, "Connect", 
      new CodeExpression[] {
        cnref, 
        new CodeThisReferenceExpression(), 
        new CodeFieldReferenceExpression(
          new CodeThisReferenceExpression(), 
          "components") 
      });
    statements.Add(connect);
  }
  else if (host.RootComponent as System.Web.UI.Page != null)
  {
    CodeObjectCreateExpression adapter = new CodeObjectCreateExpression(
      typeof(Connector.WebFormsConnector), new CodeExpression[0]);
    CodeExpression connect = new CodeMethodInvokeExpression(
      adapter, "Connect", 
      new CodeExpression[] {
        cnref, 
        new CodeThisReferenceExpression(),
        new CodeFieldReferenceExpression(
          new CodeThisReferenceExpression(), "components") 
      });
    statements.Add(connect);
  }

放置在 Web 窗体中的控制器会生成以下代码:

private void InitializeComponent()
{    
  ...
  // 

  // publisherController

  // 

  this.publisherController.ConnectionString = 
    ((string)(configurationAppSettings.GetValue("Pubs", typeof(string))));
  // 

  // ------------- ClariuS Custom Code -------------

  // Controller code

  // 

  this.publisherController.ConfiguredViews.Add("txtID", 
       new Mvc.Components.Controller.ViewInfo("txtID", 
       "Text", "Publisher", "ID"));
  // Connect the controller with the hosting environment.

  new Mvc.Components.Connector.WebFormsConnector().Connect(
    this.publisherController, this, this.components);
  ...
}

放置在 Windows 窗体中的同一个组件会生成:

private void InitializeComponent()
{    
  ...
  // 

  // publisherController

  // 

  this.publisherController.ConnectionString = 
    ((string)(configurationAppSettings.GetValue("Pubs", typeof(string))));
  // 

  // ------------- ClariuS Custom Code -------------

  // Controller code

  // 

  this.publisherController.ConfiguredViews.Add("txtID", 
       new Mvc.Components.Controller.ViewInfo("txtID", 
       "Text", "Publisher", "ID"));
  // Connect the controller with the hosting environment.

  new Mvc.Components.Connector. WindowsFormsConnector().Connect(
    this.publisherController, this, this.components);
  ...
}

请注意,唯一的区别是实例化的具体连接器。一个重要的注意点是,此方法永远不会在设计时调用。IDE 只加载组件上的属性,但不会调用方法。

Web 窗体连接器相当简单:

public class WebFormsConnector : BaseConnector
{
  public override void Connect(BaseController controller, 
    object controlsContainer, IContainer componentsContainer)
  {
    //Connect lifecycle events.

    Page page = (Page) controlsContainer;
    page.Init += new EventHandler(controller.Init);
    page.Load += new EventHandler(controller.RefreshModels);
    page.PreRender += new EventHandler(controller.RefreshView);
  }
}

请注意,这将控制器挂钩到页面事件,导致它们在适当的时间被调用。

让我们总结一下在Load事件中将发生的交互:

这里的关键点是Site.GetService()方法调用,它必须为控制器提供一个适当的适配器实例。我们已经测试过在设计时附加服务时服务就在那里。但是设计器只在设计时调用,那么运行时会发生什么?

回到开头,当我们介绍这个 .NET 组件面向对象架构时,我们说过 UI 控件和非可视化组件之间存在差异。现在我们能够解释这种差异是什么。在设计时,两种组件都位于DesignSite中并包含在DesignHost中,正如当时所示。但在运行时,组件会将this.components 字段作为容器传递,它们在该容器上被定位,该容器只是一个类型为Container的类。相应的站点类型为Container.Site。问题是GetService方法在Container类内部实现,它只返回它本身提供的IContainer类型的服务。所有其他服务都丢失了。更糟的是,可视化控件根本没有Site,这就是区别。

我们可以做的是执行一些运行时初始化,并提供一个自定义站点,该站点可以响应我们IAdapterService的请求。请记住,服务实例必须始终保存在某个地方,就像 VS.NET 在设计时保存它一样。我们将创建一个实现ISiteRuntimeSite类,我们可以用它来“定位”组件,并使用我们自己的基础设施。

/// <summary>

/// Provides a custom site to be used at run-time, so that components can 

/// request our custom services just as if they were at design-time.

/// </summary>

public class RuntimeSite : ISite
{
  public RuntimeSite(IContainer container, 
    IComponent component, GetServiceEventHandler getServiceCallback)
  {
    _container = container;
    _component = component;
    _callback = getServiceCallback;
  }

  public IComponent Component
  {
    get {  return _component; }
  } IComponent _component;

  public IContainer Container
  {
    get { return _container; }
  } IContainer _container;

  public bool DesignMode
  {
    get { return false; }
  }

  public string Name
  {
    get { return String.Empty; }
    set { }
  }

  /// <summary>

  /// Passes the call for a service to the handler received 

  /// in the constructor.

  /// </summary>

  public object GetService(Type serviceType)
  {
    return _callback(this, new GetServiceEventArgs(serviceType));
  } GetServiceEventHandler _callback;
}

我们的构造函数除了接收当前容器和组件外,还接收一个委托。我们说服务实例在运行时必须保存在某个地方。提供了此回调,以便当组件请求服务时,调用将被传递到此委托,该委托可以根据所用技术的需要进行实现。例如,Web 版本可以使用HttpContext来保存对象,而 Windows 窗体版本则不能。委托让两者都有机会以任何他们想要的方式回答GetService请求。委托类及其参数是:

public delegate object GetServiceEventHandler(object sender, 
                                     GetServiceEventArgs e);

public class GetServiceEventArgs
{
  public GetServiceEventArgs(Type serviceType)
  {
    _service = serviceType;
  }

  public Type ServiceType
  {
    get { return _service; }
  } Type _service;
}

让我们看一下完整的 Web 连接器构造函数,它还初始化运行时站点。

public override void Connect(BaseController controller, 
  object controlsContainer, IContainer componentsContainer)
{
  //Retrieve or hook the service as needed.

  WebFormsAdapterService service;

  if (!HttpContext.Current.Items.Contains(typeof(IAdapterService).FullName))
  {
    service = new WebFormsAdapterService(controlsContainer, 
      componentsContainer);
    HttpContext.Current.Items.Add(typeof(IAdapterService).FullName, service);
  }  
  else
  {
    service = (WebFormsAdapterService) 
      HttpContext.Current.Items[typeof(IAdapterService).FullName];
  }

  //The handler for GetService is implemented in the service itself.

  controller.Site = new RuntimeSite(componentsContainer, controller, 
    new GetServiceEventHandler(service.GetServiceHandler));

  //Set new site on each component.

  foreach (IComponent component in controller.Components)
  {
    component.Site = new RuntimeSite(controller, component, 
      new GetServiceEventHandler(service.GetServiceHandler));
  }

    //Connect lifecycle events.

    Page page = (Page) controlsContainer;
    page.Init += new EventHandler(controller.Init);
    page.Load += new EventHandler(controller.RefreshModels);
    page.PreRender += new EventHandler(controller.RefreshView);
  }
}

WebFormsAdapterService.GetServiceHandler方法负责响应来自组件的请求。

internal object GetServiceHandler(object sender, GetServiceEventArgs e)
{
  if (e.ServiceType == typeof(IAdapterService))
  {
    return this;
  }
  else
  {
    return null;
  }
}

很简单,连接器的 Web 版本首先将服务存储在HttpContext中,然后让服务本身响应请求服务的组件。现在,控制器在其RefreshModels方法中将被调用在Page.Load时间,将成功检索服务。

public class BaseController : Component, IContainer, IExtenderProvider
{
  internal void RefreshModels(object sender, EventArgs e)
    {
      IAdapterService svc = (IAdapterService) 
        this.Site.GetService(typeof(IAdapterService));
      svc.RefreshModels(this);
    }

该框架的 Web 版本现在已完成。当页面加载时,模型会根据用户在表单上发布的(或最初加载的)任何值进行更新。在渲染到客户端之前,通常在所有按钮单击、文本框更改等事件处理程序都被调用之后,控制器会使用模型中最终的值刷新视图。显示发布者数据并具有正确映射的 Web 窗体,可以通过我们每个按钮处理程序中看到的单行代码提供加载/保存/删除功能。

private void btnLoad_Click(object sender, System.EventArgs e)
{
  controller.LoadPublisher();
}

现在,假设我们在相应的字段中输入一个ID并单击Load,这就是页回到我们之前的操作序列:

  • Load时,适配器服务被挂钩,组件被定位。
  • 模型被刷新,因此txtID中的值被放置在PublisherModel.ID属性中。
  • 事件处理程序调用控制器方法LoadPublisher()
  • 模型转到数据库并使用返回的行加载自身。
  • 在渲染之前,控件值会根据模型中的新值进行更新(完整的发布者数据)。
  • 页面以值呈现并发送到浏览器。

页面设计者只需使用我们的 IDE 集成功能配置映射,并在适当的时候调用相应的控制器方法即可!

更重要的是,如果我们以后决定(或同时)移动到 Windows 窗体客户端,相同的控制器和映射将完成工作。甚至 UI 事件的事件处理程序也会看起来相同!

由于 Windows 窗体支持有状态应用程序,我们可以简单地将相关变量保留在连接器本身中。但是,在 Web 中,初始化时,我们知道所有控件都已创建。Windows 窗体的情况则不是这样,控件只是类级别的变量,必须在InitializeComponent方法中初始化,就像我们的控制器一样。因此,不能保证我们的连接器会在控件初始化后被调用。

为什么我们需要在初始化时访问控件?因为与 Web 中的模型刷新只有一个点(Load事件)不同,在 Windows 窗体中,我们必须在控件修改后立即刷新模型,并且我们必须挂钩到该事件并触发模型刷新。

所以我们必须求助于一些窗体事件来执行实际的挂钩。

public class WindowsFormsConnector : BaseConnector
{
  IAdapterService _service;
  BaseController _controller;

  public override void Connect(BaseController controller, object 
    controlsContainer, IContainer componentsContainer)
  {
    _service = new WindowsFormsAdapterService(controlsContainer, 
        componentsContainer);
    _controller = controller;

    //Connect components to a run-time site.

    controller.Site = new RuntimeSite(componentsContainer, controller, 
        new GetServiceEventHandler(GetServiceHandler));
    foreach (IComponent component in controller.Components)
    {
      component.Site = new RuntimeSite(controller, component, 
        new GetServiceEventHandler(GetServiceHandler));
    }

    //Connect lifecycle events.

    Form form = (Form) controlsContainer;
    //First connect control events.

    form.Activated += new EventHandler(OnActivated);
    //Initialize controller context next.

    form.Activated += new EventHandler(controller.Init);
    //Refresh control values at loading time.

    form.Load += new EventHandler(controller.RefreshView);
    //Persist in model at deactivation time.

    form.Deactivate += new EventHandler(controller.RefreshModels);
  }

如您所见,每个连接器(每个控制器一个)将拥有服务和相应的连接器。OnActivated处理程序负责将RefreshModels方法挂钩到每个控件的Leave事件。

  void OnActivated(object sender, EventArgs e)
  {
    //Just a simple implementation based on focus changes.

    foreach (DictionaryEntry entry in _controller.ConfiguredViews)
    {
      Control ctl = (Control) 
        _service.FindControl(((ViewInfo)entry.Value).ControlID);
      ctl.Leave += new EventHandler(_controller.RefreshModels);
    }
    //Refresh the form view.

    _controller.RefreshView(sender, e);
    //Attach the model changed event to the controller refresh method, 

    //so that the view is automatically updated.

    _controller.ModelChanged += new EventHandler(_controller.RefreshView);
  }

我们在最后做的另一个技巧是将控制器触发的ModelChanged事件挂钩到同一个控制器的RefreshView方法。这样,我们就可以实现自动 UI 更新,而无需编写任何代码。

最后,处理GetService请求的委托(我们为此传递给每个组件的新RuntimeSite)只需返回它保留的服务引用。

  object GetServiceHandler(object sender, GetServiceEventArgs e)
  {
    if (e.ServiceType == typeof(IAdapterService))
    {
      return _service;
    }
    else
    {
      return null;
    }
  }
}

现在,这两个(除了连接器实例)完全相同的代码可以存在于 Windows 和 Web 窗体应用程序中,并且行为完全相同,甚至在配置级别也是如此。

超越您自己的组件

通过代码在设计时持久化肯定比运行时检测和发出具有优势:它是编译的,并且速度总是更快。例如,我们可能希望在渲染的 Web 窗体控件上发出一些属性,我们可以在客户端 JavaScript 中使用这些属性来了解映射的详细信息。同样,有一种自然的方法似乎很明显:挂钩到PreRender并再次迭代配置的映射,将相关属性添加到控件。

如果映射和控件在设计时已知,为什么我们必须在每次页面交互时浪费宝贵的运行时处理时间进行这种迭代?答案在于一种更好的利用方式。它被称为IDesignerSerializationProvider

实现此接口的对象可以注册到IDesignerSerializationManager(我们在控制器序列化器中接收到的)。

internal class ControllerCodeDomSerializer : BaseCodeDomSerializer
{
  public override object 
         Serialize(IDesignerSerializationManager manager, object value)
  {
    ...

    //Add a sample serializer for web controls.

    if (!(manager.GetSerializer(typeof(System.Web.UI.Control), 
        typeof(CodeDomSerializer)) is WebControlSerializer))
    {
      manager.AddSerializationProvider(
        new WebControlSerializationProvider());
    }

我们首先检查提供程序是否已被添加。在序列化时,管理器将调用每个配置的提供程序,并给它一个机会为某个类型提供自定义序列化器。这是在GetSerializer方法中完成的。

internal class WebControlSerializationProvider : 
    IDesignerSerializationProvider
{
  /// <summary>

  /// Returns our custom serializer only if the type received implements 

  /// <see cref="IAttributeAccessor"/>, the only common ground 

  /// (for attribute settings) for HtmlControls and WebControls.

  /// </summary>

  public object GetSerializer(IDesignerSerializationManager manager, 
    object currentSerializer, Type objectType, Type serializerType)
  {
    if (typeof(IAttributeAccessor).IsAssignableFrom(objectType) && 
        serializerType == typeof(CodeDomSerializer))
      return new WebControlSerializer();
    else
      return null;
  }
}

如果我们找到一个我们可以附加属性的控件(实现了IAttributeAccessor),我们就返回我们的新序列化器。从现在开始,每当 IDE 需要序列化 Web 控件(无论是HtmlControl还是WebControl)时,我们的序列化器都将被调用,并接收正在被序列化的控件。

internal class WebControlSerializer : BaseCodeDomSerializer
{
  public override object Serialize(
    IDesignerSerializationManager manager, object value)
  {
    ...
  }

为了避免干扰正常的序列化过程,我们将首先检索接收到的控件的常规序列化器,并从中获取代码语句。我们在基类序列化器中定义了一个帮助方法来获取适当的类型。

protected CodeDomSerializer GetConfiguredSerializer(
    IDesignerSerializationManager manager, object value)
{
  //Get the attribute anywhere in the inheritance chain.

  object[] attrs = value.GetType().GetCustomAttributes(
    typeof(DesignerSerializerAttribute), true);
  if (attrs.Length == 0) return null;
  
  DesignerSerializerAttribute serializer = 
    (DesignerSerializerAttribute) attrs[0];

  ITypeResolutionService svc = (ITypeResolutionService)
    manager.GetService(typeof(ITypeResolutionService));
  Type t = svc.GetType(serializer.SerializerTypeName);

  return (CodeDomSerializer) Activator.CreateInstance(t);
}

这里我们看到ITypeResolutionService在起作用。在我们检索到相关的属性后,我们所拥有的只是限定的类型名称,我们必须通过该服务加载它,以确保它所在的程序集被找到并成功加载。之后,我们只需使用Activator.CreateInstance方法创建序列化器并返回它。

让我们回到WebControlSerializationProvider.Serialize方法。

public override object Serialize(
  IDesignerSerializationManager manager, object value)
{
  CodeDomSerializer serial = GetConfiguredSerializer(manager, value);
  if (serial == null) 
    return null;

  CodeStatementCollection statements = (CodeStatementCollection)
    serial.Serialize(manager, value);

当属性被扩展时,如果使用TypeDescriptor,它将作为对象本身的属性出现。

  PropertyDescriptor prop = 
    TypeDescriptor.GetProperties(value)["WebViewMapping"];
  ViewInfo info = (ViewInfo) prop.GetValue(value);

现在我们需要生成以下代码来添加一个属性来存储控制器名称,例如。

((IAttributeAccessor)control).SetAttribute("MVC_Controller", "controller");

我们还将对其他四个ViewInfo属性执行此操作。

  //Attach the view mappings to the control attributes.

  if (info.ControlProperty != String.Empty && 
    info.Model != String.Empty && info.ModelProperty != String.Empty)
  {
    //Temp variables

    CodeExpression ctlref = SerializeToReferenceExpression(
      manager, value);
    CodeCastExpression cast = 
      new CodeCastExpression(typeof(IAttributeAccessor), ctlref);

    //Emits:

    //((IAttributeAccessor)control).SetAttribute("MVC_" + [key], [value]);

    statements.Add(new CodeMethodInvokeExpression(
        cast, "SetAttribute", new CodeExpression[] {
          new CodePrimitiveExpression("MVC_Controller"), 
          new CodePrimitiveExpression(manager.GetName(info.Controller)) 
                          }));
    statements.Add(new CodeMethodInvokeExpression(
        cast, "SetAttribute", new CodeExpression[] {
          new CodePrimitiveExpression("MVC_Model"), 
          new CodePrimitiveExpression(info.Model) 
                          }));
    //The other two are the same.

    ...
    return statements;
  }
}

我们定义了我们反复使用的表达式,例如转换为IAttributeAccessor和控件引用。

现在我们可以导致页面上的代码生成过程,例如,通过更改视图映射,然后更改 Web 控件属性。新序列化的代码如下所示:

private void InitializeComponent()
{    
  this.components = new System.ComponentModel.Container();
  System.Configuration.AppSettingsReader configurationAppSettings = 
      new System.Configuration.AppSettingsReader();
  this.publisherController = new PubsMVC.PublisherController(this.components);
  ((System.Web.UI.IAttributeAccessor)(this.txtID)).SetAttribute("MVC_Controller", 
                                                          "publisherController");
  ((System.Web.UI.IAttributeAccessor)(this.txtID)).SetAttribute("MVC_Model", 
                                                               "Publisher");
  ...

现在呈现的页面带有属性,而无需在运行时动态添加它们。

<table id="Table1" cellspacing="5" 
                   cellpadding="1" width="217" border="0"   style="...">
  <tr>
    <td>ID:</td>
    <td>
      <input name="txtID" type="text" 
        value="0736" id="txtID" 
        MVC_Controller="publisherController" 
        MVC_Model="Publisher" 
        MVC_ModelProperty="ID" 
        MVC_ControlProperty="Text" />
    </td>
  </tr>
  <tr>
    <td>Name:</td>
    <td>
      <input name="txtName" type="text" 
        value="New Moon Book" id="txtName" 
        MVC_Controller="publisherController" 
        MVC_Model="Publisher" 
        MVC_ModelProperty="Name" 
        MVC_ControlProperty="Text" />
    </td>
  </tr>
...

作为旁注,我们必须指出ViewInfo对象包含一个指向创建它的控制器的Controller属性。这就是我们将它的名称发出到页面的方式。这是我们特有的实现细节,但如果该属性不存在怎么办?我们如何检索实际的属性提供程序,备份控件上一个“虚假”的属性?

答案并不容易,不幸的是。当我们向TypeDescriptor请求属性时,我们期望得到一个PropertyDescriptor。但是,当属性被扩展时,在一个派生自它的类中,会返回ExtendedPropertyDescriptor。这个对象有一个Provider属性,它引用提供此属性的对象。太棒了!还有什么可以问的?嗯,我们可以要求类不要是私有的

所以,既然我们运气不好,我们就必须找到一个解决方法,像往常一样。技巧来自反射。请注意,我们将要使用的技术风险很高。如果 MS 决定更改类名或属性名,甚至删除它,您现在可以工作的代码将不再起作用。而且他们不会做任何错误的事情,因为界面是私有的。现在您已意识到,让我们开始吧!

为了实现这一点,我们必须首先加载类型。通过Assembly.GetType方法,我们无法访问private类型。我们必须使用GetTypes并迭代所有类型并找到匹配的类型。我们创建了一个实用类DesignUtils,其中有一个LoadPrivateType可以做到这一点。它的用法很简单:

Type t = DesignUtils.LoadPrivateType(
  "System.ComponentModel.ExtendedPropertyDescriptor,System");

请注意,我们避免了传递完全限定的程序集名称,方法代码如下:

internal static Type LoadPrivateType(string className)
{
  string[] names = className.Split(',');
  if (names.Length < 2) 
    throw new ReflectionTypeLoadException(null, null,
      "Invalid class name: " + className);

  //Try to load className from the referenced assemblies. 

  //If it is not referenced, throw.

  AssemblyName[] asmNames = 
    Assembly.GetExecutingAssembly().GetReferencedAssemblies();
  Assembly asm = null;

  foreach (AssemblyName name in asmNames)
  {
    if (String.CompareOrdinal(name.Name, names[1]) == 0)
    {
      asm = Assembly.Load(name);
      break;
    }
  }
  if (asm == null) 
    throw new ReflectionTypeLoadException(null, null, 
      names[1] + " assembly couldn't be loaded.");

  //Locate the type. Iteration is required because it's not public.

  Type type = null;
  foreach (Type t in asm.GetTypes())
  {
    if (String.CompareOrdinal(t.FullName,names[0]) == 0)
    {
      type = t;
      break;
    }
  }

  if (type == null) 
    throw new ReflectionTypeLoadException(null, null, 
      className + " wasn't found.");

  return type;
}

现在我们有了私有的反射类型,我们可以像往常一样通过以下方式获取属性:

PropertyInfo provider = t.GetProperty("Provider");

最后,我们可以通过使用属性信息GetValue方法来获取实际的提供程序实例。

object instance = provider.GetValue(prop, new object[0]);

我们将此行为封装在DesignUtils的静态字段中,以便此过程只发生一次。从 Web 控件自定义序列化器内部,我们可以通过以下代码获取对控制器的引用:

PropertyDescriptor prop = TypeDescriptor.GetProperties(value)["WebViewMapping"];
ViewInfo info = (ViewInfo) prop.GetValue(value);

BaseController controller = (BaseController)
  DesignUtils.ProviderProperty.GetValue(prop, new object[0]);

最终信息

这个 MVC 风格框架的原型实现远非完整或达到生产质量。但是,我们已经探索了 IDE 设计时架构的几乎所有方面,甚至将其概念扩展到运行时,并利用了整个 .NET 框架的内在组件面向对象特性。因此,即使 Web 窗体实现特别薄弱(因为我们在每次更改时都会导致完整的视图和模型刷新),它确实证明了该概念。更精细化的控制,以及更丰富的控制器事件模型,将极大地有益于架构。

与 VS.NET 的深度集成可以极大地提高开发人员的生产力,通过使用自定义代码生成技术,我们可以实现相当复杂的组件持久化,甚至强制执行某些架构决策,例如我们为模型访问所做的决策。

对于研究组件持久化其他领域的人来说,您应该知道组件也可以存储在资源文件中。更重要的是,我们可以附加我们自己的序列化提供程序,就像我们为 Web 窗体控件所做的那样,以序列化到其他媒介,例如 XML、数据库或任何其他媒介。

还可以创建自定义根设计器,具有自定义绘图、工具箱中的元素等。就像 DataSet 或 XmlSchema 设计器一样。请参阅此处的文章以获取一个很好的例子。

提示 1:调试这些设计器很困难。我们必须启动一个 Web 客户端(以便在其中工作)或启动一个 Windows 客户端。尝试同时调试两者非常困难,因为进程似乎被加载了两次,所以您实际上停止了调试。

提示 2:请注意,使用ExpandableObjectConverter可能会导致子组件(可以通过ITypeDescriptorContext.Instance属性在转换器中访问)停止被定位,我们可能会在检索服务时遇到麻烦。

提示 3:如果您使用 DropDown 类型的编辑器,如果您打开新的对话框窗体,属性浏览器会立即失去焦点,编辑器将不再控制打开的窗体。

提示 4:当我们在提供标准值列表时,使用转换器的IsValid方法来确定特定值的有效性是很诱人的。然而,验证不是自动的。我们必须重写此方法并检查接收到的值。但是,此方法重写可能需要TypeDescriptorContext。由于没有必要使用IWindowsFormsEditorService,我们可以安全地在此处传递我们的自定义上下文实现。

摘要

在本文中,我们探讨了 .NET 和 VS.NET 在组件化开发方面提供的最先进的功能。我们提供了与 IDE 的深度集成,甚至将模型扩展到了运行时。

我们讨论了 MVC 设计模式,并创建了一个具体的实现,该实现可以使应用程序开发速度大大加快。不仅如此,我们还能够创建一个可以在 Windows 和 Web 窗体之间共享相同代码库的实现。

使用代码

  1. 下载并解压。
  2. 在 IIS 中创建一个指向WebComponents文件夹的虚拟文件夹,名称相同。
  3. 打开解决方案,然后运行 Windows 和 Web 应用程序。
  4. 该代码假定您已在本地安装了 SQL,并带有示例 Pubs 数据库。文件WebComponents\Web.configWinComponents\App.config包含指向它的连接字符串。您必须确保这些文件中的用户和密码对于您安装的数据库是有效的。

历史

© . All rights reserved.