现在的位置: 首页 > 综合 > 正文

Leveraging .NET Components and IDE Integration: UI AOP in an MVC use case By Daniel Cazzulino [XML MVP]

2014年01月18日 ⁄ 综合 ⁄ 共 38434字 ⁄ 字号 评论关闭
 

Contents

Introduction

Microsoft targets .NET as a platform extremely well suited for component
development. Behind this assertion is a brand-new architecture that really turns
it into much more than a marketing hype. Microsoft shows that it has learned a
lot from its previous component-oriented architectures (COM/ActiveX/OLE), and
offers the developer of professional components a comprehensive set of features
(both at design-time, inside the VS.NET IDE, and at run-time) that make them
much easier to develop and at the same time much more powerful and useful.

But what are “software components” after all? Through this article, we will
unmask what components really are in the .NET and VS.NET world, and we will
specifically discuss the advanced features made available to them through the
IDE that allow us to create effective professional components that can greatly
boost programmer productivity, increase the separation of concerns and enforce
design patterns across the company.

We will take advantage of as many advanced features as possible and build a
model-view-controller framework on top of them. In the meantime, we will see
some minor drawbacks and hacks generally necessary when developing hi-end
componentized architectures. This is a prototype application development model
that can be completed and extended easily to build production-quality
applications on top.

During the course of this article, we will discuss:

  • .NET and VS.NET vision of components: the building blocks and how they fit
    together.
  • The design-time architecture.
  • The MVC pattern: separating concerns and component responsibilities. Brief
    overview and our proposed architecture.
  • Aspect Oriented Programming (AOP): extending existing components with new
    features. How to do it without inheritance or containment through VS.NET
    architecture.
  • Integration of components with the IDE: through the property browser and
    designers.
  • Taking advantage of services provided by the host (IDE).
  • How to control code generation.
  • Design patterns to increase component reuse: making components
    cross-technology (Web and Windows-aware).
  • How to provide custom services through the VS.NET architecture.
  • Extending design-time infrastructure at run-time.

We will start with a brief architectural overview before we move on to
implementation and code.

A component-oriented architecture

Let’s start by defining a component:

A component in the .NET world is any class that directly
or indirectly implements the System.ComponentModel.IComponent
interface.

That was really straightforward. Now let’s see a brief list of some classes
in the .NET framework that are components in this sense (the ones we will use
for the rest of the article):

  • System.ComponentModel.Component: base implementation of
    IComponent that usually serves as the base class for non-visual
    components.
  • System.Web.UI.Control: base class for all ASP.NET controls,
    implements IComponent directly.
  • System.Windows.Forms.Control: inherits from
    System.ComponentModel.Component and is the base class for all
    Windows Forms controls.
  • System.Data.DataSet: this class inherits from
    System.ComponentModel.MarshalByValueComponent, which in turn
    implements IComponent.
  • System.Diagnostics.EventLog: inherits from
    System.ComponentModel.Component.

As you can see, pretty much everything is a component in the .NET platform.
The main consequence of a class being a component is that the IDE has features
that are made available to it. The key property for the IDE to offer services to
components is the IComponent.Site. A so-called sited component is
one that has been placed in a Container. This containment is general and
is not related to visual containment.

For example, an ASP.NET server control, when dropped in a Web Form, is said
to be sited. Its Site property (part of the implementation of the
IComponent interface), is set to the host where the component lives
now, which inside VS.NET is an instance of the
Microsoft.VisualStudio.Designer.Host.DesignSite class. Exactly the
same object type is assigned as the Site property of a Windows
Forms user control when dropped in the forms designer, and to a non-visual
component at design-time. There are differences between the last one and the
former, which we will discuss when we look at components in the strict sense
(non-visual IComponent implementations).

The Site property, of type ISite, contains members
that allow the component to communicate with other components, with its
container (a logical container) and services provided by it. We will learn the
benefits and how to take advantage of this as we move on. So the overall
architecture is:

The container is an object that implements the
System.ComponentModel.IContainer (IContainer alone
from now on) interface. At design-time, the container is always an instance of
the Microsoft.VisualStudio.Designer.Host.DesignerHost. This object
is the core of VS.NET IDE features for components, so let’s look at it in more
depth.

Hosted components

First of all, to look at the DesignerHost class, you will need a
tool that uses reflection and that is capable of showing non-public members of
an assembly. One such tool is Reflector, and I strongly suggest that if you’re
not using it yet, you start familiarizing with it. It’s an invaluable (and free)
tool to learn the internals of any .NET assembly. You can download it from here. The class
we’re interested in is located in the Microsoft.VisualStudio.dll assembly
in the Common7/IDE subfolder of your VS.NET installation (by default
C:/Program Files/Microsoft Visual Studio .NET/Common7/IDE). You will have
to enable reflector to display non-public members, through its View – Customize
menu.

This class implements (among many other interfaces) the
IContainer interface we talked about and which is required to be
able to contain components, and indirectly the
System.ComponentModel.Design.IDesignerHost interface (by
implementing the derived
Microsoft.VisualStudio.Designer.IDesignerDocument interface). This
interface provides the upper-level layer of services, such as creating and
destroying components, accessing designers associated with components, etc. A
component sited (and therefore contained) inside an object implementing this
interface is called a hosted component and that will mean that it can use
services provided by it. Currently only the DesignerHost implements
it, but other IDEs may choose to do so also.

The host holds most of the services we can use in our components, which are
accessible through the component Site property. Currently, the host
holds the following services: IDesignerHost,
IComponentChangeService, IExtenderProviderService,
IContainer, ITypeDescriptorFilterService,
IMenuEditorService, ISelectionService,
IMenuCommandService, IOleCommandTarget,
IHelpService, IReferenceService,
IPropertyValueUIService and ManagedPropertiesService.
As you can see, there’s a breed of services for our components to use. We will
show uses of most of these through the article.

The entry point to get these services is the IServiceProvider
interface. Both the DesignSite and the DesignerHost
classes indirectly implement this interface:

