深入观察 .NET组件模型及继承
【IT168 专稿】
我们了解了属性扩展器的基本情况以及自定义代码序列化,下面继续学习MVC。
完成MVC的构建模型
在我们的模式中,控制器包括模式。视图通过控制器访问模式。这些模式也是构件,这些构件则是控制器设计器内部的拖&曳。我们知道在表层中,默认设计器允许拖曳构件的任何类型。这就们所得到的。我们仅仅需要在控制器内使用模式,除了避免在控制器内拖曳模式外,也需要避免在内含包容内拖曳模式。
完成控件此层的一个方法就是使用两个相联系的集成开发环境功能。一个功能是将自定义RootDesigner附加到基础控制器类型,而所有自定义控制器都从此基本控制器类别继承而来。
[Designer(typeof(ControllerRootDesigner), typeof( IRootDesigner))]另外一个方法就是在新的根设计器和BaseModel类型中都附加有ToolboxItemFilter,所有自定义模式都将从此BaseModel类型中继承。
public class BaseController : Component, IExtenderProvider
[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值告诉集成开发环境仅仅在工具栏中创建项目。这里有一篇有趣的文章,是关于属性值上不同的结合,以及工具栏带来的影响。 BaseModel类型提供一个虚拟属性,此虚拟属性允许继承者为模式指定一个名称,视图映射将会引用这个名称。最后的一步就是,通过为它们补充下面的属性,我们能阻止这两个基本类型出现在工具栏中:
[ToolboxItem(false)]注意:此属性是可以继承的,如果派生类型想在工具栏中利用ToolboxItem(true)属性的话,派生出来的类型必须有一个ToolboxItem(true)属性。Usual OO程序设计建议创建基本类型,而开发者abstract没打算使用基本类型。因为当前集成开发环境需要能实例化构件来工作,我们不能使用我们的构件来完成这一过程。
现在创建一个PublisherController和一个PublisherModel,PublisherController 和一个PublisherModel都是从这两个类型继承来的,编译一切,并把它们补充到工具栏。使用设计模式中的PublisherController,我们可以看到过滤器的影响,此影响导致PublisherController构件不能工作。在窗体 (Windows 或者是Web)中发生的情况却完全相反,在窗体 (Windows 或者是Web)中只能激活一个 。


在完成模式之前,必须考虑一些详情,此详情就是:当拖曳设计器中的构件时,尤其是当构件继承其它构件时,VS.NET是创建构件hierarchies的的方法。
.NET组件模型: 深入探索和继承问题
当我们为项目补充一个新的构件,我们自动地从基本模板中生成代码。如果将另一个自定义构件拖到代码中,组件代码将表现为下面的情况:
public class Component1 : System.ComponentModel.Component这里有一个可以利用的private components,意识到这一点非常重要。private components是类型容器,并且被传递到Component2 构造函数。我们也知道此构造函数将做Component1 构造函数所做的事情:把它自己补充到容器:
{
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);
}
}
public Component2(System.ComponentModel.IContainer container)这个构件也包含它自己的components领域,此构件将把components领域传递到另一个构件,我们拖曳它的设计图面,而此设计图面则依次补充它。这个容器类型实现将为引入的构件Site属性(用Add方法)指定一个新创建的ISite,此ISite既指向容器也指向构件。构件可以使用类型Container.Site的Site,以此来知道到底是什么包含了此Site。记住在设计时期时,此Site将被指向DesignSite,在文章前面的介绍中已经解释了。
{
container.Add(this);
InitializeComponent();
}
此机制为内含包容服务得很好,但是当从其它构件继承构件时,结果会得到一个内含包容的断链,因为基本类型有它自己的components领域。因此我们将有一个与此相似的层次结构。

