System.ComponentModel

Brian in Coding | 24 Comments April 18, 2004

Just about anyone who has programmed in the .NET Framework has come across the System.ComponentModel namespace. The Windows Forms, ASP.NET and data classes all implement interfaces from this namespace. But other than that, most people dont know what this namespace is for. Most think it is just a random collection of stuff necessary to support designers. Well, that's partially true: designers make extensive use of this namespace. System.ComponentModel is not just for designers, however. It provides a group of generally useful interfaces and classes you can use in your own applications. In current versions of the .NET Framework all we've really done with component model is provide a useful design pattern; we haven't really made much use of it outside of designers. This post will show you how you can use this namespace in your own applications, utilizing the same design patterns we've used in the designer.

Components, Sites and Containers

At the core of System.ComponentModel is an interface called IComponent. IComponent doesn't have much to it: it derives from IDisposable, offers a Site property and raises an event when the component has been disposed. Anything that implements this interface is called a component. I've drawn a picture relating components, sites and containers below:

Components have two important characteristics:

  • They can be owned by a container
  • They can retrieve services from the container that owns them

The first characteristic doesn't get a component very far, but it does allow a component's lifetime to be controlled by a container. The second characteristic is far more interesting. It allows a component to gain access to services from other parts of the application. A service is simply an instance of an object that is stored in a dictionary keyed off of the object's type. One part of an application can provide a service for another part of the application to use. The power of services lies in their loose coupling: an application publishes the interface or base class that defines the service, but does not publish the class that implements the service. Components can request the service and retrieve an instance they can use, but never have to know about the actual implementation nor where it comes from. This design pattern allows you to develop extremely large applications because each of the parts of the application is only loosely coupled to the other.

Visual Studio makes extensive use of this service pattern for software design, as do the designers we provide within the framework. You can use this pattern in your own applications, too. As an example of this, lets take a look at a service that is already defined in Windows Forms: the AmbientProperties service.

Windows Forms AmbientProperties Service

Try this: change the font of a Form and then place a button on the form. What happens? The button receives the forms font. Why? Because all controls in Windows Forms have several properties that, if unset, ask their parent for a value. This allows you to set fonts and colors once, and have them flow down to child controls. Of course, if you don't set a font on the form a default font is used. Where do you think Windows Forms gets the default font? It actually checks two places: first, it checks to see if it can get to a service that will tell it what font to use. If it cant get to this service it then asks Windows for the default dialog font for the user.

The service Windows Forms looks for is called AmbientProperties and has been sitting in the Windows Forms namespace since the framework originally shipped. By using a container to site all of the dialogs in your application, you can setup application-wide fonts and colors. Our first step is to modify Main so we can create a container for our application:

[STAThread]
static void Main() 
{
    AppContainer c = new AppContainer();
    Form1 f = new Form1();
    c.Add(f);
    Application.Run(f);
}

AppContainer is an internal class I wrote that derives from Container. It implements a single service: AmbientProperties. The entire class is shown below:

internal class AppContainer : Container 
{
    AmbientProperties _props;
    protected override object GetService(Type service)
    {
        if (service == typeof(AmbientProperties)) 
        {
            if (_props == null) 
            {
                _props = new AmbientProperties();
                _props.Font = new Font("Arial", 16);
            }
            return _props;
        }
        return base.GetService(service);
    }
}

If you run this code, and have controls on Form1, youll see that their fonts have enlarged to be 16 points in size. In addition, you can make other forms also have this same behavior by siting them as well. The following code does this in a button click handler for a button I have on form1:

Form2 f = new Form2();
if (Site != null) Site.Container.Add(f);
f.ShowDialog();

You can add other services to the AppContainer class that can be used elsewhere in your application.

Service Containers

In the above example I showed you a really simple mechanism for providing services. Even this simple mechanism has a big advantage: the service wasn't created until someone asked for it. This is what allows large applications to scale well: they can have vast lists of services but those services are not actually instantiated until someone needs them. This pay for play enhances the performance of the application.