The interface contains only one method: GetService which
receives a Type indicating the service to retrieve. For
example:

IComponentChangeService ccs = (IComponentChangeService) <br>host.GetService(<span class="code-keyword">typeof</span>(IComponentChangeService));

Currently, the DesignSite (which we access through the component
Site property) provides two services itself:
IDictionaryService and IExtenderListService. Requests
for other services are passed up to the host.

These services provided by the VS.NET host are not only available directly
through the component’s Site property but also from associated
(through attributes) classes that offer design-time improvements to components.
Those other classes complete the architecture.

Design-time architecture

Besides the component/site/container architecture, there’re a number of
additional aspects that make up a feature-rich platform to offer improved
design-time support for components. This is very important for RAD tools and to
increase programmer productivity. Professional components should offer a rich
design-time experience to developers if they are to be successful.

These additional features are added to a component through Attributes.
Each type of attribute assigns a different design-time (and some of them also
run-time) feature to the component. The IDE uses these attributes in two main
areas: the design surface and its interaction with the code (behind) file and
the property browser. Note that by design surface we mean not only the
Windows or Web Forms design surfaces but also the component area below them,
which is made visible whenever a non-visual component is dropped on the
designer.

Most design-time features are contained in the System.Design.dll
assembly. The following picture shows the kind of attributes and their usage at
design-time.

We don’t show in this image the more basic attributes like
DescriptionAttribute, CategoryAttribute and others,
which provide limited features that are mainly used by the property browser.
However, there are some other attributes that add more complex and useful
characteristics to components that we don’t show either, as they are very
specific. We will introduce them as we move to more advanced scenarios.

Each of the three attributes associates another related class with the
component. DesignerAttribute associates a class directly or
indirectly implementing System.ComponentModel.Design.IDesigner
(such as System.ComponentModel.Design.ComponentDesigner,
System.Web.UI.Design.HtmlControlDesigner or
System.Windows.Forms.Design.ControlDesigner).
TypeConverterAttribute does the same for a class derived from
System.ComponentModel.TypeConverter, and
EditorAttribute for a class derived from
System.Drawing.Design.UITypeEditor (yes, it’s a weird namespace
location for this one!).

Usually, and erroneously in the author’s opinion, these attributes are
classified according to the level of design-time enhancement they provide, in
the following categorization:

  • Basic: those attributes not covered in our image.
  • Intermediate: TypeConverter and Editor attributes.
  • Advanced: Designer attribute.

We don’t subscribe to this categorization because many attributes allow us to
offer both simple and advanced design-time features. This will be clear by the
end of the article.

The three attributes (and the associated classes) provide the following
features:

  • Designer: interacts with the designer host to provide
    various design features. The common functionality offered by a designer can be
    found in the System.ComponentModel.Design.ComponentDesigner class,
    which is usually the base class for any designer. There are members to notify
    the host about changes in the component and filter the properties the host and
    the property browser will see, for example.

    The more specific features depend on the type of designer, which in turn
    depends on the type of component the designer is attached to. So, the
    System.Windows.Forms.Design.ControlDesigner (the base class for all
    Windows Forms controls) handles hooks between a control and its children, drag
    and drop operations, mouse events, etc. On the other hand, the
    System.Web.UI.Design.ControlDesigner (the base class for all Web
    Forms controls) is responsible for emitting the HTML to use to display a control
    at design-time, and for performing persistence of the control state to the page
    source (.aspx file), showing errors, etc.

    Usually, designers handle functionality that applies to the whole lifecycle
    of the component at design-time, and they are the more flexible part of the
    architecture.

  • Editor: provides custom user interfaces for editing a
    component’s properties, and optionally paints a property value in the property
    browser cell that displays it. It’s accessed through the property browser
    whenever a property with an associated editor is about to be changed. Examples
    of editor are the System.Drawing.Design.FontEditor that is
    displayed when you click the ellipsis next to a property of type
    System.Drawing.Font, or the
    System.Drawing.Design.ColorEditor that provides a dialog for color
    selection.

    You may notice that these two are different kinds of editors: the
    FontEditor appears as a modal dialog (in a Windows Forms control
    property for example), and the ColorEditor (both in Windows and Web
    controls) is shown as a dropdown widget (like the AnchorEditor
    too).

  • TypeConverter: this is by far the most difficult piece
    to classify and describe, because several features belong to it. First of all,
    it provides an extensive set of methods, many of them virtual, to convert an
    object to and from other types. This conversion is mainly used by the property
    browser to convert to/from the string representation of the object, which is
    used to show a property value. But we will see that other conversions may apply,
    and can even affect code serialization for the component.

    It can also provide a list of values for the property, which is displayed in
    dropdown mode. You may wonder what this has to do with the “Converter” word. I
    also do. Maybe this feature should have been placed in the
    Editor

    Finally, we can filter the list of properties that will appear in the
    property browser. This may be useful when you want to filter the editable
    properties based on a custom attribute you create, for example (instead of the
    default BrowsableAttribute). This is an advanced case, but you’ll
    notice that this feature is also available through the
    ComponentDesigner (the base class for almost all designers), that
    allows pre/post filtering of not only properties but also events and attributes.
    So I also wonder why this feature is here at all…

TypeConverter and Editor attributes can be applied
to an individual property or directly to the type. In this last case, any object
with a property of that type will automatically have the editor/converter
attached.

Some features of the designer are also used by the property browser, such as
the DesignerVerbs we will discuss shortly together with examples of
each attribute and their usage.

Root Components and Designers