此层次结构是通过这种方式建立的,因为PublisherControler中被拉曳的模式传递“错误”容器。结果就是:设置在BaseControler类型中的任何代码将不能访问被包含的构件。解决方案就是把模式构造函数传递到基本类型components领域。然而,这样要使此领域能被继承者访问,继承者在某种程度上违背了封装的OO法则。一个更好的解决方案就是在基本类型内实现IContainer,将this引用传递到模式构造函数。基本类型内的接口实现将提供访问components领域的正确途径。
public class BaseController : Component, IContainer, IExtenderProvider我们能安全删除包括容器的基本类型构造函数,因为继承类型(例如:PublisherController)将它们自己补充到基本类型构造函数,这是默认行为,与继承不相冲突。
{
#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
...
public PublisherController(IContainer container)基本类型不需要InitializeComponent或者特殊构造函数代码,因为在领域宣告中,components领域初始化了它自己。
{
container.Add(this);
InitializeComponent();
}
现在,为模式构造函数定制代码,因此要使用下面的代码:
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: the = operation, 有左右两部分
• CodeObjectCreateExpression: the right part of the assignment. 指定赋值的右边部分
• CodeFieldReferenceExpression: 表示法Parameters集合中的第一个参数,此参数指向this.components领域。
我们只需要代替或者补充那个参数。
CodeAssignStatement assign = (CodeAssignStatement) statements[0];我们那可以安全地传递CodeThisReferenceExpression,因为包含构件的基本类型实现IContainer,这正是构造函数超载期待完成的。
//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;
}
}
直到现在,除了ExpandableObjectConverter和被扩展属性,没有提供任何与集成开发环境的集成。让我们看一下怎样提高设计时期经验。
深层集成开发环境集成
到目前为止,在有些方面的实现仍然很薄弱:
• . 在属性浏览器内,被扩展属性内部映射值所作的改变不会总是马上被保存(有些时候它们根本没有被保存)
• 一个可视构件被重新命名,我们失去所有的视图映射,因为它们被储存在ConfiguredViews表(此ConfiguredViews表是建立在它的名称的基础上的)中。同样的,此构件被删除时,映射不会相应地从那个表中被删除。
• 当控制器不是根组件时,一些属性不应该出现在属性浏览器汇总(甚至在)Intellisense中),例如控制器 Components,或者是ConfiguredViews。
• 一个控制器可能需要访问一个DB 连接。如果它支持web.config-bindable 属性或者EXEs 中的app.config 那就非常好。
• 键入控件和模式属性名称,而且模式名称是易于出错的。
• 如果需要很多控件, 通过控件来设置视图映射控件可能非常麻烦。
• 没有办法马上检查所有被应用映射。
集成开发环境无法知道ViewInfo对象属于控制器构件,当对象改变时,必须再次马上串行化它,这就是第一个问题带来的后果。完成这一任务的一个方法就是修改所有属性的设值函数,因此它们能引起的某种事件,可以将事件限制在包含的控制器中。尽管这是一个更加有效的方案,它要求输写代码,通知对每一个属性设值函数所做出的改变,而且它还要求.NET给我们提供一个更好的方法
在集成开发环境内部,所有属性对构件的改变都是通过所谓的描述符来执行的,这是System.ComponentModel.PropertyDescriptor类型的实例。对通过属性浏览器而被应用的改变来说,这一点是非常正确的。事实上,许多控件和Windows Forms绑定机制使用这个方法。通过把System.Windows.Forms.dll IL代码和ILDasm工具倾印起来,以及操作搜索PropertyDescriptor::SetValue,你可以核对检查
描述符有一个方法AddValueChanged,此方法AddValueChanged允许我们补充一个处理程序,无论什么时候,当属性被改变时,可以调用此处理程序。为了运行序列化过程,这正是我们所要做的事情。如果我们早已附加了我们的处理程序,我们也可以为ViewInfo类型补充一个IsHooked 标记。
//Verify that we are trapping the changes.我们使用TypeDescriptor类型,以此来得到PropertyDescriptor对象集合,此PropertyDescriptor对象集合可以应用到我们的对象。这个类型有许多静态方法,以此来处理值得考虑的构件。看一下中的MSDN对成员表以及它们的使用的帮助。
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;
}
RaiseViewInfoChanged处理程序通知宿主:通过使用来自于集成开发环境的另一个服务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);
}
}
第二个问题就是:使用同样的服务,可以完成追踪命名和删除控件改变(追踪命名和删除控件改变早已被映射了),但是在这种情况下,这是一种全局追踪,我们不可能像上面所完成的任务一样来完成这种全局追踪。完成这种全局追踪的方法就是为我们的构件创建一个设计器,聆听改变。一个设计器允许我们为构件补充更多设计时期功能,马上会看到这一结果。此设计器,就像RootDesigner,是通过一个属性连接起来的:
[Designer(typeof(ControllerDesigner))]ControllerDesigner类型是从ComponentDesigner继承来的,ControllerDesigner类型是一个与IComponent接口相连接的默认设计器。一个设计器有更多的控制能力,并且与集成开发环境交流的更多,当根组件 例如:一个 Form 或者Page)被设计时,它一直存在,而且它被永远地连接到它正在 “设计”构件,在我们的情况中,是一个具体的控制器。设计器中的关键点就是Initialize方法:
public class BaseController : Component, IContainer, IExtenderProvider
internal class Controller Designer : ComponentDesigner正如在代码注释中所提到的,在Initialize方法中必须做的第一件事情就是调用基本类型实现。这允许设计器正确地与集成开发环境连接起来。设计器包括一个Component属性,这个功能引入的IComponent参数中设置有此Component属性。运行之后,使用通用GetService方法,我们请求服务,在基本ComponentDesigner类型中,直接实现了此过程。此实现只是将调用传递到设计器Component.Site属性,这仅仅是一条捷径。
{
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);
}
}
Base Controller Current Controller
{
get { return (Base Controller) this.Component; }
}
}
使用现有的服务,我们增加兴趣的两个事件:ComponentRemoving 以及ComponentRename。首先看一下重命名处理程序。
void OnComponentRename(object sender, ComponentRenameEventArgs e)我们简单地删除了映射,使用新的构件名称,重新补充此映射作为键。最后必须通知环境再一次引发代码生成。通过一个基本ComponentDesigner类型,RaiseComponentChanging就完成了这一过程。删除处理程序有一点点不同:
{
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"]);
}
}
void OnComponentRemoving(object sender, ComponentEventArgs e)在这里使用另一个服务IReferenceService,IReferenceService允许我们重新找回构件名称,一个特定类型的构件集合,此特定类型的构件集合存在在宿主内部。通过使用它,我们知道被删除的构件的名称,相应地我们删除映射,通知环境,就像我们先前所做的一样。
{
IReferenceService svc = (IReferenceService)
GetService(typeof(IReferenceService));
string id = svc.GetName(sender);
if (id != null && CurrentController.ConfiguredViews.Contains键(id))
{
CurrentController.ConfiguredViews.Remove(id);
//Notify environment of the change.
RaiseComponentChanging(
TypeDescriptor.GetProperties(CurrentController)["ConfiguredViews"]);
}
}
下一个问题就是:依靠方案,隐藏成员,使其不出现在属性浏览器中。隐藏公共属性通常包括创建一个属性。
[Browsable(false)]简单地隐藏这个BaseController成员,使其不出现在属性浏览器中。有时候,我们想隐藏一个成员,既不想它出现在属性浏览器中,也不想它出现在智能感知中,例如:对于那些能被生成代码利用的成员,我们把它们创建成公共的,但是我们要为用户隐藏这些代码。其中一个例子就是Get/Set方法,Get/Set方法是作为属性提供程序实现而工作的。类型用户完全不应该看到它们,因为它们仅仅与集成开发环境和面向方面的程序设计模式相关。使用另一个属性,我们可以完成这一过程。
public ComponentCollection Components
[EditorBrowsable(EditorBrowsableState.Never)]依靠某种外部因素来有选择地隐藏属性不是一项简单的事情。只有控制器是根组件时,我们才谈到隐藏ConfiguredViews。我们为它创建了一个根设计器:
public ViewInfo GetWinViewMapping(object target)
public class ControllerRootDesigner : ComponentDocumentDesigner在基本ComponentDesigner中有许多可以被覆盖的成员,ComponentDocumentDesigner是从基本ComponentDesigner中继承来的。我们预过滤了属性,核对IDesignerHost.RootComponent,决定是否删除属性。GetService实现像平常一样向当前构件Site属性提出请求,再次指出这一点是非常值得的。
{
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");
}
}
集成开发环境为我们提供了一个嵌入式机制,以此来在应用程序配置文件中保存属性。通过DynamicProperties 种类下面的属性浏览器可以利用这个功能。然后会出现对话框,此对话框使构件属性能绑定到应用程序配置文件中appSettings章的一个键。我们用一个单属性ConnectionString实现一个IConnectable接口,ConnectionString的控制器以及模式可以用来发出信号——它们需要某种类型的一个连接点:
public interface IConnectable
{
string ConnectionString { get; set; }
}
样本PublisherController像下面一样实现它。
[RecommendedAsConfigurable(true)]它简单地将设置宣传到任何包含的可连接的构件。当点击DynamicProperties下面的Advanced item旁边的椭圆时,构件中的每一个公共读写属性(如果它的类型是一个嵌入式的)出现在打开的对话框中。通过应用RecommendedAsConfigurable属性,它也会直接出现在Advanced item下面,就像下面的情况一样:
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;

注意:属性被绑定到config文件,正如小绿色图标(ConnectionString属性旁边)所显示的。如果首先设置属性值,它们定义DynamicProperties下面的键,在应用程序配置文件中,增添正确的输入途径:
<configuration>为了解决最后三个问题(为映射属性提供值的序列表,创建一个用户界面,将所有映射设置在一个单独的位置,核实配置的映射),我们将利用集成开发环境的服务型程序结构,用自己的服务扩展它。
<appSettings>
<add 键="Pubs"
value="data source=.\NetSdk;initial catalog=pubs;
persist security info=False;user id=sa;" />
</appSettings>
0
相关文章