In my previous example I specifically check for a service type of AmbientProperties and create the object on demand. This technique has its drawbacks, however. For one thing, as the number of services I want to supply increases, I need to forever expand my if statement. More importantly, only my main method today can offer services. One very powerful concept of the service mechanism is that anyone can provide services to anyone else. The component model namespace can help here too: in addition to service providers, it also defines a service container. As their name implies, service containers contain a table of services. Perfect. Even better, service containers support delayed instantiation of service objects. Lets look at what it would take to change my sample to use service providers.

First, I am going to use a pre-built class in System.ComponentModel called ServiceContainer, that implements the IServiceContainer interface for me. I will plug this into my AppContainer class as follows:

internal class AppContainer : Container 
{
    ServiceContainer _services = new ServiceContainer();
 
    internal IServiceContainer Services 
    {
        get { return _services; }
    }
 
    protected override object GetService(Type service)
    {
        object s = _services.GetService(service);
        if (s == null) s = base.GetService(service);
        return s;
    }
}

Notice that my AppContainer class no longer has any code in it to handle AmbientProperties. It simply routes any service requests into the service container, and calls base if the service wasn't found in the service container. Now that I've done this, I need to add AmbientProperties back into the service container. I do this in Main:

[STAThread]
static void Main() 
{
    AppContainer c = new AppContainer();
    AmbientProperties p = new AmbientProperties();
    p.Font = new Font("Arial", 16);
    c.Services.AddService(typeof(AmbientProperties), p);
    Form1 f = new Form1();
    c.Add(f);
    Application.Run(f);
}

A quick run of the code shows that I still have my big 16 point font in my dialog. But, I lost something here. I've lost the pay for play feature I had before. Now, even if no one ever asks for AmbientProperties, I've still created it, and that takes resources. I'd like to only create AmbientProperties if someone asked for it. Can I still use service containers? You bet! Service containers can accept a callback delegate in place of a service instance. Lets just modify Main a bit more:

[STAThread]
static void Main() 
{
    AppContainer c = new AppContainer();
    c.Services.AddService(typeof(AmbientProperties), 
    new ServiceCreatorCallback(OnCreateService));
    Form1 f = new Form1();
    c.Add(f);
    Application.Run(f);
}

Here I've supplied a callback delegate instead of an instance of AmbientProperties. This delegate will be invoked the first time someone asks for AmbientProperties. My implementation of OnCreateService is pretty straightforward too:

private static object OnCreateService(IServiceContainer c, Type service) 
{
    AmbientProperties p = new AmbientProperties();
    p.Font = new Font("Arial", 16);
    return p;
}

The final thing that ServiceContainer provides for me is an instance of IServiceContainer as -- you guessed it -- a service! Anyone who is sited in my AppContainer class can call GetService(typeof(IServiceContainer)) and add their own services to the application. In the next section I'll demonstrate how you can put that to use.

Services in your Application

Using the IServiceContainer service provided by ServiceContainer (wow, thats a lot of "service" terminology) you can add services from anywhere in your application. For example, say youve written an MDI application and you have a status bar on the bottom of your main MDI window. You can add the status bar as a service in your forms Load event:

private void Form1_Load(object sender, System.EventArgs e)
{
    IServiceContainer s = GetService(typeof(IServiceContainer)) as IServiceContainer;
    s.AddService(typeof(StatusBar), statusBar1);
}

Then, you can use the status bar in other MDI children within your application:

private void Form2_Load(object sender, System.EventArgs e)
{
    StatusBar sb = GetService(typeof(StatusBar)) as StatusBar;
    if (sb != null) sb.Text = "My Status";
}

Notice that I am checking for null when I call GetService for the status bar. This is one of the fundamental things to remember about services: you should have some sort of fallback if the service isnt around. Here, the consequences of not having status text are not major, so I simply handle null by doing nothing. If I needed the service as a requirement of my application, I would probably display an error to the user, or log an error to the event log so support personnel could diagnose the problem.

Wrapping Up