Some classes cause a design surface to be shown where we can drop other
components. This is the case for a Web Form (Page class), a Windows
Form, or any Component-inherited class (in general). You will
notice that this is not the case for your custom classes or ASP.NET custom
controls, for example. A combination of two attributes makes it possible for the
IDE to offer a design surface for a class:

  • DesignerCategoryAttribute: the constructor of the attribute
    must specify the “Component” category.

    [System.ComponentModel.DesignerCategory(<span class="code-string">"</span><span class="code-string">Component"</span>)]
  • DesignerAttribute: the overload taking the base designer type
    must be passed, and the designer must implement IRootDesigner.

    [System.ComponentModel.Designer(<span class="code-keyword">typeof</span>(MyRootDesigner), <br><span class="code-keyword">typeof</span>(System.ComponentModel.Design.IRootDesigner)]

The IComponent interface implements the last attribute. So every
class that implements directly or indirectly this interface will have the
default design surface we see when we double click a component class, if it has
the appropriate category. The Component class is an example. The
case for ASP.NET custom controls is that the base
System.Web.UI.Control class specifies a DesignCategory
of “Code” and that’s why they’re not “designable” (for now I
hope).

When an item is selected for edition in the Solution Explorer, if it has the
appropriate category and root designer, the IDE will instantiate the root
designer and show the design surface to the user. It will also create an
instance of the component and make it the root component of the designer host.
This is the relation between objects when a Web Forms page with some child
components is opened in design view, for example:

The following pseudo code shows the Page class declaration and
the attribute that causes this behavior:

[Designer(<span class="code-keyword">typeof</span>(WebFormDesigner), <span class="code-keyword">typeof</span>(IRootDesigner))]<br><span class="code-keyword">public</span> <span class="code-keyword">class</span> Page

The WebFormsDesigner class is located in the
Microsoft.VSDesigner.dll assembly in the same folder as the
Microsoft.VisualStudio.dll we mentioned earlier. This designer, among
other things, inherits from
System.ComponentModel.Design.ComponentDesigner, which is the base
class of most designers and is the default implementation of the
IDesigner interface.

A similar process happens for Windows Forms components.

The IComponent contains the following attribute
declarations:

[Designer(<span class="code-keyword">typeof</span>(ComponentDocumentDesigner), <span class="code-keyword">typeof</span>(IRootDesigner)]<br>[Designer(<span class="code-keyword">typeof</span>(ComponentDesigner)]<br><span class="code-keyword">public</span> <span class="code-keyword">interface</span> IComponent

As we said, the first attribute defines the class that will handle the
display of the design surface when the component is the root component. The
other designer specifies the behavior the component will offer when it’s placed
inside another component, for example, a Web Form.

All the architecture we discussed so far has a primary goal of making a
programmer more productive by enhancing the design-time experience. There are
far too many features to explore, but they can only be fully realized in the
context of a concrete and highly integrated application, instead of isolated
examples. For that purpose, and to dig deep into the IDE, we will implement an
MVC framework that allows Web and Windows applications to share a common code
base and isolate visual programmers/designers from the complexities of their
business objects. This framework will be mainly based on non-visual components,
but most of the IDE integration features we will implement are equally pertinent
for visual controls.

As we move on with the implementation, we will revisit many of the concepts
of this first architecture overview, and will put them in context with concrete
code. If the MVC design pattern is already familiar to you, feel free to skip
the next section. It is not intended to be a comprehensive explanation of the
pattern, but just an introduction to let you move forward to the
implementation.

MVC: the Model-View-Controller design pattern

Under this pattern, there are three very cleanly separated concerns in an
application:

  • Model: this is the part of the architecture that holds the data about your
    business entity and its behavior. This is the only one in charge of going to a
    database, for example, to perform some action.
  • View: this is the piece that displays (or outputs) the information in the
    model. Typically represented by a form and its controls.
  • Controller: all interaction between the view and the model is isolated by
    the controller. So when the view needs to perform an operation on the model, it
    asks for it to the controller. When it needs to show data about a model, it asks
    for the model to the controller.

This separation makes for a loosely-coupled architecture where components can
evolve independently, and where changes to one of them don’t impact the others,
and maintainability is greatly increased. What’s more, the same code in the
model is reused by disparate views. And depending on the way the controller
itself is programmed, it can also be reused.

The architecture we will implement will have the following interactions:

This pattern has been implemented and adapted so many times that some purists
will surely object that this is not a “true” MVC. In the more traditional
concept, the View is in charge of displaying the data in the model, in a
pull-fashion. In our implementation, the Controller is actually
pushing the data to the View. This will prove more effective in web
scenarios, without hindering applicability to desktop applications.

In order for this model to be feasible for both Windows Forms and Web Forms
Views, we implement another pattern, the Adapter, which will take care of
updating the appropriate UI:

The benefit of this approach is that the same controller is reused across
view technologies.

We will begin the implementation and our journey through the IDE features by
implementing the core enabling piece of this puzzle: the view mappings between
the Controller and the View.

AOP in the .NET era

There are many approaches to mapping two components, one of which is using an
XML file with the data, another one may be to place those mappings in a
database. However, both of them (and others too) have a couple important
drawbacks:

  • The mapping file/storage becomes another point of maintenance.
  • The loading/parsing of the mappings becomes an issue with high-load.
  • There’s a significant departure from the usual drag &
    drop-control-set-properties-run development style.

One way to avoid these issues would be to extend the built-in controls and
implement the mapping configuration in the controls themselves. Besides being a
daunting task (just count the number of built-in Web and Windows Forms
controls), any change in the mapping feature would require modifications to the
control's code, which doesn’t seem like a very good idea. Besides that, we may
not be able to inherit third-party acquired controls.

VS.NET supports the notion of an extender, which is a component that extends
the feature set of existing components from the outside, without requiring
either inheriting, nor containing, or even accessing any internals of the
extended component. This extensibility mode is usually called Aspect Oriented
Programming, because it allows us to easily add and remove aspects (in this
case, the mappings) to existing components from outside. This technique has its own web site and there are may
articles on the internet about it. An interesting article on the topic is
available at MSDN, although its approach takes the path of custom
attributes.

The component-way to AOP are the IExtenderProvider interface and
the ProvidePropertyAttribute class in the
System.ComponentModel namespace. The attribute must be applied to
the component that will extend other components:

[ProvideProperty(<span class="code-string">"</span><span class="code-string">WebViewMapping"</span>, <span class="code-keyword">typeof</span>(System.Web.UI.Control))] <br><span class="code-keyword">public</span> <span class="code-keyword">class</span> BaseController : Component, IExtenderProvider

What this attribute is saying to VS.NET is that the component
(BaseController) will provide a property
WebViewMapping to all components of type
System.Web.UI.Control in the root component (a Web Form). We can
refine the controls for which we provide the extender property in the
implementation of the interface unique method:

<span class="code-keyword">bool</span> IExtenderProvider.CanExtend(<span class="code-keyword">object</span> extendee)<br>{<br><span class="code-keyword">return</span> <span class="code-keyword">true</span>;<br>}

Here we are saying that we always support Control-inherited
objects (the ProvideProperty filter has already passed at this
stage). Mappings don’t make sense, however, for the root component, that is, the
Page object itself, which also inherits from Control.
To verify this condition, we can make use of the IDesignerHost
service, which can be accessed from our component directly by calling the
GetService method, as we discussed at the beginning:

<span class="code-keyword">bool</span> IExtenderProvider.CanExtend(<span class="code-keyword">object</span> extendee)<br>{<br><span class="code-comment">//</span><span class="code-comment">Retrieve the service.<br></span>  IDesignerHost host = (IDesignerHost) GetService(<span class="code-keyword">typeof</span>(IDesignerHost));<br><span class="code-comment">//</span><span class="code-comment">Never allow mappings at the root component level (Page or Form).<br></span>  <span class="code-keyword">if</span> (extendee == host.RootComponent) <br>  {<br><span class="code-keyword">return</span> <span class="code-keyword">false</span>;<br>  }<br><span class="code-keyword">else</span><br>  {<br><span class="code-keyword">return</span> <span class="code-keyword">true</span>;<br>  }<br>}

The Component class implementation of GetService
simply forwards the request to the Component.Site property value
object, which is, as we saw, the DesignSite, and which contains
several other services we will take advantage of as we go.

It’s important to note that both the interface and the attribute have to be
implemented in order for this to work. The final piece is a couple of methods
with specific naming conventions, which must exist in the extender
component:

<span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment">&lt;</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">&gt;</span><br></span><span class="code-SummaryComment">///</span><span class="code-comment"> Gets/Sets the view mapping that is used with the control.<br></span><span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment">&lt;</span><span class="code-SummaryComment">/</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">&gt;</span><br></span>[Category(<span class="code-string">"</span><span class="code-string">ClariuS MVC"</span>)]<br>[Description(<span class="code-string">"</span><span class="code-string">Gets/Sets the view mapping that is used with the control."</span>)]<br><span class="code-keyword">public</span> <span class="code-keyword">string</span> GetWebViewMapping(<span class="code-keyword">object</span> target)<br>{<br><span class="code-comment">//</span><span class="code-comment">Retrieve a mapping<br></span>}<br><br><span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment">&lt;</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">&gt;</span><br></span><span class="code-SummaryComment">///</span><span class="code-comment"> Sets the mapping that applies to this control.<br></span><span class="code-SummaryComment">///</span><span class="code-comment"> <span class="code-SummaryComment">&lt;</span><span class="code-SummaryComment">/</span><span class="code-SummaryComment">summary</span><span class="code-SummaryComment">&gt;</span><br></span><span class="code-keyword">public</span> <span class="code-keyword">void</span> SetWebViewMapping(<span class="code-keyword">object</span> target, <span class="code-keyword">string</span> value)<br>{<br><span class="code-comment">//</span><span class="code-comment">Store the mapping<br></span>}

The naming convention is: Get/Set + [property name used in
ProvidePropertyAttribute]. The GetXX return value must
match the value parameter of the SetXX method.
If we were implementing only the code shown so far (with the addition of a private field to hold the value)
and we dropped a BaseController component on a WebForm, we would
see the following new property in the property browser, attached to any control
on the page:

Note that the Category and Description attributes
applied on the GetWebViewMapping method are used just as if they
were applied to a regular property. The Get is always the one that
counts for all the usual property-attributes.

As the state for the property is kept outside the control itself, inside our
component, the target parameter that both methods receive allows us
to determine the object for which the property is being accessed. We will
usually keep a hashtable with the configured properties for each control, based
on its unique identifier. Furthermore, a single value isn’t usually enough to
keep our information, so we can create another class to keep the settings. In
our case, it’s the ViewInfo class. This class contains the
following properties: ControlID, ControlProperty,
Model and ModelProperty. All of them are strings and
will be used to synchronize the model values with the configured control. If the
property is not a simple type, we can offer improved property browser
integration by assigning a specific type converter with the class:

[TypeConverter(<span class="code-keyword">typeof</span>(ExpandableObjectConverter))]<br><span class="code-keyword">public</span> <span class="code-keyword">class</span> ViewInfo

This converter is provided in the System.ComponentModel
namespace, and causes the property browser to display the property as
follows:

The property can be expanded and configured directly in the property browser.
The custom string representation shown next to the property name is simply a
matter of overriding the class ToString method:

<span class="code-keyword">public</span> <span class="code-keyword">override</span> <span class="code-keyword">string</span> ToString()<br>{<br><span class="code-keyword">if</span> (_controlproperty == <span class="code-SDKkeyword">String</span>.Empty &amp;&amp; <br>    _model == <span class="code-SDKkeyword">String</span>.Empty &amp;&amp; _modelproperty == <span class="code-SDKkeyword">String</span>.Empty)<br><span class="code-keyword">return</span> <span class="code-string">"</span><span class="code-string">No mapping configured."</span>;<br><span class="code-keyword">else</span><br><span class="code-keyword">return</span> _model + <span class="code-string">"</span><span class="code-string">."</span> + _modelproperty + <span class="code-string">"</span><span class="code-string"> -&gt; "</span> + <br>      _controlid + <span class="code-string">"</span><span class="code-string">."</span> + _controlproperty;<br>}

In order to access the property for the current control, we could use the
following code:

<span class="code-keyword">public</span> ViewInfo GetWebViewMapping(<span class="code-keyword">object</span> target)<br>{<br><span class="code-keyword">return</span> ConfiguredViews[((System.Web.UI.Control)target).ID] <span class="code-keyword">as</span> ViewInfo;<br>}<br><br><span class="code-keyword">public</span> <span class="code-keyword">void</span> SetWebViewMapping(<span class="code-keyword">object</span> target, ViewInfo value)<br>{<br>  ConfiguredViews[((System.Web.UI.Control)target).ID] = value;<br>}

where ConfiguredViews is a property of type
Hashtable which keeps the mappings. Note that the extended property
is just like another member for the IDE and its persistence in code. So right
now, the IDE will not know how to persist the new WebViewMapping
property of the control. It won’t know either how to persist the
ConfiguredViews property of the component itself. In the next
section, we will discuss how to emit custom code to persist these values. To
tell VS.NET it should ignore these properties in the persistence (called
serialization) process, we add the following attribute:

[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]

This attribute only has to be applied to the GetXX method which
is the one that counts for attributes, as learned above.

You may have noticed that the ProvideProperty attribute we used
specifically states that we are extending web controls. Wouldn’t it have been
great to have the following attributes applied?:

[ProvideProperty(<span class="code-string">"</span><span class="code-string">ViewMapping"</span>, <span class="code-keyword">typeof</span>(System.Web.UI.Control))] <br>[ProvideProperty(<span class="code-string">"</span><span class="code-string">ViewMapping"</span>, <span class="code-keyword">typeof</span>(System.Windows.Forms.Control))] <br><span class="code-keyword">public</span> <span class="code-keyword">class</span> BaseController : Component, IExtenderProvider

Well, this will not work, actually. Even if we can have both, the first
extender property used “wins”. That is, if we open a Windows Forms with a
controller and define some mappings, Web Forms controls will no longer see the
extended property for the entire VS.NET session. We would have to close and
reopen VS.NET in order to get the extended property again in Web Forms. But
then, if Web Forms “wins”, Windows Forms lose. So we implement the Get/Set for
each one:

[ProvideProperty(<span class="code-string">"</span><span class="code-string">WinViewMapping"</span>, <span class="code-keyword">typeof</span>(System.Windows.Forms.Control))]<br>[ProvideProperty(<span class="code-string">"</span><span class="code-string">WebViewMapping"</span>, <span class="code-keyword">typeof</span>(System.Web.UI.Control))]<br><span class="code-keyword">public</span> <span class="code-keyword">class</span> BaseController : Component, IExtenderProvider

What we achieved, effectively, is adding functionality to existing controls
without actually touching them. Now we need a way to persist these configured
values before we move on, because right now, we will lose all values as soon as
we close the form.

Custom Code Serialization: the CodeDom power

The main object persistence mechanism in VS.NET is handled through direct
code emission. You have already seen this in the
InitializeComponent method that all Web and Windows Forms contain.
It’s also present in Component-inherited classes. Two attributes
determine this behavior:
System.ComponentModel.Design.Serialization.RootDesignerSerializerAttribute
and
System.ComponentModel.Design.Serialization.DesignerSerializerAttribute.
Just like the DesignerAttribute we talked about at the beginning,
there’s a distinction between the root and the normal serializer. But unlike the
DesignerAttribute, the normal (non-root) serializer is
always used, and the root serializer is additionally used when the
component is at the same time the root component being designed. It’s usual to
customize only the non-root serializer. An indicator of this is that the
IComponent interface already includes the root serializer:

[RootDesignerSerializer(<span class="code-keyword">typeof</span>(RootCodeDomSerializer), <span class="code-keyword">typeof</span>(CodeDomSerializer))] <br><span class="code-keyword">public</span> <span class="code-keyword">interface</span> IComponent

but it doesn’t provide the attribute designating the regular serializer.
Rather, this attribute is provided by specific implementations of
IComponent, such as:

[DesignerSerializer(<span class="code-keyword">typeof</span>(Microsoft.VSDesigner.WebForms.ControlCodeDomSerializer)), <br><span class="code-keyword">typeof</span>(CodeDomSerializer))] <br><span class="code-keyword">public</span> <span class="code-keyword">class</span> System.Web.UI.Control : IComponent

and

[DesignerSerializer(<span class="code-keyword">typeof</span>(System.Windows.Forms.Design.ControlCodeDomSerializer)), <br><span class="code-keyword">typeof</span>(CodeDomSerializer))] <br><span class="code-keyword">public</span> <span class="code-keyword">class</span> System.Windows.Forms.Control : Component

Note that both have their unique serializer, because the way Windows Forms
controls are persisted to code is very different than that of Web Forms
controls. The former serializes all values and settings to the
InitializeComponent method, while the latter only stores in the
code-behind the event handlers' attachments, because control properties are
persisted in the aspx page itself.

You have surely noticed that whether the control is used in a VB.NET project
or a C# one (or any other .NET language, for that matter), the
InitializeComponent always has code emitted in the proper language.
This is possible because of a new feature in .NET called the CodeDom.
CodeDom is a set of classes that allows us to write object hierarchies
representing the more common language constructs, such as type, field and
property declarations, event attachments, try..catch..finally blocks, etc. They allow us to build
what is called an abstract syntax tree (AST) of the intended target code.
It is abstract because it doesn’t represent VB.NET or C# code, but the
constructs themselves.

What the serializers hand to the IDE are these ASTs containing the code they
wish to persist. The IDE, in turn, creates a
System.CodeDom.Compiler.CodeDomProvider-inherited class that
matches the current project, such as the
Microsoft.CSharp.CSharpCodeProvider or the
Microsoft.VisualBasic.VBCodeProvider. This object is finally
responsible for transforming the AST in the concrete language code that gets
inserted inside the InitializeComponent method.

There’s nothing terribly complicated about CodeDom, but beware that it is
extremely verbose, and can take some time to get used to. Let’s have a quick
crash-course on CodeDom.

CodeDom syntax

CodeDom is best learned by example, so let’s have a look at some C# code and
its equivalent CodeDom statements (we assume they all happen inside a class).
The code download contains a project to test for CodeDom in the
CodeDomTester folder. It’s a simple console application where you have
two skeleton methods, GetMembers and GetStatements
where you can put sample CodeDom code and see the results in the output. Let’s
see some examples:

C#:

<span class="code-keyword">private</span> <span class="code-keyword">string</span> somefield;

CodeDom:

CodeMemberField field = <span class="code-keyword">new</span> CodeMemberField(<span class="code-keyword">typeof</span>(<span class="code-keyword">string</span>), <span class="code-string">"</span><span class="code-string">somefield"</span>);

All class-level member representations, CodeMemberEvent,
CodeMemberField, CodeMemberMethod and
CodeMemberProperty, all of which inherit from
CodeTypeMember, have the private and (where applicable) final attributes by default.

C#:

<span class="code-keyword">public</span> <span class="code-keyword">string</span> somefield = <span class="code-string">"</span><span class="code-string">SomeValue"</span>;

CodeDom:

CodeMemberField field = <span class="code-keyword">new</span> CodeMemberField(<span class="code-keyword">typeof</span>(<span class="code-keyword">string</span>), <span class="code-string">"</span><span class="code-string">somefield"</span>);<br>field.InitExpression = <span class="code-keyword">new</span> CodePrimitiveExpression(<span class="code-string">"</span><span class="code-string">SomeValue"</span>);<br>field.Attributes = MemberAttributes.Public;

C#

<span class="code-keyword">this</span>.somefield = GetValue();

CodeDom:

CodeFieldReferenceExpression field = <span class="code-keyword">new</span> CodeFieldReferenceExpression(<br><span class="code-keyword">new</span> CodeThisReferenceExpression(), <span class="code-string">"</span><span class="code-string">somefield"</span>);<br>CodeMethodInvokeExpression getvalue = <span class="code-keyword">new</span> CodeMethodInvokeExpression(<br><span class="code-keyword">new</span> CodeThisReferenceExpression(), <span class="code-string">"</span><span class="code-string">GetValue"</span>, <span class="code-keyword">new</span> CodeExpression[<span class="code-digit">0</span>]);<br>CodeAssignStatement assign = <span class="code-keyword">new</span> CodeAssignStatement(field, getvalue);

Note that verbosity increases practically exponentially. Beware that the
GetValue() method call in the C# code has an implicit reference to
this, which must be made
explicit in CodeDom.

C#

<span class="code-keyword">this</span>.GetValue(<span class="code-string">"</span><span class="code-string">Someparameter"</span>, <span class="code-keyword">this</span>.somefield);

CodeDom

CodeMethodInvokeExpression call = <span class="code-keyword">new</span> CodeMethodInvokeExpression();<br>call.Method = <span class="code-keyword">new</span> CodeMethodReferenceExpression(<br><span class="code-keyword">new</span> CodeThisReferenceExpression(), <span class="code-string">"</span><span class="code-string">GetValue"</span>);<br>call.Parameters.Add(<span class="code-keyword">new</span> CodePrimitiveExpression(<span class="code-string">"</span><span class="code-string">Someparameter"</span>));<br>CodeFieldReferenceExpression field = <span class="code-keyword">new</span> <br>  CodeFieldReferenceExpression(<span class="code-keyword">new</span> CodeThisReferenceExpression(), <span class="code-string">"</span><span class="code-string">somefield"</span>);<br>call.Parameters.Add(field);

Here we are calling a hypothetical overload of the same method we called
before. Note that we first create the method invoke expression. Then we assign
its method reference to the one pointing to this and the method name. Next we append the
two parameters, the second being a reference to the field as we did above for
the assignment.

If you want (and you will, I guarantee that) to avoid endless (and useless)
variable declarations, you can compose the statements without resorting to
temporary variables. This makes the code less legible, but much more compact. A
good technique for creating those (rather lengthy) statements is to think about
the target code to generate from inside out. For example, in the code above:

<span class="code-keyword">this</span>.GetValue(<span class="code-string">"</span><span class="code-string">Someparameter"</span>, <span class="code-keyword">this</span>.somefield);

We should start by the parameters, then think about the method reference, and
once we have that in mind, write something like this:

CodeMethodInvokeExpression call = <br><span class="code-keyword">new</span> CodeMethodInvokeExpression(<span class="code-keyword">new</span> CodeThisReferenceExpression(), <br><span class="code-string">"</span><span class="code-string">GetValue"</span>, <br><span class="code-keyword">new</span> CodeExpression[] { <span class="code-keyword">new</span> CodePrimitiveExpression(<span class="code-string">"</span><span class="code-string">Someparameter"</span>),<br><span class="code-keyword">new</span> CodeFieldReferenceExpression(<span class="code-keyword">new</span> CodeThisReferenceExpression(), <br><span class="code-string">"</span><span class="code-string">somefield"</span>)});

It looks pretty bad, but again, analyze it from inside-out: the last thing we
see is a this.somefield.
Next, the primitive expression. That is passed as the initialization expression
for the array of parameters to the method call. Then you have the this.GetValue and finally that
makes the actual invoke.

Note that proper indenting can greatly help, but you have to do it mostly by
yourself, especially with several nesting levels. The most important nesting in
order to achieve some legibility is the array initialization, as shown above.
It’s also recommended that you put the intended C# (or VB.NET) output code just
above the big multi-line statement, so that anyone can know what you’re trying
to emit (maybe even yourself after a week!).

There are classes to define all the cross-language features. But let’s look
at the concrete persistence code we need for our extended property.

Emitting CodeDom

Like we said, we will need to associate a custom serializer with our
BaseController in order to customize persistence and emit the code
to preserve the view mappings and potentially any code we need.

[DesignerSerializer(<span class="code-keyword">typeof</span>(ControllerCodeDomSerializer), <br><span class="code-keyword">typeof</span>(CodeDomSerializer))]<br><span class="code-keyword">public</span> <span class="code-keyword">class</span> BaseController : Component, IExtenderProvider

Our custom serializer must inherit from CodeDomSerializer. This
base abstract class, located in the
System.ComponentModel.Design.Serialization, contains two abstract
methods that we must implement:

<span class="code-keyword">public</span> <span class="code-keyword">abstract</span> <span class="code-keyword">class</span> CodeDomSerializer<br>{<br><span class="code-keyword">public</span> <span class="code-keyword">abstract</span> <span class="code-keyword">object</span> Serialize(<br>      IDesignerSerializationManager manager, <span class="code-keyword">object</span> value);<br><span class="code-keyword">public</span> <span class="code-keyword">abstract</span> <span class="code-keyword">object</span> Deserialize(<br>      IDesignerSerializationManager manager, <span class="code-keyword">object</span> codeObject);<br>}

The Serialize method is called whenever our object needs to be
persisted, for example when a property changes. The return value must be an
object of type CodeStatementCollection containing the code to
persist. Likewise, the codeObject parameter in the
Deserialize method contains the statements that have previously
been emitted.

A little insight on this process and how the IDE works is helpful here. We
said in the introductory section that VS.NET instantiates the root component in
order to “design” it through the root designer. This process actually happens
for all the components (and controls) in the root component. What’s actually
happening is that the IDE executes most of the code in
InitializeComponent in order to recreate the objects just as they
will be at run-time. We say most and not all because only the
statements modifying the component in question are called: i.e., property sets
and method calls in them. We are given an opportunity to interact in this
design-time recreation process by customizing the Deserialize
method. Usually this is not necessary, so most of the time we will just be
passing the ball to the original component serializer, the
ComponentCodeDomSerializer, which basically “executes” the code. In
order to get the serializer for a type, we use the GetSerializer
method of the IDesignerSerializationManager parameter we receive.
This object has other useful methods we will use later.

So our Deserialize implementation usually looks like this:

<span class="code-keyword">public</span> <span class="code-keyword">override</span> <span class="code-keyword">object</span> Deserialize(<br>  IDesignerSerializationManager manager, <span class="code-keyword">object</span> codeObject)<br>{<br>  CodeDomSerializer serializer = <br>    manager.GetSerializer(<span class="code-keyword">typeof</span>(Component), <span class="code-keyword">typeof</span>(CodeDomSerializer));<br><span class="code-keyword">return</span> serializer.Deserialize(manager, codeObject);<br>}

Retrieving the component original serializer is a common usage of the
manager, and as the deserialization is usually (and for our implementation,
always) the same, we will put that in a base class from which we will inherit
our controller serializer:

<span class="code-keyword">internal</span> <span class="code-keyword">abstract</span> <span class="code-keyword">class</span> BaseCodeDomSerializer : CodeDomSerializer<br>{<br><span class="code-keyword">protected</span> CodeDomSerializer GetBaseComponentSerializer(<br>    IDesignerSerializationManager manager)<br>  {<br><span class="code-keyword">return</span> (CodeDomSerializer) <br>      manager.GetSerializer(<span class="code-keyword">typeof</span>(Component), <span class="code-keyword">typeof</span>(CodeDomSerializer));<br>  }<br><br><span class="code-keyword">public</span> <span class="code-keyword">override</span> <span class="code-keyword">object</span> Deserialize(<br>    IDesignerSerializationManager manager, <span class="code-keyword">object</span> codeObject)<br>  {<br><span class="code-keyword">return</span> GetBaseComponentSerializer(manager).Deserialize(manager, <br>                                                       codeObject);<br>  }<br>}

We will add other common methods to this class later on. Now we have to go to
the serialization process. We need to iterate through all the
DictionaryEntry elements of the Hashtable and emit
code like the following in order to persist our ConfiguredViews
property:

controller.ConfiguredViews.Add(<span class="code-string">"</span><span class="code-string">txtID"</span>, <br><span class="code-keyword">new</span> ViewInfo(<span class="code-string">"</span><span class="code-string">txtID"</span>, <span class="code-string">"</span><span class="code-string">Text"</span>, <span class="code-string">"</span><span class="code-string">Publisher"</span>, <span class="code-string">"</span><span class="code-string">ID"</span>));

Another common practice is to let the original component serializer to do its
work and then add our custom statements. This way we avoid having to persist the
common component properties ourselves. So our serializer starts by doing just
that:

<span class="code-keyword">internal</span> <span class="code-keyword">class</span> ControllerCodeDomSerializer : BaseCodeDomSerializer<br>{<br><span class="code-keyword">public</span> <span class="code-keyword">override</span> <span class="code-keyword">object</span> <br>         Serialize(IDesignerSerializationManager manager, <span class="code-keyword">object</span> value)<br>  {<br>    CodeDomSerializer serial = GetBaseComponentSerializer(manager);<br><span class="code-keyword">if</span> (serial == <span class="code-keyword">null</span>) <br><span class="code-keyword">return</span> <span class="code-keyword">null</span>;<br>    CodeStatementCollection statements = (CodeStatementCollection) <br>      serial.Serialize(manager, value);

It’s important to note that our serializer will be called even when the root
component is the BaseController itself. In this case, we don’t want
to persist our custom code, as it applies basically when it’s used inside
another component, such as a Page or a Form. To take
this into account, we ask for the IDesignerHost service we used
before and check against its RootComponent property. The
IDesignerSerializationManager implements
IServiceProvider, so we have the usual GetService
method to do that:

  IDesignerHost host = (IDesignerHost)<br>    manager.GetService(<span class="code-keyword">typeof</span>(IDesignerHost));<br><span class="code-keyword">if</span> (host.RootComponent == value)<br><span class="code-keyword">return</span> statements;

The base CodeDomSerializer class has a number of useful methods
to work with while serializing/deserializing code. When we access the
ConfiguredViews property, we do so through a reference to the
actual controller being processed (the value parameter). One helper method in the base
class creates the appropriate CodeDom object to use this reference in our
so-called CodeDom graph:

  CodeExpression cnref = SerializeToReferenceExpression(manager, value);

The CodeExpression can be used now to create the property
reference:

  CodePropertyReferenceExpression propref = <br><span class="code-keyword">new</span> CodePropertyReferenceExpression(cnref, <span class="code-string">"</span><span class="code-string">ConfiguredViews"</span>);

We define these two variables mainly to simplify a little the expression we
will build next. Let’s have a second look at the sample target method call:

controller.ConfiguredViews.Add(<span class="code-string">"</span><span class="code-string">txtID"</span>, <br><span class="code-keyword">new</span> ViewInfo(<span class="code-string">"</span><span class="code-string">txtID"</span>, <span class="code-string">"</span><span class="code-string">Text"</span>, <span class="code-string">"</span><span class="code-string">Publisher"</span>, <span class="code-string">"</span><span class="code-string">ID"</span>));

We already have the first part up to the ConfiguredViews
property access. The next parts are:

  • CodeMethodInvokeExpression: the call to Add.
  • CodeExpression[]: the parameters to the method call.
  • CodePrimitiveExpression: the "txtID" raw string
    value.
  • CodeObjectCreateExpression: the new ViewInfo part.
  • CodePrimitiveExpression: one for each primitive string value
    passed to the constructor.

So the code is:

BaseController cn = (BaseController) value;<br>CodeExpression cnref = SerializeToReferenceExpression(manager, value);<br><br>CodePropertyReferenceExpression propref = <br><span class="code-keyword">new</span> CodePropertyReferenceExpression(cnref, <span class="code-string">"</span><span class="code-string">ConfiguredViews"</span>);<br><span class="code-comment">//</span><span class="code-comment">Iterate the entries<br></span><span class="code-keyword">foreach</span> (DictionaryEntry cv <span class="code-keyword">in</span> cn.ConfiguredViews)<br>{<br>  ViewInfo info = (ViewInfo) cv.Value;<br><span class="code-keyword">if</span> (info.ControlID != <span class="code-SDKkeyword">String</span>.Empty &amp;&amp; info.ControlProperty != <span class="code-keyword">null</span> &amp;&amp;<br>    info.Model != <span class="code-SDKkeyword">String</span>.Empty &amp;&amp; info.ModelProperty != <span class="code-SDKkeyword">String</span>.Empty)<br>  {  <br><span class="code-comment">//</span><span class="code-comment">Generates:<br></span>    <span class="code-comment">//</span><span class="code-comment">controller.ConfiguredViews.Add(key, new ViewInfo([values]));<br></span>    statements.Add(<br><span class="code-keyword">new</span> CodeMethodInvokeExpression(<br>        propref, <span class="code-string">"</span><span class="code-string">Add"</span>, <br><span class="code-keyword">new</span> CodeExpression[] { <br><span class="code-keyword">new</span> CodePrimitiveExpression(cv.Key), <br><span class="code-keyword">new</span> CodeObjectCreateExpression(<br><span class="code-keyword">typeof</span>(ViewInfo),<br><span class="code-keyword">new</span> CodeExpression[] { <br><span class="code-keyword">new</span> CodePrimitiveExpression(info.ControlID),<br><span class="code-keyword">new</span> CodePrimitiveExpression(info.ControlProperty), <br><span class="code-keyword">new</span> CodePrimitiveExpression(info.Model), <br><span class="code-keyword">new</span> CodePrimitiveExpression(info.ModelProperty) }<br>          ) }<br>        ));<br>  }<br>}

Notice how proper indenting helps in making the statements more readable. And
we only declared two temporary variables, which could even be omitted
altogether, by the way.

We can add comments to the code also, with:

statements.Add(<span class="code-keyword">new</span> <br>  CodeCommentStatement(<span class="code-string">"</span><span class="code-string">-------- ClariuS Custom Code --------"</span>));

Going back to a form using the component, we would have the following code in
the relevant InializeComponent section:

<span class="code-keyword">private</span> <span class="code-keyword">void</span> InitializeComponent()<br>{<br>  ...<br><span class="code-comment">//</span><span class="code-comment"> ------------- ClariuS Custom Code -------------<br></span>  <span class="code-keyword">this</span>.controller.ConfiguredViews.Add(<span class="code-string">"</span><span class="code-string">txtID"</span>, <br><span class="code-keyword">new</span> Mvc.Components.Controller.ViewInfo(<span class="code-string">"</span><span class="code-string">txtID"</span>, <br><span class="code-string">"</span><span class="code-string">Text"</span>, <span class="code-string">"</span><span class="code-string">Publisher"</span>, <span class="code-string">"</span><span class="code-string">ID"</span>));<br><span class="code-keyword">this</span>.controller.ConfiguredViews.Add(<span class="code-string">"</span><span class="code-string">txtName"</span>, <br><span class="code-keyword">new</span> Mvc.Components.Controller.ViewInfo(<span class="code-string">"</span><span class="code-string">txtName"</span>, <br><span class="code-string">"</span><span class="code-string">Text"</span>, <span class="code-string">"</span><span class="code-string">Publisher"</span>, <span class="code-string">"</span><span class="code-string">Name"</span>));<br>  ...

The code generator fully qualifies all class references because there’s no
guarantee that the developer will add the necessary using clauses to the class.

Finally, we can also emit errors we find while generating code. We can do so,
for example, if we detect that the properties are not properly set:

if (info.ControlID != String.Empty && info.ControlProperty != 

抱歉!评论已关闭.