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)
host.GetService(typeof(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("Component")]
DesignerAttribute
: the overload taking the base designer type must be passed, and the designer must implement IRootDesigner
. [System.ComponentModel.Designer(typeof(MyRootDesigner),
typeof(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(typeof(WebFormDesigner), typeof(IRootDesigner))]
public class 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(typeof(ComponentDocumentDesigner), typeof(IRootDesigner)]
[Designer(typeof(ComponentDesigner)]
public interface 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("WebViewMapping", typeof(System.Web.UI.Control))]
public class 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:
bool IExtenderProvider.CanExtend(object extendee)
{
return true;
}
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:
bool IExtenderProvider.CanExtend(object extendee)
{
IDesignerHost host = (IDesignerHost) GetService(typeof(IDesignerHost));
if (extendee == host.RootComponent)
{
return false;
}
else
{
return true;
}
}
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:
[Category("ClariuS MVC")]
[Description("Gets/Sets the view mapping that is used with the control.")]
public string GetWebViewMapping(object target)
{
}
public void SetWebViewMapping(object target, string value)
{
}
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(typeof(ExpandableObjectConverter))]
public class 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:
public override string ToString()
{
if (_controlproperty == String.Empty &&
_model == String.Empty && _modelproperty == String.Empty)
return "No mapping configured.";
else
return _model + "." + _modelproperty + " -> " +
_controlid + "." + _controlproperty;
}
In order to access the property for the current control, we could use the following code:
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;
}
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("ViewMapping", typeof(System.Web.UI.Control))]
[ProvideProperty("ViewMapping", typeof(System.Windows.Forms.Control))]
public class 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("WinViewMapping", typeof(System.Windows.Forms.Control))]
[ProvideProperty("WebViewMapping", typeof(System.Web.UI.Control))]
public class 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(typeof(RootCodeDomSerializer), typeof(CodeDomSerializer))]
public interface 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(typeof(Microsoft.VSDesigner.WebForms.ControlCodeDomSerializer)),
typeof(CodeDomSerializer))]
public class System.Web.UI.Control : IComponent
and
[DesignerSerializer(typeof(System.Windows.Forms.Design.ControlCodeDomSerializer)),
typeof(CodeDomSerializer))]
public class 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#:
private string somefield;
CodeDom:
CodeMemberField field = new CodeMemberField(typeof(string), "somefield");
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#:
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);
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#
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);
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:
this.GetValue("Someparameter", this.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 =
new CodeMethodInvokeExpression(new CodeThisReferenceExpression(),
"GetValue",
new CodeExpression[] { new CodePrimitiveExpression("Someparameter"),
new CodeFieldReferenceExpression(new CodeThisReferenceExpression(),
"somefield")});
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(typeof(ControllerCodeDomSerializer),
typeof(CodeDomSerializer))]
public class 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:
public abstract class CodeDomSerializer
{
public abstract object Serialize(
IDesignerSerializationManager manager, object value);
public abstract object Deserialize(
IDesignerSerializationManager manager, object codeObject);
}
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:
public override object Deserialize(
IDesignerSerializationManager manager, object codeObject)
{
CodeDomSerializer serializer =
manager.GetSerializer(typeof(Component), typeof(CodeDomSerializer));
return serializer.Deserialize(manager, codeObject);
}
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:
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);
}
}
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("txtID",
new ViewInfo("txtID", "Text", "Publisher", "ID"));
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:
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);
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)
manager.GetService(typeof(IDesignerHost));
if (host.RootComponent == value)
return 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 =
new CodePropertyReferenceExpression(cnref, "ConfiguredViews");
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("txtID",
new ViewInfo("txtID", "Text", "Publisher", "ID"));
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;
CodeExpression cnref = SerializeToReferenceExpression(manager, value);
CodePropertyReferenceExpression propref =
new CodePropertyReferenceExpression(cnref, "ConfiguredViews");
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)
{
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) }
) }
));
}
}
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(new
CodeCommentStatement("-------- ClariuS Custom Code --------"));
Going back to a form using the component, we would have the following code in the relevant InializeComponent
section:
private void InitializeComponent()
{
...
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"));
...
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 != null &&
info.Model != String.Empty && info.ModelProperty != String.Empty)
{
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;
}
Note that we use other manager
methods to format the error message, GetInstance
and GetName
, which allow us to retrieve object references and their names respectively. By using continue
after an error is found, we are avoiding serialization of invalid settings, effectively removing them. The component user will see something like the following when there are invalid values:
Back to simplicity
Having discussed the power and flexibility of custom CodeDom persistence, it�s true it can be a daunting task to serialize several members using this technique. Luckily, VS.NET supports a simpler way of serializing custom types to the code-behind file.
The process simply requires that we implement a type converter for our class that can convert the object to an InstanceDescriptor
type. This is done just like we did before for string conversions:
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);
}
The InstanceDescriptor
constructor receives as the first parameter, the MemberInfo
of the method or constructor to call in order to create an instance of our custom class. We use reflection to retrieve a constructor that matches the signature of the parameter types we specify. The second parameter passes the actual arguments to the call, and finally, the third parameter specifies whether the object instantiation is complete with this method call. If we say it�s not, each property will be serialized in turn.
This mechanism works automatically if the property to be serialized is an array of ViewInfo
objects, where we would get the following automatically emitted code:
private void InitializeComponent()
{
�
this.publisherController.ConfiguredViews =
new ViewInfo [] {
new ViewInfo("txtID", "Text", "Publisher", "ID"),
new ViewInfo("txtName", "Text", "Publisher", "Name")
};
In our case, this doesn�t meet our needs, but there are many occasions where this kind of serialization is enough.
Finally, another way (rather unadvisable in the author�s opinion) is implementing ISerializable
in your class. This way, VS.NET will serialize the object directly to a resource file. This makes it much more difficult to see what�s going on behind the serialization process, which can be good or bad, depending on the point of view.
Now that we know the basics of property extenders and custom code serialization, let�s complete the MVC picture.
Completing the MVC Component Model
In our model, controllers contain models. Views access the models through the controller. The models are also components that are drag & dropped inside a controller designer. We know that the default designer allows any kind of component to be dropped in the surface. That�s now what we want. We want only models to be used in our controller, and we also want to avoid the models to be dropped on a container other than a controller.
The way to achieve this level of control is to use two related IDE features. One is attaching a custom RootDesigner
to the base controller class from which all custom controllers will inherit:
[Designer(typeof(ControllerRootDesigner), typeof( IRootDesigner))]
public class BaseController : Component, IExtenderProvider
The other is attaching a ToolboxItemFilter
both in the new root designer and the BaseModel
class (from which all custom models will inherit):
[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";
}
The ToolboxItemFilterType.Require
value tells the IDE to only enable the item in the ToolBox when both the current root designer and the component have a matching string (the first attribute parameter). There�s an interesting article with the different combinations on this attribute values and the effect on the toolbox items here.
The BaseModel
class provides a virtual property that allows inheritors to assign a name to the model, which will be used to reference it by the view mappings. As a final step, we can prevent these two base classes to appear in the toolbox by adding the following attribute to them:
[ToolboxItem(false)]
Note that this attribute is inheritable, so derived classes must have a ToolboxItem(true)
attribute if they want to be available on the toolbox. Usual OO programming advices to make base classes that are not intended to be used by developers abstract
. We can�t do that with our components, though, because currently the IDE needs to be able to instantiate them in order to work.
We can now create a PublisherController
and a PublisherModel
inheriting from these two classes, compile everything and add them to the toolbox. With the PublisherController
class in design mode, we can see the effect of the filter, which causes the PublisherController
component to be disabled. The contrary happens in a form (Windows or Web), where it will be the only one enabled:
Before the model is complete, however, we must take into account some details regarding the way VS.NET builds the component hierarchies as we drag & drop components in designers, especially when components inherit other components.
.NET Component Model: a closer look and inheritance problems
When we add a new component to a project, we get some automatically generated code from the basic template. If we drop another custom component into it, the component code will look something like the following (comments aside):
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);
}
}
The important thing to notice here is the private
components
variable. It is of type Container
and it is passed to the Component2
constructor. We know that in turn, this constructor will do just what the Component1
constructor does: adds itself to the container:
public Component2(System.ComponentModel.IContainer container)
{
container.Add(this);
InitializeComponent();
}
This component, in turn, contains its own components
field, which it will pass to any other component we drag into its design surface, which will in turn add itself to it and so on. The Container
class implementation assigns the incoming component Site
property (in the Add
method) a newly created ISite
that points to the container as well as the component. This Site
, of type Container.Site
, can later be used by the component to know who contains it. Recall that at design-time, this Site
will be pointed to the DesignSite
as explained in the introduction.
This mechanism works fine for containment, but when components inherit from other components, we end-up with a broken chain of containments, because the base class will have its own components
field. So we will have a hierarchy similar to this:
This hierarchy is created this way because the model dropped in the PublisherControler
is passed the �wrong� container. The consequence is that any code we place in the BaseController
class will not be able to access the contained components. The solution would be to pass to the model constructor the base class components
field. This, however, would involve making this field accessible to the inheritors (protected
) which in some way violates the OO principle of encapsulation. A better solution is to implement IContainer
in the base class itself, and pass a this
reference to the model constructor. The interface implementation in the base class will provide proper access to the components
field:
public class BaseController : Component, IContainer, IExtenderProvider
{
#region Implementation of IContainer
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
...
We can safely remove the base class constructor that takes the container, as the inheriting classes such as PublisherController
will add themselves to it, which is the default behavior and doesn�t conflict with inheritance:
public PublisherController(IContainer container)
{
container.Add(this);
InitializeComponent();
}
The base class doesn�t need InitializeComponent
or the special constructor code, as the components
field is always initialized in the field declaration itself.
Now, to customize the code for the model constructor, so that it uses the following code:
this.model = new Mvc.Components.Model.PublisherModel(this);
instead of the current:
this.model = new Mvc.Components.Model.PublisherModel(this.components);
we have to attach a custom CodeDomSerializer
just as we did for the base controller:
[DesignerSerializer(typeof(ModelCodeDomSerializer),
typeof(CodeDomSerializer))]
public class BaseModel : Component
The serializer code is fairly simple, compared with the controller one:
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);
Now that we have the statements generated by the built-in serializer, we have to modify it. Up to now, the statements collection contains an expression that represents the following line of code:
this.model = new Mvc.Components.Model.PublisherModel(this.components);
If we analyze it according to its CodeDom representation, we see that it will have the following form:
CodeAssignStatement
: the =
operation, with right and left parts.
CodeObjectCreateExpression
: the right part of the assignment.
CodeFieldReferenceExpression
: this is the first parameter in the expression Parameters
collection, which points to the this.components
field.
We just have to replace (or add) that parameter:
CodeAssignStatement assign = (CodeAssignStatement) statements[0];
CodeObjectCreateExpression create = (CodeObjectCreateExpression)
assign.Right;
if (create.Parameters.Count > 0)
{
create.Parameters[0] = new CodeThisReferenceExpression();
}
else
{
create.Parameters.Add(new CodeThisReferenceExpression());
}
return statements;
}
}
We can safely pass the CodeThisReferenceExpression
because the base class of the containing component implements IContainer
, as expected by the constructor overload.
Up to now, except for the ExpandableObjectConverter
and the extended property, we didn�t offer any deep integration with the IDE. Let�s see how to enhance the design-time experience.
Deep IDE Integration
There are several aspects where our implementation so far is very weak:
- Changes made to mapping values inside the expanded property in the property browser are not always persisted immediately (sometimes they�re not persisted at all).
- When a visual component is renamed, we lose all view mappings, because they are stored in the
ConfiguredViews
table based on its name. Likewise, when it�s removed the mappings are not accordingly removed from that table.
- Some properties shouldn�t appear in the property browser (even in Intellisense), such as the controller
Components
, or ConfiguredViews
when the controller is the root component.
- A controller may need to access a DB connection. It would be nice to support web.config-bindable properties (or app.config for EXEs).
- Typing the control and model properties name, as well as the model name itself is error prone.
- Setting view mappings control by control can be annoying if many controls are involved.
- There�s no way to check all applied mappings at once.
The first issue is a direct consequence of the fact that the IDE has no way to know that the ViewInfo
object belongs to the controller component, and that it should be serialized again as soon as there are changes to it. One way to do this would be to modify all property setters so that they raise some kind of event that can be trapped from the containing controller. Although this is the more evident solution, it requires writing code to notify changes to all property setters, and .NET provides us a more elegant approach.
Inside the IDE, all property changes to components are performed through so-called descriptors, which are instances of the System.ComponentModel.PropertyDescriptor
class which describe a property. This is true especially for changes applied through the property browser. In fact, many controls and the Windows Forms binding mechanism also use this approach. You can check that by dumping the System.Windows.Forms.dll IL code assembly with the ILDasm tool and performing a search for PropertyDescriptor::SetValue
.
This descriptor has a method, AddValueChanged
, which allows us to add a handler that will be called whenever the property is changed. This is exactly what we need in order to trigger the serialization process. We will also add an IsHooked
flag to the ViewInfo
class, so that we know if we have already attached our handler:
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;
}
We use the TypeDescriptor
class to get the collection of PropertyDescriptor
objects that apply to our object. This class has many static methods to handle components that are worth a look. Look at the MSDN help for a list of members and their usage.
The RaiseViewInfoChanged
handler notifies the host that a change occurred to the mappings by using another service from the IDE, the 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);
}
}
By using this service, the IDE will take appropriate measures to persist our component again.
The second problem, tracking naming and removal changes of controls that have already been mapped, is achieved using the same service, but in this case, it�s a sort of global tracking, which can�t be done as we did above. The way to do this is to create a designer for our component to listen to changes. A designer allows us to add more design-time features to our component, as we will see shortly. This designer, just like the RootDesigner
, is associated through an attribute:
[Designer(typeof(ControllerDesigner))]
public class BaseController : Component, IContainer, IExtenderProvider
Our ControllerDesigner
class inherits from ComponentDesigner
, which is the default designer associated with the IComponent
interface. A designer has greater control and interaction with the IDE, and it exists for the whole time the root component (i.e. a Form
or Page
) is being designed, and is permanently attached to the component it�s �designing�, in our case, the concrete controller. The critical point in a designer, where we can hook into the IDE services, is the Initialize
method, where we receive the component to be designed:
internal class ControllerDesigner : ComponentDesigner
{
public override void Initialize(IComponent component)
{
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; }
}
}
As stated in the code comment, the first thing we must do inside the Initialize
method is call the base class implementation. This allows the designer to be properly hooked with the IDE. The designer contains a Component
property which is set to the incoming IComponent
parameter of this function. After processing, we request for the service using the usual GetService
method, which now is implemented directly in the base ComponentDesigner
class. This implementation just passes the call to the designer�s Component.Site
property, being just a shortcut.
With the service at hand, we can attach to the two events we are interested in: ComponentRemoving
and ComponentRename
. Let�s look at the rename handler first:
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);
RaiseComponentChanging(
TypeDescriptor.GetProperties(CurrentController)["ConfiguredViews"]);
}
}
We simply remove the mapping and add it again using the new component name as the key. Finally, we have to notify the environment to trigger the code generation again. This is achieved through a method of the base ComponentDesigner
class, RaiseComponentChanging
. The removing handler is slightly different:
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);
RaiseComponentChanging(
TypeDescriptor.GetProperties(CurrentController)["ConfiguredViews"]);
}
}
Here we use another service, the IReferenceService
. It allows us to retrieve component names, a component given its name (the opposite) and the set of components of a certain type that exists inside the host. We use it to know the name of the component being removed and we remove the mapping accordingly, notifying the environment as we did before.
The next problem, hiding members from the property browser, depends on the scenario. Hiding a public property usually involves setting an attribute:
false)>
public ComponentCollection Components
This will simply hide this BaseController
member from the property browser. Sometimes we will want to hide a member not only from the property browser but also from Intellisense, such as for members that are made public in order to be accessible by the generated code, but which we want to hide from users. An example of this are the Get/Set methods that act as the property provider implementation. The class user shouldn�t see them at all, as they are relevant only for the IDE and its AOP model. We can do that using another attribute:
[EditorBrowsable(EditorBrowsableState.Never)]
public ViewInfo GetWinViewMapping(object target)
The case is not so easy to selectively hide a property depending on some external factor. We are talking about hiding the ConfiguredViews
only when the controller is the root component. We can take advantage of the fact that we created a root designer for it, and take advantage of that to filter the list of properties:
public class ControllerRootDesigner : ComponentDocumentDesigner
{
protected override void PreFilterProperties(IDictionary properties)
{
base.PreFilterProperties(properties);
IDesignerHost host = (IDesignerHost)
GetService(typeof(IDesignerHost));
if (host.RootComponent == this.Component)
properties.Remove("ConfiguredViews");
}
}
There are many overridable members in the base ComponentDesigner
, from which ComponentDocumentDesigner
inherits. There is a set of Pre/Post filter methods for properties, attributes and events. We are pre-filtering the property and checking against the IDesignerHost.RootComponent
to determine the property removal. It�s worth pointing again that the GetService
implementation forwards the request to the current component Site
property as usual.
The IDE provides us with a built-in mechanism for persisting property values in an application configuration file. This feature is available through the property browser, under the DynamicProperties category. The dialog that appears makes it possible to �bind� a component property to a key in the appSettings
section of the application configuration file. We implemented an IConnectable
interface with a single property, ConnectionString
, which controllers as well as models can use to signal they need a connection of some kind.
public interface IConnectable
{
string ConnectionString { get; set; }
}
The sample PublisherController
implements it as follows:
[RecommendedAsConfigurable(true)]
public string ConnectionString
{
get { return _connection; }
set
{
_connection = value;
foreach (IComponent model in Components)
if (model is IConnectable)
((IConnectable)model).ConnectionString = value;
}
} string _connection;
It simply propagates the setting to any contained �connectable� component. Every public read/write property (if its type is a built-in one) in a component will appear in the dialog that opens when we click the ellipses next to the Advanced item under DynamicProperties. However, by applying the RecommendedAsConfigurable
attribute, it will also appear directly below the Advanced item, as follows:
Note that the property has been bound to the config file as shown by the little green icon next to the ConnectionString
property. If we first set the property value, and they define the key under DynamicProperties, the appropriate entry is added in the application configuration file:
<configuration>
<appSettings>
<add key="Pubs"
value="data source=.\NetSdk;initial catalog=pubs;
persist security info=False;user id=sa;" />
</appSettings>
To solve the last three issues (providing list of values for the mapping properties, creating a UI to set all mappings in a single place and verifying configured mappings), we will take advantage of the serviced architecture of the IDE and extend it with our own service.
Extending the Serviced Architecture
We have been using several services from the IDE. Most of them are provided by the designer host. If we need to access a feature from several places, it doesn�t make sense to duplicate the same code. Implementing them as static members of some class may work, but we can achieve the same effect by playing according to the VS.NET rules: create a custom design-time service.
The IDesignerHost
interface provides us with a way to attach and remove our services, the AddService
and RemoveService
methods. The VS.NET way to attach services is to define an interface, and add the concrete instance through the host methods. We will need the following methods in our service implementation:
public interface IControllerService
{
void VerifyMappings(BaseController controller);
string[] GetControlProperties(object control);
string[] GetModelProperties(BaseModel model);
}
Attaching the service can be done in several places, but doing so as soon as the component becomes �designable� is usual. This happens when the Initialize
method is called in a component designer, where we receive the component being designed. Therefore, we will add the following code to our ControllerDesigner
:
public override void Initialize(IComponent component)
{
...
SetupServices();
}
void SetupServices()
{
service = GetService(typeof(IControllerService));
if (service == null)
{
host.AddService(typeof(IControllerService),
new ControllerService(host), false);
}
}
We first check if the service has already been added by trying to retrieve it. Finally, we add the service passing the type and the service instance. We also have the possibility to pass a delegate that performs the actual object instantiation, in case we want to do that on-demand. This is useful if the service is costly to create. In our case, the service instance is created immediately and it receives a reference to the IDesignerHost
, so that we can access other IDE services from within our implementation:
internal class ControllerService : IControllerService
{
IDesignerHost _host;
public ControllerService(IDesignerHost host)
{
_host = host;
}
public string[] GetControlProperties(object control)
{
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;
}
}
The behavior of the GetControlProperties
method is exactly the same as the property browser itself. We use the BrowsableAttribute
to filter the list of property names to return. We do the same for the model properties, but with the BindableAttribute
, which is not strictly necessary, but allows us to provide a well-known approach to hiding properties. We could have used any attribute we wanted, but using the built-in ones makes for an easier understanding by our component users. For example, the BaseModel.ModelName
property shouldn�t be available for the controller component user, so we can apply the attribute and it will stop appearing wherever we show the available properties:
public class BaseModel : Component
{
false)>
public virtual string ModelName
In the PublisherModel
class, on the contrary, we add the attribute to the model properties that reflect the database field containing the publisher data:
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;
Note that at the same time, we apply the BrowsableAttribute
to hide them from the property browser, which is logical since these properties only make sense at run-time, and certainly not while we are designing the model component itself or the controller containing it.
Side-note: component properties serialization
We found that sometimes, using the DesignerSerializationVisibilityAttribute
causes the IDE to behave strangely. Specifically, we found that the UndoManager
, a type of IDesignerSerializationManager
that controls the undo/redo feature, loses its ability to serialize the component for undo operations, throwing from time to time an �Unknown Undo/Redo exception�. When this happens, whenever we type code in the source we lose all syntax coloring and Intellisense features.
There�s another way to tell the IDE that a property shouldn�t be serialized, through a method (it can be private) with the name ShouldSerializeXX
, which returns a boolean indicating the serialization support. The XX must be replaced with the property name. For the two properties above, we can add the following methods to achieve the same effect as applying the DesignerSerializationVisibilityAttribute
:
bool ShouldSerializeID() { return false; }
bool ShouldSerializeName() { return false; }
-- End of side-note ;)
Now we can get back to the controller service. To provide lists of values in the property browser, for the view mappings, we have to use a TypeConverter
. A class of this type provides a way to interact with the property browser, in several ways. The converter determines which conversions are supported (to and from other types), as well as what values are valid, also called standard values.
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);
}
In this implementation, we state that we support conversions to and from the string
type. We can provide custom conversions, just like the Width
property does for Web controls. You may have noticed that the property is of type Unit
, so that a value is composed by a numeric value and a UnitType
. But we can type directly in the property browser a value of �2px� for example, and it will be successfully converted to an object of type Unit
, with the numeric part and the appropriate UnitType
. This is done in its associated converter, which can split the string and create the unit object with the parts.
This base class, will be used to provide an exclusive list of values in the property browser (a dropdown). All the code so far is common to all converters providing lists of values, that�s why we use this base class. The methods that tell the property browser that we intend to provide lists of values and that they are exclusive are the following:
public override bool GetStandardValuesExclusive(
ITypeDescriptorContext context)
{
return true;
}
public override bool GetStandardValuesSupported(
ITypeDescriptorContext context)
{
return true;
}
}
Now, a converter that lists the control properties, simply inherits this class and provides the list of values:
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));
}
}
The ITypeDescriptorContext
parameter provides important information about the context in which the conversion is taking place. It provides access to the instance being changed (the ViewInfo
in this case), the current container (the DesignerHost
) and the property being changed as a PropertyDescriptor
. It also implements IServiceProvider
, so we can also ask for services just like we do from inside the designer or the component.
The list of values is provided by the controller service we added earlier to the design host and we have to return a StandardValuesCollection
object that can be constructed out of a string array with the values. Notice that for some (strange) reason, this class will not appear in Intellisense, even when it doesn�t have the EditorBrowsable
attribute applied.
Now we only have to associate the converter with the corresponding property in the ViewInfo
class and that�s it:
[TypeConverter(typeof(ControlPropertyConverter))]
public string ControlProperty
The effect of this association is immediately visible in the property browser:
But what if we want to check if the received ViewInfo.ControlID
actually points to an object in the containing component (Windows or Web Form)? It�s only natural to think we can simply pull the IReferenceService
out of the context (an IServiceProvider
) and use its GetReference
method. This won�t work, however, because edition is happening inside the property browser, and it�s the one answering for the GetService
requests, and unfortunately, it will return null
. There�s an easy workaround, though, which involves getting the IDesignerHost
service first, and asking for the desired service to it instead:
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));
}
}
The ModelPropertyConverter
has another issue. We need a way to know the controller the current ViewInfo
is linked to in order to get a reference to the model instance given its name. Without this reference, it�s impossible to reflect it and show its properties. To solve this, we added a Controller
internal
property to the ViewInfo
, which is set to point to the containing controller just before leaving the GetXxxViewMapping
extender property getter. We will get back to that in a moment, but let�s look at the code for the remaining converters first:
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));
}
}
They are associated with their corresponding ViewInfo
properties through the TypeConverter
as we did before.
So far, taking the property enumeration code out of the converters themselves and putting them in the IControllerService
doesn�t make much sense. After all, it�s the only place we use it so far. However, the next feature we will add, a central place to edit all mappings in the form, will make this design decision relevant.
Custom Editors
The property browser is usually enough for setting small amounts of property values. However, configuring a complex component can be really difficult through the simple property browser interface. Think about the way the Windows Forms font editor dialog simplifies selecting several font-related properties. Or the really useful Style Builder available for CSS files and HTML style attributes:
The good news is that we can have such an editor ourselves. It�s basically a custom Windows form we build just like any other form. Inside the form we can configure a component by setting several properties on behalf of the user, based on the form settings selected. We created the following form to edit all mappings applied to a controller:
It�s far from the style builder, but it�s what we need for our purposes. We just fill the listbox on the left with the ViewInfo
instances we find in the controller being configured, and fill the comboboxes with the relevant values, positioning the selected item to match that of the selected ViewInfo
. The question now is how to start editing with this form. One way is by attaching a custom type Editor to the property, in our case, the controller ConfiguredViews
:
[Editor(typeof(ViewMappingsEditor), typeof(UITypeEditor))]
public Hashtable ConfiguredViews
The second parameter of the EditorAttribute
will always be the type of the System.Drawing.Design.UITypeEditor
class, from which our custom editor must derive:
internal class ViewMappingsEditor : UITypeEditor
We have to override the GetEditStyle
method to tell the property browser which kind of UI we are going to show to the user:
public override UITypeEditorEditStyle GetEditStyle(
ITypeDescriptorContext context)
{
return UITypeEditorEditStyle.Modal;
}
In our case, we�re going to show a full-blown modal Windows form. The other possibility we have is UITypeEditorEditStyle.DropDown
, in which case we are able to show an in-place Windows Forms user control, just like the Windows Forms Anchor
and Dock
property editors do, and also the Web Forms editor for BackColor
and ForeColor
control properties.
With what we have so far, we will get the ellipsis drawn next to the controller ConfiguredViews
, but nothing will happen yet when clicked:
The property knows it has to show the ellipses because of the value we returned in the GetEditStyle
override. In order so show the form and begin editing, we must override the EditValue
method:
public override object EditValue(ITypeDescriptorContext context,
IServiceProvider provider, object value)
{
object retvalue = value;
try
{
IWindowsFormsEditorService srv = null;
IDesignerHost host = (IDesignerHost)
context.GetService(typeof(IDesignerHost));
if (provider != null)
srv = (IWindowsFormsEditorService)
context.GetService(typeof(IWindowsFormsEditorService));
Even though we can simply create the new form instance and call its ShowDialog
method, the proper way to do this inside the property browser is through the IWindowsFormsEditorService
, a service provided by the property browser itself.
if (srv != null && host != null)
{
BaseController controller = (BaseController) context.Instance;
ViewMappingsEditorForm form = new ViewMappingsEditorForm(
host, (BaseController) context.Instance);
We will see the form code in a minute, but note that we pass a reference to the designer host to it. We do so because the form, unlike the type converters we used so far, has no knowledge of the environment, and if it needs to ask for some service from the IDE, it doesn�t have a context whatsoever to ask it for. So it will keep a reference to the host we pass to the constructor for that purpose.
if (srv.ShowDialog(form) == DialogResult.OK)
{
context.PropertyDescriptor.SetValue(
context.Instance, form.ConfiguredMappings);
}
}
}
catch (Exception ex)
{
System.Windows.Forms.MessageBox.Show(
"An exception occurred during edition: " + ex.ToString());
}
return retvalue;
}
Note that after showing the form, we use the PropertyDescriptor
to set the new value on the controller. This notifies the IDE of the change and triggers the code generation process again. The form provides us with a property with the new settings the user chose through the form (ConfiguredMappings
).
We won�t show all the form code, as it�s mostly Windows Forms (boring) UI interaction code. But there are some important points we will highlight.
The first is that whenever we need to work with an instance of an object where we only have its ID, we should go through the IReferenceService
as we did before. Besides retrieving instances from the GetInstance
method, this service also allows us to avoid some �inconsistencies� in the way Windows and Web Forms name their controls. The former uses the control�s Name
property, while the latter uses the control�s ID
. We can avoid hard coding this difference by using that service�s GetName
method. We do that in the InitControls
method where we load the available controls in the appropriate dropdown:
void InitControls(object state)
{
...
cbControl.Items.Add(
new ControlEntry(control, _reference.GetName(control)));
The ControlEntry
is a helper struct
that simply provides a custom ToString
override that gets displayed in the dropdown. This way we centralize the padding and make it easier to locate an item later:
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 + "]";
}
}
Another important issue is related to threading. If form initialization is costly, you may be tempted to span a new thread to perform it. We could use the following code to invoke the InitControls
method in another thread:
ThreadPool.QueueUserWorkItem(new WaitCallback(InitControls));
However, if your initialization code has to ask for a service through the host, this will not work. An exception will be thrown. That�s because services are expected to be pulled from the main application (VS.NET) thread. If you don�t need to retrieve services, or if you did so from inside the Load
event handler, for example, then there�s no problem.
Depending on the concrete application and service our components provide, we may use qualified type names that we use to instantiate the concrete objects. For example, your application may allow the user to define through a dropdown the type that will handle a certain request or application feature. You may even get the list from a database. The usual way to load a type and reflect it, to show its members in a form for example, is through Type.GetType
:
Type t = Type.GetType("Mvc.Components.Controller.ViewInfo, Mvc.Components");
This will only work at design-time for assemblies in the GAC. That�s because the IDE is not running from the place where the project is stored. So, there�s no way for the process running the IDE to locate the assemblies, even if they are referenced by the project. There is another service that provides us with the type loading feature, the ITypeResolutionService
. This service provides a GetType
method that will correctly locate an assembly by taking into account the assemblies referenced in the current project.
In this editor, we can take advantage of the IControllerService
to fill the dropdown with values:
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));
...
Here we can immediately appreciate the benefit of having globally available services to avoid code duplication mainly with type converters:
Custom and Global Commands
The pending issue is how to provide an easy way to check all configured mappings in a single step. The IControllerService
has a method, VerifyMappings
for that purpose, and its implementation is as follows:
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);
}
The VerifyOne
checks the references to model and control values in each ViewInfo
in the controller:
string VerifyOne(BaseController controller)
{
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();
}
Now we need a way to execute this command. We can add items to the context menu for our controller by overriding the Verbs
property of the ControllerDesigner
:
public override DesignerVerbCollection Verbs
{
get
{
DesignerVerb[] verbs = new DesignerVerb[] {
new DesignerVerb("Verify this controller mappings ...",
new EventHandler(OnVerifyOne)) };
return new DesignerVerbCollection(verbs); }
}
When the menu item is selected, our handler will be called:
void OnVerifyMappings(object sender, EventArgs e)
{
IControllerService svc = (IControllerService)
GetService(typeof(IControllerService));
svc.VerifyMappings(CurrentController);
}
Note how the code becomes extremely simple when the common features are moved to first-class IDE services. With the verb in place, we will see the modified context menu:
Another usual feature is to add to this context menu the most used editors for the component. For example, we could add an Edit mappings item to the menu. This may seem like a no-brainer, but there are subtleties too. The first step is to add another verb, that�s the easy part:
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); }
}
Recall that we used a type Editor
to implement the ConfiguredViews
property editing. What would be great is to be able to call directly the ViewMappingsEditor.EditValue
method:
public override object EditValue(
ITypeDescriptorContext context, IServiceProvider provider, object value)
But looking at the parameters we have to pass to it, where are we supposed to get an ITypeDescriptorContext
from? Well, we can implement our own, the interface isn�t so complicated after all:
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];
}
}
It�s basically a placeholder for properties and passes any request of GetService
to the designer host. We can now go and try calling the editor directly:
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);
Unfortunately, this will not work. The failing point will be the moment we retrieve the IWindowsFormsEditorService
inside the EditValue
:
srv = (IWindowsFormsEditorService)
context.GetService(typeof(IWindowsFormsEditorService));
Note that we are asking for the service to the host directly (because of our implementation of ITypeDescriptorContext
). Anyway, the srv
variable will always be null
. Why? The answer is that this service is not provided by the IDE itself, but by the property grid, more precisely, the internal class System.Windows.Forms.PropertyGridInternal.PropertyGridView
, which responds to requests for this service by returning itself (because it implements the service interface). Looking at the IL code for the ShowDialog
implementation in this class reveals some complexity in the process, which involves calculating the dialog position, setting focus and passing the call to another service, IUIService
. That�s why we didn�t directly instantiate a form and called ShowDialog
in the first place in the editor!
We won�t duplicate all this code, so we will instantiate the form directly instead, as there�s no restriction to doing so from within a designer verb:
void OnEditMappings(object sender, EventArgs e)
{
try
{
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());
}
}
The code is almost the same that is used in the editor itself, except for the call to the IWindowsFormsEditorService
.
If the form developer needs to use several components at once, checking the settings one by one can be tedious. We can use another IDE service to add so-called global commands. It�s the IMenuCommandService
. As checking the mappings is a task being performed in the controller service, it seems logical to put this new behavior inside it. Recall that our service is instantiated by the controller designer in the Initialize
method call. In the service constructor, we can hook our global command:
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)));
}
}
We can only add designer verbs, which inherit from the MenuCommand
class received by the AddCommand
method. This new context menu item is shown always every time the user clicks anywhere in the components area of the form:
Note that the controller is not selected. We clicked next to it, in the components surface. The handler now iterates all components checking one by one in turn:
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);
}
It simply passes the ball to the VerifyOne
method for each controller found. The IDesignerHost.Container.Components
property contains all the references that currently exist.
If only it were that easy� Unfortunately, if we now right-click on the controller, we will sadly see that the global command has replaced the first controller designer verb:
The Verify this controller � item has disappeared! This strange behavior happens because the IDE first asks for the component verbs, and then (rather inexplicably) puts all the global verbs on top. The only workaround I found is adding a sort of �placeholder� verb in the controller designer:
public override DesignerVerbCollection Verbs
{
get
{
DesignerVerb[] verbs = new DesignerVerb[] {
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); }
}
All the designer verbs are displayed at the bottom of the property browser too, allowing easy access to them. But unfortunately, the new empty verb we added appears there! Note the first colon at the beginning :(
For now, we either have to live with that, or drop global commands.
Now we have all the IDE integration plumbing in place, we get to the point where we have to actually reflect the model data in the mapped UI widgets. As we stated above when analyzing the framework architecture, the controller delegates the responsibility of setting/getting values in the UI to adapter classes which know how to handle Web and Windows forms.
Dealing with different View technologies
We know that we will support Windows and Web Forms. We will do so by defining the common members we will need from both technologies in a common base abstract class, and let each �adapter� concrete implementation to perform the necessary actions for the particular technology.
There are fundamental differences in the way Web and Windows Forms access and set values in their child controls. We already mentioned naming differences also. These details will be isolated in a new service we will provide, the IAdapterService
. Its interface contains the basic method we need:
public interface IAdapterService
{
object FindControl(string controlId);
string GetControlID(object control);
object[] GetControls();
ComponentCollection GetComponents();
void RefreshView(BaseController controller);
void RefreshModels(BaseController controller);
}
Two classes implement this interface, WebFormsAdapterService
and WindowsFormsAdapterService
. Both are pretty similar, so we will look at the former. Both adapters receive in the constructor the parameters they will use to resolve the other methods:
internal class WebFormsAdapterService : IAdapterService
{
Page _container;
IContainer _components;
internal WebFormsAdapterService(object controlsContainer,
IContainer componentsContainer)
{
_container = (Page) controlsContainer;
_components = componentsContainer;
}
The Windows Forms version is similar, but casts the controlsContainer
parameter to a Form
. The first four methods are pretty simple. You can look at the downloaded code for their implementation. The interesting bits are the RefreshView
and RefreshModels
methods. The former causes an iteration on all the configured mappings in the received controller, where the appropriate control properties are set to the values found in the model. The opposite happens in the latter method.
public void RefreshView(BaseController controller)
{
Hashtable models = new Hashtable(controller.Components.Count);
foreach (BaseModel model in controller.Components)
models.Add(model.ModelName, model);
foreach (DictionaryEntry entry in controller.ConfiguredViews)
{
ViewInfo info = (ViewInfo) entry.Value;
object model = models[info.Model];
PropertyInfo modelprop = model.GetType().GetProperty(info.ModelProperty);
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]);
}
}
}
}
We simply use reflection and load types and properties and set values. The reverse process (RefreshModels
) is identical but instead of calling SetValue
on the control property we do so in the model property.
We already know to hook a new service into the architecture, so let�s have a brief look at the added code in the ControllerDesigner.SetupServices
method that performs the task:
void SetupServices()
{
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);
}
}
service = GetService(typeof(IControllerService));
if (service == null)
{
host.AddService(typeof(IControllerService),
new ControllerService(host), false);
}
}
You may have noticed that we pass false
as the last parameter when we add any service to the host. That parameter specifies whether we want to promote the service to higher layers in the IDE architecture. It�s not advisable to do so, unless you�re absolutely sure that the service is unique and never needs to be changed dynamically. In our case, we are switching the service object depending on the root component type, so we need it to remain in the IDesignerHost
and be easily replaced. By passing false
to the third parameter, we indicate that the service only lasts for the life of the current root designer.
We can now take advantage of this service when we access the mappings in the controller extender properties (we removed attributes for better readability):
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);
}
Now both properties (Win and Web) can share a common implementation, because the adapter service resolves the inconsistencies:
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;
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;
}
Finally, the base controller exposes internal methods to cause synchronization between the view and the model:
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);
}
Note that these methods are really simple now because the adapter is taking care of the interaction with the view. This is slightly different than the model proposed by the original MVC pattern, where each view technology would have its own controller.
Before we move on with the view synchronization, however, we need to know how the view asks the controller to load a certain model.
Model Behavior, the MVC way
As the view is not allowed to execute actions directly on the model, isolation is preserved. As a consequence, the controller exposes methods that cause the actual model method execution. The model contains behavior as well as data, and it uses that data either to hold results from method executions or as input for some actions.
A simple PublisherModel
component may expose, besides the properties that map to the publishers table fields in the sample Pubs database, the three basic operations on an entity: Load
, Save
and Delete
:
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();
}
}
Note that the method takes the publisher ID to load from its own property. Likewise, whatever it loads is put back into the model properties. The other two method implementations behave similarly.
Like we said, in order to access these model behavior, it must be exposed through the controller:
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);
}
Basically, they propagate the method call to the contained model and finally raises an event the view can use to update its own control values.
Both in a Windows Forms button click, as well as on a Web Forms button click, we can put the following code to cause actions in the model:
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();
}
Note that there is no �platform� specific code. Not a single line. But how and when does the controller update the UI? There are fundamental differences in this timing in the Web and Windows environments. In the former, the UI is loaded typically on the Load
event, as post-backs cause new Load
events to be fired. A Windows Form, however, needs to refresh the view at times other than the Load
, because further actions don�t cause new Load
events. To provide this connectivity between the containing form, its events and the controller actions (namely, RefreshView
and RefreshModels
), we use connectors.
Connecting the Views
The responsibility of the connector is to call the controller methods to synchronize the models with the view at the appropriate time. Basically, the UI container (a Web or a Windows Form) should instantiate the appropriate connector and call Connect
passing the relevant parameter to perform the wiring. We provide a base class as usual from which both connectors will inherit:
public abstract class BaseConnector
{
public abstract void Connect(BaseController controller,
object controlsContainer, IContainer componentsContainer);
}
In order to make the process automatic, we will emit the concrete connection code from inside the controller code generation, after detecting the hosting technology. This is the relevant code from the ControllerCodeDomSerializer
method:
public override object
Serialize(IDesignerSerializationManager manager, object value)
{
...
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);
}
A controller placed in a Web Form generates the following code:
private void InitializeComponent()
{
...
this.publisherController.ConnectionString =
((string)(configurationAppSettings.GetValue("Pubs", typeof(string))));
this.publisherController.ConfiguredViews.Add("txtID",
new Mvc.Components.Controller.ViewInfo("txtID",
"Text", "Publisher", "ID"));
new Mvc.Components.Connector.WebFormsConnector().Connect(
this.publisherController, this, this.components);
...
}
The same component placed in a Windows Form generates:
private void InitializeComponent()
{
...
this.publisherController.ConnectionString =
((string)(configurationAppSettings.GetValue("Pubs", typeof(string))));
this.publisherController.ConfiguredViews.Add("txtID",
new Mvc.Components.Controller.ViewInfo("txtID",
"Text", "Publisher", "ID"));
new Mvc.Components.Connector. WindowsFormsConnector().Connect(
this.publisherController, this, this.components);
...
}
Note that the only difference is the concrete connector being instantiated. An important thing to notice here is that this method will never be called at design-time. The IDE only loads properties on components, but it will not call methods.
The Web Forms connector is fairly simple:
public class WebFormsConnector : BaseConnector
{
public override void Connect(BaseController controller,
object controlsContainer, IContainer componentsContainer)
{
Page page = (Page) controlsContainer;
page.Init += new EventHandler(controller.Init);
page.Load += new EventHandler(controller.RefreshModels);
page.PreRender += new EventHandler(controller.RefreshView);
}
}
Note that this will hook the controller to the page events, causing them to be called at the appropriate times.
Let�s wrap up the interactions that will happen at the Load
event:
The critical point here is the Site.GetService()
method call, which must give the controller an instance of the appropriate adapter to use. We have already tested that the service is right there when attached at design time. But the designer is only called at design-time, so what happens at run-time?
Back at the beginning, when we introduced this .NET Component-oriented architecture, we said there were differences between UI controls and non-visual components. We are now in a position to explain what this difference is. At design-time, both kinds of components are sited in the DesignSite
and contained in the DesignHost
, as shown back then. But at run-time, components get passed the this.components
field as the container and they become sited on this container, simply a class of type Container
. The site, accordingly, is of type Container.Site
. The problem is that the GetService
method, implemented internally in the Container
class itself, only returns services of type IContainer
, which it itself provides. All other services are gone. Worse, visual controls don�t have a Site
at all, and that�s the difference.
What we can do is perform some run-time initialization and provide a custom site that can answer requests for our IAdapterService
. Remember that the service instance must always be kept somewhere, just as it�s kept by VS.NET at design-time. We will create a RuntimeSite
class implementing ISite
that we can use to �site� the components with our own infrastructure:
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 { }
}
public object GetService(Type serviceType)
{
return _callback(this, new GetServiceEventArgs(serviceType));
} GetServiceEventHandler _callback;
}
Our constructor, besides receiving the current container and component, receives a delegate. We said that the service instance must be kept somewhere during runtime. This callback is provided so that when a component asks for a service, the call is passed to this delegate, which can be implemented as it fits in the technology in use. For example, the web version can use the HttpContext
to keep the object, something a Windows Forms version can�t do. The delegate gives both a chance to answer the GetService
request any way they want. The delegate class and its arguments are:
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;
}
Let�s have a look at the complete web connector constructor, which initializes the run-time site also:
public override void Connect(BaseController controller,
object controlsContainer, IContainer componentsContainer)
{
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];
}
controller.Site = new RuntimeSite(componentsContainer, controller,
new GetServiceEventHandler(service.GetServiceHandler));
foreach (IComponent component in controller.Components)
{
component.Site = new RuntimeSite(controller, component,
new GetServiceEventHandler(service.GetServiceHandler));
}
Page page = (Page) controlsContainer;
page.Init += new EventHandler(controller.Init);
page.Load += new EventHandler(controller.RefreshModels);
page.PreRender += new EventHandler(controller.RefreshView);
}
}
The WebFormsAdapterService.GetServiceHandler
method is in charge of responding to requests from components now:
internal object GetServiceHandler(object sender, GetServiceEventArgs e)
{
if (e.ServiceType == typeof(IAdapterService))
{
return this;
}
else
{
return null;
}
}
Simply enough, the web version of the connector first stores the service in the HttpContext
, and later lets the service itself answer to components asking for the service. Now the controller, in its RefreshModels
method to be called at Page.Load
time, will successfully retrieve the service:
public class BaseController : Component, IContainer, IExtenderProvider
{
internal void RefreshModels(object sender, EventArgs e)
{
IAdapterService svc = (IAdapterService)
this.Site.GetService(typeof(IAdapterService));
svc.RefreshModels(this);
}
The web version of the framework is now complete. When the page is loaded, the model is updated with whatever values existing on the form as posted (or initially loaded) by the user. Just before rendering to the client, and generally after all the event handlers for button clicks, textbox changes, etc. have been called, the controller refreshes the view with what has ended in the model. A Web Form showing publisher data and with the correct mappings, can provide load/save/delete features with the single line of code that we saw in each button handler:
private void btnLoad_Click(object sender, System.EventArgs e)
{
controller.LoadPublisher();
}
Now, suppose we enter an ID
in the respective field and click Load
, this is the sequence of actions until the page comes back to us:
- On
Load
, the adapter service is hooked and components are sited.
- Model is refreshed, so the value in
txtID
is placed in the PublisherModel.ID
property.
- The event handler calls the controller method,
LoadPublisher()
.
- The model goes to the database and loads itself with the returned row.
- Before rendering, control values are updated with the new values in the model (the complete publisher data).
- The page is rendered with the values and sent to the browser.
And the page designer only had to configure the mappings using our IDE integration features, and call the appropriate controller method when it was appropriate!
What�s more, if later we decide to move (or at the same time) to a Windows Forms client, the same controller and mappings will do the work. And even the event handlers for the UI events will look the same!
As Windows Forms allows statefull applications, we can simply keep the relevant variables inside the connector itself. However, in the web, at initialization time, we know all controls have been created. This is not the case for Windows Forms, where controls are just class-level variables that must be initialized in the InitializeComponent
method just like our controller. Therefore, there�s no guarantee that our connector will be called after the controls have been initialized.
And why do we need to access the controls at initialization time? Because unlike the web where we have a single point of model refreshes, the Load
event, in Windows Forms, we have to refresh the models as soon as a control is modified, and we have to attach to that event and trigger the model refresh.
So we have to resort to some Form event to perform the actual hook:
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;
controller.Site = new RuntimeSite(componentsContainer, controller,
new GetServiceEventHandler(GetServiceHandler));
foreach (IComponent component in controller.Components)
{
component.Site = new RuntimeSite(controller, component,
new GetServiceEventHandler(GetServiceHandler));
}
Form form = (Form) controlsContainer;
form.Activated += new EventHandler(OnActivated);
form.Activated += new EventHandler(controller.Init);
form.Load += new EventHandler(controller.RefreshView);
form.Deactivate += new EventHandler(controller.RefreshModels);
}
As you can see, each connector (one for each controller) will have the service and the corresponding connector. The OnActivated
handler takes care of hooking the RefreshModels
method to each control Leave
event:
void OnActivated(object sender, EventArgs e)
{
foreach (DictionaryEntry entry in _controller.ConfiguredViews)
{
Control ctl = (Control)
_service.FindControl(((ViewInfo)entry.Value).ControlID);
ctl.Leave += new EventHandler(_controller.RefreshModels);
}
_controller.RefreshView(sender, e);
_controller.ModelChanged += new EventHandler(_controller.RefreshView);
}
The other trick we did at the end is hook the ModelChanged
event fired from the controller to the same controller RefreshView
method. This way we achieve automatic UI updates without writing a single line of code.
Finally, the delegate that handles GetService
requests (and which we passed to the new RuntimeSite
for each component), simply returns the reference it keeps to the service:
object GetServiceHandler(object sender, GetServiceEventArgs e)
{
if (e.ServiceType == typeof(IAdapterService))
{
return _service;
}
else
{
return null;
}
}
}
The exact same code (except for the connector instance) can now be present in both a Windows and a Web Forms application that will behave exactly the same, even at the configuration level.
Persisting at design-time through code certainly has an edge over run-time detection and emission: it�s compiled and will always be faster. For example, we may want to emit some attributes in the rendered Web Forms controls that we could use in client-side JavaScript to know the details of the mappings. Again, there�s a natural way that seems obvious: attach to PreRender
and iterate configured mappings again, adding the relevant attributes to the control.
If the mappings and the controls are known at design-time, why do we have to waste precious run-time processing doing this iteration over and over on each page interaction? The answer lies in a much better way that we can take advantage of. It�s called an IDesignerSerializationProvider
.
An object implementing this interface can be registered with the IDesignerSerializationManager
(which we receive in our controller serializer).
internal class ControllerCodeDomSerializer : BaseCodeDomSerializer
{
public override object
Serialize(IDesignerSerializationManager manager, object value)
{
...
if (!(manager.GetSerializer(typeof(System.Web.UI.Control),
typeof(CodeDomSerializer)) is WebControlSerializer))
{
manager.AddSerializationProvider(
new WebControlSerializationProvider());
}
We first check that the provider hasn�t been added already. At serialization time, the manager will call each configured provider and give it a chance to provide a custom serializer for a type. This is done in the GetSerializer
method:
internal class WebControlSerializationProvider :
IDesignerSerializationProvider
{
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;
}
}
If we find a control we can attach attributes to (implementing IAttributeAccessor
), we return our new serializer. From now on, every time the IDE needs to serialize a Web control (either HtmlControl
or WebControl
), our serializer will be called, receiving the control being serialized.
internal class WebControlSerializer : BaseCodeDomSerializer
{
public override object Serialize(
IDesignerSerializationManager manager, object value)
{
...
}
In order to avoid disturbing the normal serialization process, we will retrieve the regular serializer for the received control first, and get the code statements from it. We defined a helper method in the base serializer to get the appropriate type:
protected CodeDomSerializer GetConfiguredSerializer(
IDesignerSerializationManager manager, object value)
{
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);
}
Here we see in action the ITypeResolutionService
. After we retrieve the relevant attribute, all we have is the qualified type name, and we must load it through that service to guarantee the assembly it lives in is located and successfully loaded. After that, we simply use the Activator.CreateInstance
method to create the serializer and return it.
Let�s go back to the WebControlSerializationProvider.Serialize
method:
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);
When a property is extended, it will appear as a property of the object itself if we use the TypeDescriptor
:
PropertyDescriptor prop =
TypeDescriptor.GetProperties(value)["WebViewMapping"];
ViewInfo info = (ViewInfo) prop.GetValue(value);
Now we need to generate the following code to add an attribute to store the controller name, for example:
((IAttributeAccessor)control).SetAttribute("MVC_Controller", "controller");
We will do so for each of the four ViewInfo
properties also:
if (info.ControlProperty != String.Empty &&
info.Model != String.Empty && info.ModelProperty != String.Empty)
{
CodeExpression ctlref = SerializeToReferenceExpression(
manager, value);
CodeCastExpression cast =
new CodeCastExpression(typeof(IAttributeAccessor), ctlref);
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)
}));
...
return statements;
}
}
We defined the expressions we use repeatedly, such as the cast to IAttributeAccessor
and the control reference.
Now we can cause a code generation process on the page, by changing a view mapping and later changing a Web control property, for example. The newly serialized code looks like the following:
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");
...
Now the rendered page has the attributes without having to add them dynamically at run-time:
<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>
...
As a side node, we have to point that the ViewInfo
object contains a Controller
property that points to the controller that created it. That�s how we emit its name to the page. This is a specific implementation detail of ours, but what if that property didn�t exist? How would we retrieve the real property provider backing up a �fake� property on a control?
The answer is not easy, unfortunately. When we ask the TypeDescriptor
for the property, we expect a PropertyDescriptor
. However, when the property is extended, in a class derived from it, ExtendedPropertyDescriptor
is returned instead. This object has a property, Provider
that has a reference to the object that is providing this property. Great! What else could we ask for? Well, we could ask for the class not to be private!
So, now that we�re out of luck, we have to find a workaround, as usual. The trick comes from reflection. Beware that the technique we�re about to use is highly risky. If MS decides to change the class name or the property name, or even remove it, your now-working code will stop doing so. And they wouldn�t be doing anything wrong, as the interface was private. Now that you are aware, let�s get to it!
To achieve this, we have to first load the type. Through the Assembly.GetType
method we can�t reach a private
type. We have to use GetTypes
and iterate all of them and find the matching one. We created a utility class, DesignUtils
with a LoadPrivateType
that does the trick. Its usage is simple:
Type t = DesignUtils.LoadPrivateType(
"System.ComponentModel.ExtendedPropertyDescriptor,System");
Note that we avoid having to pass the fully qualified assembly name, the method code is as follows:
internal static Type LoadPrivateType(string className)
{
string[] names = className.Split(',');
if (names.Length < 2)
throw new ReflectionTypeLoadException(null, null,
"Invalid class name: " + className);
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.");
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;
}
Now that we have the private reflected type, we can get the property as usual with:
PropertyInfo provider = t.GetProperty("Provider");
Finally, we can get the actual provider instance by using the property info GetValue
method:
object instance = provider.GetValue(prop, new object[0]);
We have encapsulated this behavior in a static field in DesignUtils
, so that this process happens only once. From inside the Web control custom serializer, we could get a reference to the controller with the following code:
PropertyDescriptor prop = TypeDescriptor.GetProperties(value)["WebViewMapping"];
ViewInfo info = (ViewInfo) prop.GetValue(value);
BaseController controller = (BaseController)
DesignUtils.ProviderProperty.GetValue(prop, new object[0]);
This prototype implementation of an MVC-like framework is by no means complete or production-quality. However, we have explored almost every aspect of the IDE design-time architecture, and we have even extended the concept to the run-time, taking advantage of the intrinsic component-oriented features of the whole of the .NET framework. So, even when the Web Forms implementation is particularly weak (because we are causing a full view and model refresh on every change), it does prove the concept. More granularity, as well as a richer event model for the controller would greatly benefit the architecture.
Deep integration with VS.NET can dramatically increase developer productivity, and by using custom code generation techniques we can achieve quite complex component persistence, and even enforce certain architectural decisions, such as the ones we made for the model accesses.
For those researching other areas of component persistence, you should know that components can also be stored in resource files. Even more, we can attach our own serialization provider, as we did for Web Forms controls, to serialize to other mediums, such as XML, a database or whatever.
It�s also possible to create a custom root designer, with custom drawing, elements in the toolbox, etc. Just like the DataSet or XmlSchema designers. Look at the article here for a good example of that.
Tip 1: debugging these designers is hard. We have to either start a web client (to work from within it) or start a Windows one. Trying to debug both is very difficult, as the process seems to be loaded twice, so you effectively stop debugging.
Tip 2: beware that using an ExpandableObjectConverter
may cause the child component (accessible in a converter through the ITypeDescriptorContext.Instance
property) to cease being sited, and we may have trouble retrieving services.
Tip 3: if you use a DropDown type of editor, if you open new dialog forms, the property browser immediately loses control of the focus and the editor no longer controls the opened form.
Tip 4: it�s tempting to use a converter IsValid
method to determine validity of a certain value when we provide lists of standard values. The validation is not automatic, however. We have to override this method and check the received value. But again, this method override may need a TypeDescriptorContext
. As there is no need to use the IWindowsFormsEditorService
, we can safely pass our custom implementation of the context here.
During this article we explored the most advanced features available in .NET and VS.NET for component-based development. We offered deep integration with the IDE, and even expanded the model to the run-time.
We have discussed the MVC design pattern, and created a concrete implementation that can make application development substantially faster. Not only that, we were able to create an implementation that can work with the same code base for both Windows and Web Forms.
- Download and unzip.
- Create a virtual folder in IIS pointing to the WebComponents folder, with the same name.
- Open the solution, and run the Windows and Web apps.
- The code assumes you have SQL installed locally, with the sample Pubs database. The files WebComponents\Web.config and WinComponents\App.config contain a connection string pointing to it. You must ensure the user and password in those files is valid for your installed database.
DE>GetName method. We do that in the
InitControls
method where we load the available controls in the appropriate dropdown:
void InitControls(object state)
{
...
cbControl.Items.Add(
new ControlEntry(control, _reference.GetName(control)));
The ControlEntry
is a helper struct
that simply provides a custom ToString
override that gets displayed in the dropdown. This way we centralize the padding and make it easier to locate an item later:
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 + "]";
}
}
Another important issue is related to threading. If form initialization is costly, you may be tempted to span a new thread to perform it. We could use the following code to invoke the InitControls
method in another thread:
ThreadPool.QueueUserWorkItem(new WaitCallback(InitControls));
However, if your initialization code has to ask for a service through the host, this will not work. An exception will be thrown. That�s because services are expected to be pulled from the main application (VS.NET) thread. If you don�t need to retrieve services, or if you did so from inside the Load
event handler, for example, then there�s no problem.
Depending on the concrete application and service our components provide, we may use qualified type names that we use to instantiate the concrete objects. For example, your application may allow the user to define through a dropdown the type that will handle a certain request or application feature. You may even get the list from a database. The usual way to load a type and reflect it, to show its members in a form for example, is through Type.GetType
:
Type t = Type.GetType("Mvc.Components.Controller.ViewInfo, Mvc.Components");
This will only work at design-time for assemblies in the GAC. That�s because the IDE is not running from the place where the project is stored. So, there�s no way for the process running the IDE to locate the assemblies, even if they are referenced by the project. There is another service that provides us with the type loading feature, the ITypeResolutionService
. This service provides a GetType
method that will correctly locate an assembly by taking into account the assemblies referenced in the current project.
In this editor, we can take advantage of the IControllerService
to fill the dropdown with values:
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));
...
Here we can immediately appreciate the benefit of having globally available services to avoid code duplication mainly with type converters:
Custom and Global Commands
The pending issue is how to provide an easy way to check all configured mappings in a single step. The IControllerService
has a method, VerifyMappings
for that purpose, and its implementation is as follows:
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);
}
The VerifyOne
checks the references to model and control values in each ViewInfo
in the controller:
string VerifyOne(BaseController controller)
{
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();
}
Now we need a way to execute this command. We can add items to the context menu for our controller by overriding the Verbs
property of the ControllerDesigner
:
public override DesignerVerbCollection Verbs
{
get
{
DesignerVerb[] verbs = new DesignerVerb[] {
new DesignerVerb("Verify this controller mappings ...",
new EventHandler(OnVerifyOne)) };
return new DesignerVerbCollection(verbs); }
}
When the menu item is selected, our handler will be called:
void OnVerifyMappings(object sender, EventArgs e)
{
IControllerService svc = (IControllerService)
GetService(typeof(IControllerService));
svc.VerifyMappings(CurrentController);
}
Note how the code becomes extremely simple when the common features are moved to first-class IDE services. With the verb in place, we will see the modified context menu:
Another usual feature is to add to this context menu the most used editors for the component. For example, we could add an Edit mappings item to the menu. This may seem like a no-brainer, but there are subtleties too. The first step is to add another verb, that�s the easy part:
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); }
}
Recall that we used a type Editor
to implement the ConfiguredViews
property editing. What would be great is to be able to call directly the ViewMappingsEditor.EditValue
method:
public override object EditValue(
ITypeDescriptorContext context, IServiceProvider provider, object value)
But looking at the parameters we have to pass to it, where are we supposed to get an ITypeDescriptorContext
from? Well, we can implement our own, the interface isn�t so complicated after all:
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];
}
}
It�s basically a placeholder for properties and passes any request of GetService
to the designer host. We can now go and try calling the editor directly:
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);
Unfortunately, this will not work. The failing point will be the moment we retrieve the IWindowsFormsEditorService
inside the EditValue
:
srv = (IWindowsFormsEditorService)
context.GetService(typeof(IWindowsFormsEditorService));
Note that we are asking for the service to the host directly (because of our implementation of ITypeDescriptorContext
). Anyway, the srv
variable will always be null
. Why? The answer is that this service is not provided by the IDE itself, but by the property grid, more precisely, the internal class System.Windows.Forms.PropertyGridInternal.PropertyGridView
, which responds to requests for this service by returning itself (because it implements the service interface). Looking at the IL code for the ShowDialog
implementation in this class reveals some complexity in the process, which involves calculating the dialog position, setting focus and passing the call to another service, IUIService
. That�s why we didn�t directly instantiate a form and called ShowDialog
in the first place in the editor!
We won�t duplicate all this code, so we will instantiate the form directly instead, as there�s no restriction to doing so from within a designer verb:
void OnEditMappings(object sender, EventArgs e)
{
try
{
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());
}
}
The code is almost the same that is used in the editor itself, except for the call to the IWindowsFormsEditorService
.
If the form developer needs to use several components at once, checking the settings one by one can be tedious. We can use another IDE service to add so-called global commands. It�s the IMenuCommandService
. As checking the mappings is a task being performed in the controller service, it seems logical to put this new behavior inside it. Recall that our service is instantiated by the controller designer in the Initialize
method call. In the service constructor, we can hook our global command:
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)));
}
}
We can only add designer verbs, which inherit from the MenuCommand
class received by the AddCommand
method. This new context menu item is shown always every time the user clicks anywhere in the components area of the form:
Note that the controller is not selected. We clicked next to it, in the components surface. The handler now iterates all components checking one by one in turn:
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);
}
It simply passes the ball to the VerifyOne
method for each controller found. The IDesignerHost.Container.Components
property contains all the references that currently exist.
If only it were that easy� Unfortunately, if we now right-click on the controller, we will sadly see that the global command has replaced the first controller designer verb:
The Verify this controller � item has disappeared! This strange behavior happens because the IDE first asks for the component verbs, and then (rather inexplicably) puts all the global verbs on top. The only workaround I found is adding a sort of �placeholder� verb in the controller designer:
public override DesignerVerbCollection Verbs
{
get
{
DesignerVerb[] verbs = new DesignerVerb[] {
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); }
}
All the designer verbs are displayed at the bottom of the property browser too, allowing easy access to them. But unfortunately, the new empty verb we added appears there! Note the first colon at the beginning :(
For now, we either have to live with that, or drop global commands.
Now we have all the IDE integration plumbing in place, we get to the point where we have to actually reflect the model data in the mapped UI widgets. As we stated above when analyzing the framework architecture, the controller delegates the responsibility of setting/getting values in the UI to adapter classes which know how to handle Web and Windows forms.
Dealing with different View technologies
We know that we will support Windows and Web Forms. We will do so by defining the common members we will need from both technologies in a common base abstract class, and let each �adapter� concrete implementation to perform the necessary actions for the particular technology.
There are fundamental differences in the way Web and Windows Forms access and set values in their child controls. We already mentioned naming differences also. These details will be isolated in a new service we will provide, the IAdapterService
. Its interface contains the basic method we need:
public interface IAdapterService
{
object FindControl(string controlId);
string GetControlID(object control);
object[] GetControls();
ComponentCollection GetComponents();
void RefreshView(BaseController controller);
void RefreshModels(BaseController controller);
}
Two classes implement this interface, WebFormsAdapterService
and WindowsFormsAdapterService
. Both are pretty similar, so we will look at the former. Both adapters receive in the constructor the parameters they will use to resolve the other methods:
internal class WebFormsAdapterService : IAdapterService
{
Page _container;
IContainer _components;
internal WebFormsAdapterService(object controlsContainer,
IContainer componentsContainer)
{
_container = (Page) controlsContainer;
_components = componentsContainer;
}
The Windows Forms version is similar, but casts the controlsContainer
parameter to a Form
. The first four methods are pretty simple. You can look at the downloaded code for their implementation. The interesting bits are the RefreshView
and RefreshModels
methods. The former causes an iteration on all the configured mappings in the received controller, where the appropriate control properties are set to the values found in the model. The opposite happens in the latter method.
public void RefreshView(BaseController controller)
{
Hashtable models = new Hashtable(controller.Components.Count);
foreach (BaseModel model in controller.Components)
models.Add(model.ModelName, model);
foreach (DictionaryEntry entry in controller.ConfiguredViews)
{
ViewInfo info = (ViewInfo) entry.Value;
object model = models[info.Model];
PropertyInfo modelprop = model.GetType().GetProperty(info.ModelProperty);
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]);
}
}
}
}
We simply use reflection and load types and properties and set values. The reverse process (RefreshModels
) is identical but instead of calling SetValue
on the control property we do so in the model property.
We already know to hook a new service into the architecture, so let�s have a brief look at the added code in the ControllerDesigner.SetupServices
method that performs the task:
void SetupServices()
{
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);
}
}
service = GetService(typeof(IControllerService));
if (service == null)
{
host.AddService(typeof(IControllerService),
new ControllerService(host), false);
}
}
You may have noticed that we pass false
as the last parameter when we add any service to the host. That parameter specifies whether we want to promote the service to higher layers in the IDE architecture. It�s not advisable to do so, unless you�re absolutely sure that the service is unique and never needs to be changed dynamically. In our case, we are switching the service object depending on the root component type, so we need it to remain in the IDesignerHost
and be easily replaced. By passing false
to the third parameter, we indicate that the service only lasts for the life of the current root designer.
We can now take advantage of this service when we access the mappings in the controller extender properties (we removed attributes for better readability):
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);
}
Now both properties (Win and Web) can share a common implementation, because the adapter service resolves the inconsistencies:
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;
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;
}
Finally, the base controller exposes internal methods to cause synchronization between the view and the model:
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);
}
Note that these methods are really simple now because the adapter is taking care of the interaction with the view. This is slightly different than the model proposed by the original MVC pattern, where each view technology would have its own controller.
Before we move on with the view synchronization, however, we need to know how the view asks the controller to load a certain model.
Model Behavior, the MVC way
As the view is not allowed to execute actions directly on the model, isolation is preserved. As a consequence, the controller exposes methods that cause the actual model method execution. The model contains behavior as well as data, and it uses that data either to hold results from method executions or as input for some actions.
A simple PublisherModel
component may expose, besides the properties that map to the publishers table fields in the sample Pubs database, the three basic operations on an entity: Load
, Save
and Delete
:
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();
}
}
Note that the method takes the publisher ID to load from its own property. Likewise, whatever it loads is put back into the model properties. The other two method implementations behave similarly.
Like we said, in order to access these model behavior, it must be exposed through the controller:
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);
}
Basically, they propagate the method call to the contained model and finally raises an event the view can use to update its own control values.
Both in a Windows Forms button click, as well as on a Web Forms button click, we can put the following code to cause actions in the model:
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();
}
Note that there is no �platform� specific code. Not a single line. But how and when does the controller update the UI? There are fundamental differences in this timing in the Web and Windows environments. In the former, the UI is loaded typically on the Load
event, as post-backs cause new Load
events to be fired. A Windows Form, however, needs to refresh the view at times other than the Load
, because further actions don�t cause new Load
events. To provide this connectivity between the containing form, its events and the controller actions (namely, RefreshView
and RefreshModels
), we use connectors.
Connecting the Views
The responsibility of the connector is to call the controller methods to synchronize the models with the view at the appropriate time. Basically, the UI container (a Web or a Windows Form) should instantiate the appropriate connector and call Connect
passing the relevant parameter to perform the wiring. We provide a base class as usual from which both connectors will inherit:
public abstract class BaseConnector
{
public abstract void Connect(BaseController controller,
object controlsContainer, IContainer componentsContainer);
}
In order to make the process automatic, we will emit the concrete connection code from inside the controller code generation, after detecting the hosting technology. This is the relevant code from the ControllerCodeDomSerializer
method:
public override object
Serialize(IDesignerSerializationManager manager, object value)
{
...
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);
}
A controller placed in a Web Form generates the following code:
private void InitializeComponent()
{
...
this.publisherController.ConnectionString =
((string)(configurationAppSettings.GetValue("Pubs", typeof(string))));
this.publisherController.ConfiguredViews.Add("txtID",
new Mvc.Components.Controller.ViewInfo("txtID",
"Text", "Publisher", "ID"));
new Mvc.Components.Connector.WebFormsConnector().Connect(
this.publisherController, this, this.components);
...
}
The same component placed in a Windows Form generates:
private void InitializeComponent()
{
...
this.publisherController.ConnectionString =
((string)(configurationAppSettings.GetValue("Pubs", typeof(string))));
this.publisherController.ConfiguredViews.Add("txtID",
new Mvc.Components.Controller.ViewInfo("txtID",
"Text", "Publisher", "ID"));
new Mvc.Components.Connector. WindowsFormsConnector().Connect(
this.publisherController, this, this.components);
...
}
Note that the only difference is the concrete connector being instantiated. An important thing to notice here is that this method will never be called at design-time. The IDE only loads properties on components, but it will not call methods.
The Web Forms connector is fairly simple:
public class WebFormsConnector : BaseConnector
{
public override void Connect(BaseController controller,
object controlsContainer, IContainer componentsContainer)
{
Page page = (Page) controlsContainer;
page.Init += new EventHandler(controller.Init);
page.Load += new EventHandler(controller.RefreshModels);
page.PreRender += new EventHandler(controller.RefreshView);
}
}
Note that this will hook the controller to the page events, causing them to be called at the appropriate times.
Let�s wrap up the interactions that will happen at the Load
event:
The critical point here is the Site.GetService()
method call, which must give the controller an instance of the appropriate adapter to use. We have already tested that the service is right there when attached at design time. But the designer is only called at design-time, so what happens at run-time?
Back at the beginning, when we introduced this .NET Component-oriented architecture, we said there were differences between UI controls and non-visual components. We are now in a position to explain what this difference is. At design-time, both kinds of components are sited in the DesignSite
and contained in the DesignHost
, as shown back then. But at run-time, components get passed the this.components
field as the container and they become sited on this container, simply a class of type Container
. The site, accordingly, is of type Container.Site
. The problem is that the GetService
method, implemented internally in the Container
class itself, only returns services of type IContainer
, which it itself provides. All other services are gone. Worse, visual controls don�t have a Site
at all, and that�s the difference.
What we can do is perform some run-time initialization and provide a custom site that can answer requests for our IAdapterService
. Remember that the service instance must always be kept somewhere, just as it�s kept by VS.NET at design-time. We will create a RuntimeSite
class implementing ISite
that we can use to �site� the components with our own infrastructure:
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 { }
}
public object GetService(Type serviceType)
{
return _callback(this, new GetServiceEventArgs(serviceType));
} GetServiceEventHandler _callback;
}
Our constructor, besides receiving the current container and component, receives a delegate. We said that the service instance must be kept somewhere during runtime. This callback is provided so that when a component asks for a service, the call is passed to this delegate, which can be implemented as it fits in the technology in use. For example, the web version can use the HttpContext
to keep the object, something a Windows Forms version can�t do. The delegate gives both a chance to answer the GetService
request any way they want. The delegate class and its arguments are:
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;
}
Let�s have a look at the complete web connector constructor, which initializes the run-time site also:
public override void Connect(BaseController controller,
object controlsContainer, IContainer componentsContainer)
{
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];
}
controller.Site = new RuntimeSite(componentsContainer, controller,
new GetServiceEventHandler(service.GetServiceHandler));
foreach (IComponent component in controller.Components)
{
component.Site = new RuntimeSite(controller, component,
new GetServiceEventHandler(service.GetServiceHandler));
}
Page page = (Page) controlsContainer;
page.Init += new EventHandler(controller.Init);
page.Load += new EventHandler(controller.RefreshModels);
page.PreRender += new EventHandler(controller.RefreshView);
}
}
The WebFormsAdapterService.GetServiceHandler
method is in charge of responding to requests from components now:
internal object GetServiceHandler(object sender, GetServiceEventArgs e)
{
if (e.ServiceType == typeof(IAdapterService))
{
return this;
}
else
{
return null;
}
}
Simply enough, the web version of the connector first stores the service in the HttpContext
, and later lets the service itself answer to components asking for the service. Now the controller, in its RefreshModels
method to be called at Page.Load
time, will successfully retrieve the service:
public class BaseController : Component, IContainer, IExtenderProvider
{
internal void RefreshModels(object sender, EventArgs e)
{
IAdapterService svc = (IAdapterService)
this.Site.GetService(typeof(IAdapterService));
svc.RefreshModels(this);
}
The web version of the framework is now complete. When the page is loaded, the model is updated with whatever values existing on the form as posted (or initially loaded) by the user. Just before rendering to the client, and generally after all the event handlers for button clicks, textbox changes, etc. have been called, the controller refreshes the view with what has ended in the model. A Web Form showing publisher data and with the correct mappings, can provide load/save/delete features with the single line of code that we saw in each button handler:
private void btnLoad_Click(object sender, System.EventArgs e)
{
controller.LoadPublisher();
}
Now, suppose we enter an ID
in the respective field and click Load
, this is the sequence of actions until the page comes back to us:
- On
Load
, the adapter service is hooked and components are sited.
- Model is refreshed, so the value in
txtID
is placed in the PublisherModel.ID
property.
- The event handler calls the controller method,
LoadPublisher()
.
- The model goes to the database and loads itself with the returned row.
- Before rendering, control values are updated with the new values in the model (the complete publisher data).
- The page is rendered with the values and sent to the browser.
And the page designer only had to configure the mappings using our IDE integration features, and call the appropriate controller method when it was appropriate!
What�s more, if later we decide to move (or at the same time) to a Windows Forms client, the same controller and mappings will do the work. And even the event handlers for the UI events will look the same!
As Windows Forms allows statefull applications, we can simply keep the relevant variables inside the connector itself. However, in the web, at initialization time, we know all controls have been created. This is not the case for Windows Forms, where controls are just class-level variables that must be initialized in the InitializeComponent
method just like our controller. Therefore, there�s no guarantee that our connector will be called after the controls have been initialized.
And why do we need to access the controls at initialization time? Because unlike the web where we have a single point of model refreshes, the Load
event, in Windows Forms, we have to refresh the models as soon as a control is modified, and we have to attach to that event and trigger the model refresh.
So we have to resort to some Form event to perform the actual hook:
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;
controller.Site = new RuntimeSite(componentsContainer, controller,
new GetServiceEventHandler(GetServiceHandler));
foreach (IComponent component in controller.Components)
{
component.Site = new RuntimeSite(controller, component,
new GetServiceEventHandler(GetServiceHandler));
}
Form form = (Form) controlsContainer;
form.Activated += new EventHandler(OnActivated);
form.Activated += new EventHandler(controller.Init);
form.Load += new EventHandler(controller.RefreshView);
form.Deactivate += new EventHandler(controller.RefreshModels);
}
As you can see, each connector (one for each controller) will have the service and the corresponding connector. The OnActivated
handler takes care of hooking the RefreshModels
method to each control Leave
event:
void OnActivated(object sender, EventArgs e)
{
foreach (DictionaryEntry entry in _controller.ConfiguredViews)
{
Control ctl = (Control)
_service.FindControl(((ViewInfo)entry.Value).ControlID);
ctl.Leave += new EventHandler(_controller.RefreshModels);
}
_controller.RefreshView(sender, e);
_controller.ModelChanged += new EventHandler(_controller.RefreshView);
}
The other trick we did at the end is hook the ModelChanged
event fired from the controller to the same controller RefreshView
method. This way we achieve automatic UI updates without writing a single line of code.
Finally, the delegate that handles GetService
requests (and which we passed to the new RuntimeSite
for each component), simply returns the reference it keeps to the service:
object GetServiceHandler(object sender, GetServiceEventArgs e)
{
if (e.ServiceType == typeof(IAdapterService))
{
return _service;
}
else
{
return null;
}
}
}
The exact same code (except for the connector instance) can now be present in both a Windows and a Web Forms application that will behave exactly the same, even at the configuration level.
Persisting at design-time through code certainly has an edge over run-time detection and emission: it�s compiled and will always be faster. For example, we may want to emit some attributes in the rendered Web Forms controls that we could use in client-side JavaScript to know the details of the mappings. Again, there�s a natural way that seems obvious: attach to PreRender
and iterate configured mappings again, adding the relevant attributes to the control.
If the mappings and the controls are known at design-time, why do we have to waste precious run-time processing doing this iteration over and over on each page interaction? The answer lies in a much better way that we can take advantage of. It�s called an IDesignerSerializationProvider
.
An object implementing this interface can be registered with the IDesignerSerializationManager
(which we receive in our controller serializer).
internal class ControllerCodeDomSerializer : BaseCodeDomSerializer
{
public override object
Serialize(IDesignerSerializationManager manager, object value)
{
...
if (!(manager.GetSerializer(typeof(System.Web.UI.Control),
typeof(CodeDomSerializer)) is WebControlSerializer))
{
manager.AddSerializationProvider(
new WebControlSerializationProvider());
}
We first check that the provider hasn�t been added already. At serialization time, the manager will call each configured provider and give it a chance to provide a custom serializer for a type. This is done in the GetSerializer
method:
internal class WebControlSerializationProvider :
IDesignerSerializationProvider
{
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;
}
}
If we find a control we can attach attributes to (implementing IAttributeAccessor
), we return our new serializer. From now on, every time the IDE needs to serialize a Web control (either HtmlControl
or WebControl
), our serializer will be called, receiving the control being serialized.
internal class WebControlSerializer : BaseCodeDomSerializer
{
public override object Serialize(
IDesignerSerializationManager manager, object value)
{
...
}
In order to avoid disturbing the normal serialization process, we will retrieve the regular serializer for the received control first, and get the code statements from it. We defined a helper method in the base serializer to get the appropriate type:
protected CodeDomSerializer GetConfiguredSerializer(
IDesignerSerializationManager manager, object value)
{
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);
}
Here we see in action the ITypeResolutionService
. After we retrieve the relevant attribute, all we have is the qualified type name, and we must load it through that service to guarantee the assembly it lives in is located and successfully loaded. After that, we simply use the Activator.CreateInstance
method to create the serializer and return it.
Let�s go back to the WebControlSerializationProvider.Serialize
method:
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);
When a property is extended, it will appear as a property of the object itself if we use the TypeDescriptor
:
PropertyDescriptor prop =
TypeDescriptor.GetProperties(value)["WebViewMapping"];
ViewInfo info = (ViewInfo) prop.GetValue(value);
Now we need to generate the following code to add an attribute to store the controller name, for example:
((IAttributeAccessor)control).SetAttribute("MVC_Controller", "controller");
We will do so for each of the four ViewInfo
properties also:
if (info.ControlProperty != String.Empty &&
info.Model != String.Empty && info.ModelProperty != String.Empty)
{
CodeExpression ctlref = SerializeToReferenceExpression(
manager, value);
CodeCastExpression cast =
new CodeCastExpression(typeof(IAttributeAccessor), ctlref);
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)
}));
...
return statements;
}
}
We defined the expressions we use repeatedly, such as the cast to IAttributeAccessor
and the control reference.
Now we can cause a code generation process on the page, by changing a view mapping and later changing a Web control property, for example. The newly serialized code looks like the following:
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");
...
Now the rendered page has the attributes without having to add them dynamically at run-time:
<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>
...
As a side node, we have to point that the ViewInfo
object contains a Controller
property that points to the controller that created it. That�s how we emit its name to the page. This is a specific implementation detail of ours, but what if that property didn�t exist? How would we retrieve the real property provider backing up a �fake� property on a control?
The answer is not easy, unfortunately. When we ask the TypeDescriptor
for the property, we expect a PropertyDescriptor
. However, when the property is extended, in a class derived from it, ExtendedPropertyDescriptor
is returned instead. This object has a property, Provider
that has a reference to the object that is providing this property. Great! What else could we ask for? Well, we could ask for the class not to be private!
So, now that we�re out of luck, we have to find a workaround, as usual. The trick comes from reflection. Beware that the technique we�re about to use is highly risky. If MS decides to change the class name or the property name, or even remove it, your now-working code will stop doing so. And they wouldn�t be doing anything wrong, as the interface was private. Now that you are aware, let�s get to it!
To achieve this, we have to first load the type. Through the Assembly.GetType
method we can�t reach a private
type. We have to use GetTypes
and iterate all of them and find the matching one. We created a utility class, DesignUtils
with a LoadPrivateType
that does the trick. Its usage is simple:
Type t = DesignUtils.LoadPrivateType(
"System.ComponentModel.ExtendedPropertyDescriptor,System");
Note that we avoid having to pass the fully qualified assembly name, the method code is as follows:
internal static Type LoadPrivateType(string className)
{
string[] names = className.Split(',');
if (names.Length < 2)
throw new ReflectionTypeLoadException(null, null,
"Invalid class name: " + className);
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.");
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;
}
Now that we have the private reflected type, we can get the property as usual with:
PropertyInfo provider = t.GetProperty("Provider");
Finally, we can get the actual provider instance by using the property info GetValue
method:
object instance = provider.GetValue(prop, new object[0]);
We have encapsulated this behavior in a static field in DesignUtils
, so that this process happens only once. From inside the Web control custom serializer, we could get a reference to the controller with the following code:
PropertyDescriptor prop = TypeDescriptor.GetProperties(value)["WebViewMapping"];
ViewInfo info = (ViewInfo) prop.GetValue(value);
BaseController controller = (BaseController)
DesignUtils.ProviderProperty.GetValue(prop, new object[0]);
This prototype implementation of an MVC-like framework is by no means complete or production-quality. However, we have explored almost every aspect of the IDE design-time architecture, and we have even extended the concept to the run-time, taking advantage of the intrinsic component-oriented features of the whole of the .NET framework. So, even when the Web Forms implementation is particularly weak (because we are causing a full view and model refresh on every change), it does prove the concept. More granularity, as well as a richer event model for the controller would greatly benefit the architecture.
Deep integration with VS.NET can dramatically increase developer productivity, and by using custom code generation techniques we can achieve quite complex component persistence, and even enforce certain architectural decisions, such as the ones we made for the model accesses.
For those researching other areas of component persistence, you should know that components can also be stored in resource files. Even more, we can attach our own serialization provider, as we did for Web Forms controls, to serialize to other mediums, such as XML, a database or whatever.
It�s also possible to create a custom root designer, with custom drawing, elements in the toolbox, etc. Just like the DataSet or XmlSchema designers. Look at the article here for a good example of that.
Tip 1: debugging these designers is hard. We have to either start a web client (to work from within it) or start a Windows one. Trying to debug both is very difficult, as the process seems to be loaded twice, so you effectively stop debugging.
Tip 2: beware that using an ExpandableObjectConverter
may cause the child component (accessible in a converter through the ITypeDescriptorContext.Instance
property) to cease being sited, and we may have trouble retrieving services.
Tip 3: if you use a DropDown type of editor, if you open new dialog forms, the property browser immediately loses control of the focus and the editor no longer controls the opened form.
Tip 4: it�s tempting to use a converter IsValid
method to determine validity of a certain value when we provide lists of standard values. The validation is not automatic, however. We have to override this method and check the received value. But again, this method override may need a TypeDescriptorContext
. As there is no need to use the IWindowsFormsEditorService
, we can safely pass our custom implementation of the context here.
During this article we explored the most advanced features available in .NET and VS.NET for component-based development. We offered deep integration with the IDE, and even expanded the model to the run-time.
We have discussed the MVC design pattern, and created a concrete implementation that can make application development substantially faster. Not only that, we were able to create an implementation that can work with the same code base for both Windows and Web Forms.
- Download and unzip.
- Create a virtual folder in IIS pointing to the WebComponents folder, with the same name.
- Open the solution, and run the Windows and Web apps.
- The code assumes you have SQL installed locally, with the sample Pubs database. The files WebComponents\Web.config and WinComponents\App.config contain a connection string pointing to it. You must ensure the user and password in those files is valid for your installed database.