Well, hopefully your eyes aren't glazing over yet. The component model provides a simple design pattern you can use in your applications. Using containers and sites to offer services to the various parts of your application can help your development process scale better as your application becomes more complex. Once you get into the service groove youll find them quite flexible and easy to use.

 

Comments (24) -

Frank Hileman, Monday, April 19, 2004 at 10:04 AM

Interesting. I was thinking about adding a service to the VG.net designer to allow people to insert custom serializers. Services form a kind of plug-in model. But I did not determine how dll's providing a service could be dynamically discovered and loaded in VS. The usual substitute for this is an IExtenderProvider, but this requires an instance within each root component you want to provide the "service", or an add-in.
If you could comment on the serialization format for newer versions on VS I would be much obliged. I have been working on an xml serializer for a format called MyXaml. The serializer is nearly generic, but of course we cannot handle true instance descriptors or custom codedom serialization. Since true XAML does not seem to serialize arbitrary Types or object graphs using the current designer serialization attributes, what will be used in VS?

Brian Pepin, Monday, April 19, 2004 at 11:53 AM

VS Whidbey still uses code generation.  There are two reasons for this:  first, it would be silly of us to come up with yet another XML markup language.  XAML is going to be our markup format for both Avalon and Windows Forms on future platforms.  But, XAML hasn't shipped yet and we need to allow it to mutate before it ships.  That means we can't ship first and lock it down.  Second is the problem of supporting controls that used to do custom code gen in a markup world.  We do not yet have a story for supporting custom serializers and instance descriptors in an XML serialization model.  
XML serialization is really just code serialization "turned on its head".  Here's an example:  in code serialization, you use heuristics to find the "best" code gen: first look for a custom serializer, then an instance descriptor, and finally an ISerializable that uses type converters to spit strings to .resx files.  In XML serialization this pattern is just the opposite.  The best choice is that text you threw into the .resx file because it is the closest to your target format. Each model gets you a bit further from your target format and is therefore less desirable.
Instance descriptors are closer to XML because they are just metadata.  We could come up with some sort of mapping layer that would map an instance descriptor to XML.  But the XML would no longer direclty represent the shape of the object, which requires some special casing in the schema and I don't like that.
Custom serialization is a bit harder.  Third party control vendors have "embraced" custom serialization with abandon and lots of controls now use it.  Mapping this back to XML is very hard for two reasons.  First, CodeDom, unless it is represented as cdata sections, has no attractive mapping to XML.  Sure, you can reproduce the code dom as an XML graph, but who wants to look at that?  Second is how one would invoke the code dom serializers to begin with.  They require serialization managers and a deeply populated context stack of previous serializer activity in order to function.  You can't just pop one of these guys into the middle of an XML-based serialization scheme.  No, you have to do the whole process in parallel and somehow merge the two in an intelligent way.  
We will certainly have to tackle these issues, but they are still far on the horizon.  I'm sure we'll end up with something that favors XML and reverts to code generation for controls that don't support XML.  In time, more controls will be written to support XML and the problem diminishes.

Frank Hileman, Monday, April 19, 2004 at 5:48 PM

Thanks! No one else at MS would answer that question.
Since VG.net has to serialize well both to CodeDom and MyXaml (xml), we came up with the following scheme for InstanceDescriptors. First, all Types must have a default constructor. Otherwise, we need a special constructor syntax in the xml, and as you noted, it doesn't look good.
But the default constructor does not have to be used in CodeDom serialization -- that part does not change. We added a CanConvertTo/ConvertTo option in the TypeConverters that produce InstanceDescriptors in code serialization. They can "convert" to something called MustSerialize. MustSerialize is a list of names of properties that must be serialized, regardless of the ShouldSerialize return value of the PropertyDescriptor. These are the properties that would normally be set in the arguments to the constructor specified by an InstanceDescriptor. We cannot return "true" for ShouldSerialize for these properties, or they will be redundantly serialized in the CodeDom case -- if isComplete is false, that is.
In this way we can be ensured all properties are serialized, using the same logic as the CodeDom serializer, without having to change or break any of the existing serialization attributes or InstanceDescriptor code.
Originally we wanted to use XAML but quickly realized the markup produced simply looks awful if you are not serializing Avalon objects. Please take a look at a snippet of MyXaml serialized xml at my blog (the homepage link on my name). You can see it is very readable and a straightforward, 1-1 mapping to the same thing produced by codedom. But the xml is more compact and easier to read, and more generic than XAML.
If the MyXaml is compiled it would be just as fast as the CodeDom generated code at run-time, no longer needing reflection. We only use it as a supplemental export format since VS is fundamentally codedom based.
I am extremely disappointed in XAML and was hoping VS would not use that Avalon-specific format for general xml serialization in the future.
Regarding generating custom code: I would not be suprised if many custom controls do that. But I imagine they only generate a small amount of code, and could easily convert to a good xml format given the support needed to produce the same effect.

Ifeanyi Echeruo, Monday, April 19, 2004 at 11:02 PM

What happens when there is more than one service of the same type in a Container? How would one go about getting one service and not the other?

Fons Sonnemans, Tuesday, April 20, 2004 at 1:35 AM

Great article. I always wondered how it worked. This will realy improve my applications. Thanks!

Brian Pepin, Tuesday, April 20, 2004 at 8:50 AM

Ifeanyi -
You can only have one type of a service at a time.  This means that you should define a class for the type of service you want to supply and not use too general a class.  For example, Hashtable might be a bit too general a class to expose, but a class called MyApp.PersistentStateTable, which derives from Hashtable, would be a more descriptive choice.
Sometimes there are cases where part of your application provides a service but another part plugs in and wants to replace that service with a better version, or perhaps just wants to augment the behavior of the original service.  You can do that too, with a technique called "service chaining".  Basically:
1) call GetService for the service you want to replace.
2) Create a new service, passing the original instance to your new service constructor.
3) Call RemoveService on IServiceContainer to remove the service you obtained.
4) Re-add the service using your new object as the instance.
By doing this your new object will satisfy all service requests, and be able to route to the original service should it need to.

Carl Taphouse, Saturday, May 1, 2004 at 9:06 AM

Brian,
Great article.
I took your intial example and set the Foreground and Background properties in the AmbientProperties object (I'm a color freak) to spruce up my controls.  The Foreground property worked as expected but the Background property didn't change.  Is this a bug or am I missing something?
Here's my revised code snipet:
if (_props == null)
{
_props = new AmbientProperties();
_props.Font = new Font("Arial", 16); // works
_props.Foreground = Color.Pink; // works
_props.Background = Color.Red; // doesn't work
}
return _props;
Thanks in advance,
Carl

Fabrice, Wednesday, May 12, 2004 at 2:02 AM

Really instructive post, thanks.
I have the same concerns than Ifeanyi. If we take your example with the StatusBar, what if there is a second StatusBar? Deriving a new class for just that purpose doesn't really make sense, IMO. Why aren't we able to provide services based on a generic key (object) instead of a type? This would help.
Maybe some people would be interested to know that an English version of DotNetGuru's article is available: http://www.dotnetguru.org/us/articles/ioc/ioc.html

Brian, Friday, May 14, 2004 at 8:41 PM

Carl -
For many of these styles, individual controls override them.  Control itself process BackColor, ForeColor and Font with no problems, but some derived classes choose their own default colors and therefore ignore what's in ambient properties.
Frabrice / Ifeanyi -
Yes, for many generic types such as Control, Dictionary, etc, it would be convenient to pass a key.  Using a type here has value, though:  you really want the service "contract" to spell out what the service is for, and by using the service type as the key we are able to perform some simplisitic checking.  For example, if the key were a string you'd have to document that a "StatusBar" string mapped up to a particular type of control so the caller knew what to cast to.  By using a type here, that's one less relationship you have to document.  It does force you to create new types, but in some ways that's a good thing.  Services are all about code abstraction.  In my above example I used a StatusBar for simplicity, but a more realistic method would be to have some abstract type, say, StatusService.  This type would have a method such as SetStatus that would map to the correct status bar for you (if you had multiple).  This would also insulate you from code changes.  For example, if you upgraded your code to our upcoming Whidbey release and decided that you want to swap out status bar for our new StatusStrip control (because, I hear, it's the Bizomb), you could do that and the rest of your app would not have to change.

Ken Cowan, Monday, July 19, 2004 at 8:18 AM

Brian,
This article says "If it cant get to this service it then asks Windows for the default dialog font for the user".  Is it supposed to be the default dialog font or the default gui font?  MS Shell Dlg 2 turns into Tahoma on XP but GetStockObject( DEFAULT_GUI_FONT ) returns MS San Serif.  
We're trying to figure out how to easily get WinForms to default to a locale-sensitive font for our dialog boxes.
KC

Brian Pepin, Thursday, July 22, 2004 at 8:21 PM

Ken -
Timely; we've been discussing this internally at Microsoft recently.  Windows Forms calls GetStockObject(DEFAULT_GUI_FONT).  Under the covers, this API looks in the registry for a font that maps to "MS Shell Dlg", which on many platforms is "MS Sans Serif".  This can change on different platforms, so yes, using the current default font mechansim in Windows Forms will localize correctly to any platform.  You should not need to use MS Shell Dlg2 to get a completely localizable Windows Forms application.

Ali, Friday, August 20, 2004 at 3:11 AM

Brian,
Thank you for a wonderful article.
I, too, have a question regarding sreialization for which i couldn't find an answer anywhere.
First some background, in a c++ application, we have several com components. These com components are responsible for their own serializations. All this information is stored in a single file.
The "main" program creates OLE storages and hands them over to each component which then serializes itself. Since all the data has to be kept in one single file and there are multiple components that have no set sequence of getting serialized, the storages and streams architecture works wonderfully.
Now, we are going to rewrite most of this in c#/.net. What do you think we should do in this scenario (multiple components resopnsible for their own serialization in a single file)?
What would be the correct c# idiom or design pattern to implement this?
ps. I have a suspicion that this can be done with xml serialization but would it be able to handle out of sequence serailization.

Brian Pepin, Thursday, August 26, 2004 at 9:09 AM

Ali --
For .NET, there are several forms of serialization you can use.  The OLE storage / stream model is most analogous to .NET's serialization model.  Objects can be declared as serializable (and can control their own serialization aspects) and arbitrary graphs of objects can be saved in this fashion.  However, care must be taken because .NET's serialization model defaults to being version-brittle.

Peter, Tuesday, August 31, 2004 at 12:24 PM

Hi Brian,
I'm trying to make the MDI example work, but if I add the following line in the Form1_Load event of an MDI form, I always get null:
IServiceContainer s = GetService(typeof(IServiceContainer)) as IServiceContainer;
The .Site property of the form is also null, so it makes sense that GetService() returns null, but I don't know how to set the site property.
Thanks

Brian Pepin, Friday, September 3, 2004 at 7:43 AM

Peter -
There is no service container installed by default; you must connect a ServiceContainer and a Container together, and then site the MDI child using AppContainer class I have in the article:
AppContainer _globalAppcontainer = new AppContainer();
Form f = new MyMdiChildForm();
_globalAppcontainer.Add(f);
f.MdiParent = mdiParent;
f.Show();
At this point, f will have access to the global app container and any services that were declared within it.

Douglas McClean, Tuesday, September 28, 2004 at 1:37 PM

I notice that Control defines a GetService method as well, but siting the form that the control lives on is apparently insufficient to Site the control. If an individual control needs access to a service (suppose, for example, that OnMouseEnter it wants to get the status bar service and change the message) should it obtain it by calling this.FindForm().Site.GetService()? Or is there a better way?

Brian Pepin, Friday, October 1, 2004 at 1:28 AM

Douglas --
Yes, today you must find a sited control.  Control's GetService method does not walk parents (although I think it should).  You can implement this generically as follows:
protected object GetAnyService(Type t) {
    Control c = this;
    while (c != null && c.Site == null) {
        c = c.Parent;
    }
    if (c != null) return c.Site.GetService(t);
    return null;
}
You could also move this out to a static utility class so you could access it from within any control:
Utils.GetService(this, typeof(MyService))

Charlie Poole, Thursday, April 7, 2005 at 12:01 AM

Hi Brian,
Great article. I saw it quite a while back and only now got around to using it for NUnit. I've experimentally used it to change the font across the whole app, but the more serious use is for singleton services like UserSettings and TestLoader.
I use the AppContainer class as you did it with the single addtion of adding the _services object to itself - a perverse but cool thing to do - so it's available as a service as well.
Thanks.
Charlie

John Parlato, Wednesday, July 27, 2005 at 9:39 AM

Hello Brian, I enjoyed this article.
I wonered if this same technique be used safely in web form applications?
Thanks.

Andrew Miadowicz, Friday, September 30, 2005 at 9:46 AM

Brian,
I've been wondering about System.ComponentModel for quite a while.  It seemed to me an intriguing concept and I've attempted several times to use it in a fashion you described in your article, but failed.  Your article along with that of Daniel Cazzulino go a long way to explain how to take advantage of the classes in this namespace.
One mystery still remains, however.  What is the implementation of GetService method in the default Component class?  How does it find the requested service?  Does it contain its own ServiceContainer instance and look up services there?  Does it have access to the Site property of the class that contains it and attempt to locate the service by calling Site.GetService?  None of these appears to be true, but I'd like to understand it better.
Now, here is a scenario that I would like to use ComponentModel in.  1) I create AppServiceContainer class much like in your article. 2) I create the main form and add it to the AppServiceContainer. 3) I place some components on the designer surface of my main form. 4) These components provide and register some services. 5) These services can be subsequently accessed by any controls I add to the form either at design time or at run time.  Somehow, I can't achieve this.  Even if I did use the trick you describe in one of the comments of traversing the control parent hierarchy I still get stuck because the form uses its own "components" member field which is created as the default Container implementation.  This in turn seems blissfully unaware of the fact the the form itself is contained by the AppServiceContainer.  It also does not provide the IServiceContainer service, so my components cannot register their services.  I could register the services manually in the main form, in which case they would go to the AppServiceContainer, but the components hosted on the form still don't have access to them.  I would also like to keep all this as transparent to the forms and controls as possible.
Could you offer any advice?
Thanks.
Andrew

Brian Pepin, Friday, October 7, 2005 at 8:50 AM

Andrew --
Yes, Component's GetService method goes through the site.  The implementation checks for a non-null site, and if it finds one, it simply calls GetService on the site.  If the site is null the method always returns null.
What you are doing is a good way to go, but what you're running into is that Control.GetService only checks for a site on that control.  It doesn't walk up the parent hierarchy, so you're never getting to your form's site.  I'd recommend you write a static utility function that walks parents.  Here's a simple version:
internal static object GetControlService(Control c, Type serviceType) {
    Control walk = c;
    object service = null;
    while(walk != null && service == null) {
        ISite site = walk.Site;
        if (site != null) service = site.GetService(serviceType);
        walk = walk.Parent;
    }
    return service;
}

Alan, Saturday, February 11, 2006 at 2:41 PM

Mr. Pepin,
I got a tricky question about the ComponentTray: Could we hide the ComponentTray from the custom designer? It seems to me that the component tray sits in a split window inside the deisgner. But I don't know how to get a reference to it from my designer. Could you please kindly help me on this? Thank you very much.
Alan

Brian, Tuesday, February 14, 2006 at 4:59 PM

Alan -- There isn't a programmatic way to hide the component tray, but it will (should?) only show up if there are components down there.  You can prevent components from coming into the tray by placing a DesignTimeVisible(false) attribute on the component's class.  I'm not positive that will help, but it's worth a shot!  

Al Tenhundfeld, Friday, August 25, 2006 at 2:46 PM

Hi Brian,
I just stumbled across this article, and it's really a great intro to component programming and specifically the .Net component model. Good job.
You should make your site compatible with FireFox rss bookmarks. I don't know the specifics of implementing it, but I know Don Box has done so on his aspx blog.
Thanks,
Al
Comments are closed