Click here to Skip to main content
15,886,199 members
Articles / Web Development / CSS
Article

Windows Style via IExtenderProvider

Rate me:
Please Sign up or sign in to vote.
4.36/5 (4 votes)
1 Aug 20058 min read 39.1K   575   42   2
Implementing Cascading Style Sheets for .NET Windows Forms with IExtenderProvider and C#.

The Designer before and after style is applied

Introduction

When I read James T. Johnson's article on the IExtenderProvider, I found what I had been looking for: a way to implement Cascading Style Sheets (CSS), in Windows Forms. Whereas a full CSS implementation matching the HTML version would require lots of details, this article will focus only on the Button and TextBox controls. It is more of a proof of concept that such a thing can be done than a finished product. At a later date, I hope to release a fully functional Windows Forms CSS implementation. For now, most of the cooler features of CSS (such as the *cascading* and the varied element selectors) are left to the imagination of the coder.

Background and Resources

The reader should be familiar with C# programming and IExtenderProvider. The latter can be studied from James T. Johnson's excellent article: Getting to know IExtenderProvider. I will be using James' structure in setting up my IExtenderProvider. You do not need to be familiar with CSS stylesheets as the concepts I'm using are very basic. However, if you would like to extend this small application to aid in your Windows development, you should definitely reference W3's CSS and think carefully about how each concept can be best implemented with an IExtenderProvider. Since this solution involves code running at Design time in Visual Studio, this may be of interest: Debugging design-time functionality.

Using the code

The Zip file includes the Solution called WindowsStyle. You must do the following to get the solution up and running:

  • Extract the Zip called WindowsStyle.zip to c:\projects or your projects directory.
  • Double click the solution to open it in Visual Studio.
  • Build the solution (Ctrl+Shift+B).
  • Find the stylesheet.xml file in the solution directory.
  • Open up the form Form1.cs.
  • Find the property called Stylesheet and set it to the full path for stylesheet.xml above.
  • If you can not find the Stylesheet property of Form1, you may need to add a Style IExtenderProvider component to Form1 first:
    • Right click on the Toolbox (get this by pressing Ctrl+Alt+X) and select Add/Remove Items...
    • Click Browse, go to the bin directory and select WindowsStyle.exe.
    • You will now have a Style component you can drag from the toolbox to Form1.
    • After doing so, set the Stylesheet property of Form1 as mentioned above.
  • Click on any of the Button or TextBox controls or drag new ones to the form. Set their CssClass property and watch them change.
  • Notice that setting a property manually to something else and then running the code keeps the CssClass specified appearance.

Detail

The IExtenderProvider is implemented as a component (typical) and it provides the CssClass property to controls and the Stylesheet property to forms.

C#
[ProvideProperty("CssClass", typeof(System.Windows.Forms.Control))]
[ProvideProperty("Stylesheet", typeof(System.Windows.Forms.Form))]
public class Style : System.ComponentModel.Component, IExtenderProvider
{
    private Hashtable properties = new Hashtable();

The Hashtable properties member holds pairs of (object) --> (properties). Here the object can be a control (Button, TextBox, Form, etc.) and the properties is an instance of class Properties:

C#
private class Properties
{
    public string CssClass;
    public string Stylesheet;

    public Properties()
    {
        CssClass = string.Empty;
        Stylesheet = string.Empty;
    }
}

This class is a wrapper for all the methods we want to provide to all the controls. It holds a CssClass and Stylesheet because our controls and forms will need these.

The only thing you have to define when implementing the IExtenderProvider interface is the CanExtend method. This method is called by the Designer to allow it to figure out what objects this IExtenderProvider is providing extensions for. In our method, since a Form is a child of System.Windows.Forms.Control, all we need to extend is System.Windows.Forms.Control:

C#
public bool CanExtend(object extendee)
{
    return extendee is System.Windows.Forms.Control;
}

So this has the side effect of giving every control that is on the same form with our IExtenderProvider the CssClass property. This is what we want, even though for now we will only support Button and TextBox controls.

Next, in order to actually provide properties, our IExtenderProvider must implement Get[property-name] and Set[property-name] methods for all properties it provides. These are actually needed to allow reflection to work its property extending magic. In our case, the methods are GetCssClass, SetCssClass, GetStylesheet, and SetStylesheet:

C#
[Description("Set this property to apply a class of Style to this Control")]
[Category("Style")]
public string GetCssClass(System.Windows.Forms.Control c)
{
    return EnsurePropertiesExists(c).CssClass;
}

/// <summary>
/// Set the CssClass property. When this is done, automatically read the
/// Form's Stylesheet document and change this control according to the
/// CssClass it wants from that Stylesheet
/// </SUMMARY>
/// <PARAM name="c">The Control changing its CssClass property</PARAM>
/// <PARAM name="value">the new CssClass value</PARAM>
public void SetCssClass(System.Windows.Forms.Control c, string value)
{
    // set the Control's CssClass property
    EnsurePropertiesExists(c).CssClass = value;

    // don't load the class if it's the empty string
    if( value.Length < 1 )
    {
        return;
    }

    // depending on the type of control, change its style
    switch( c.GetType().FullName )
    {
        case "System.Windows.Forms.Button":
            CssButton(c);
            break;
        case "System.Windows.Forms.TextBox":
            CssTextBox(c);
            break;
        default:
            break;
    }
}

I'll talk about the CssClass property and you can check out the Stylesheet property in the code. The Get method is ensuring that the CssClass property exists for the calling control and then returning it. If this property does not exist, the Ensure method can handle errors. This is a common place for problems and I put a little error handling in here. Problems can occur because of controls trying to get properties they don't have.

The Set method is a little more complicated. It starts out by setting the value of the property. If the value is the empty string, it then returns. However, if it is something substantial (i.e. an actual CssClass), then it goes on. This method figures out what type of control is setting the CssClass property and it calls an appropriate method to load that control's style from the stylesheet. Here is a straightforward way to handle Button and TextBox controls. A better algorithm can surely be devised, but since hindsight's 20/20 that's better left to hindsight. Onward then, with the CssButton method. This actually attempts to load the specific CssClass from the stylesheet and apply its properties to this Button:

C#
private void CssButton(object sender)
{
    System.Windows.Forms.Button b = (System.Windows.Forms.Button)sender;
    Hashtable style = GetStyle(b);
    if( style == null ) return;

    if( style["Width"] != null )
    {
        b.Width = int.Parse((style["Width"]).ToString());
    }

    if( style["Height"] != null )
    {
        b.Height = int.Parse((style["Height"]).ToString());
    }
    
    if( style["ForeColor"] != null )
    {
        b.ForeColor = System.Drawing.Color.FromName(style["ForeColor"].ToString());
    }
    
    if( style["BackColor"] != null )
    {
        b.BackColor = System.Drawing.Color.FromName(style["BackColor"].ToString());
    }
    
    if( style["FlatStyle"] != null )
    {
        switch( style["FlatStyle"].ToString() )
        {
            default:
            case "Standard":
                b.FlatStyle = FlatStyle.Standard;
                break;
                        case "Popup":
                b.FlatStyle = FlatStyle.Popup;
                break;
                        case "Flat":
                b.FlatStyle = FlatStyle.Flat;
                break;
                        case "System":
                b.FlatStyle = FlatStyle.System;
                break;
        }
    }
}

That Hashtable-returning GetStyle method is key. We will talk about it below, but first the simple stuff. The returned Hashtable style may or may not contain some properties. It will contain these properties if they were defined under this Button's CssClass class inside of the stylesheet. The Button can then look at the style Hashtable and ask it a bunch of questions like: Do you have Width? If so, set the width of the button to the Width it has. You can see that you can do cool stuff like set the FlatStyle of the Button. Some error handling around this code would eliminate design time errors popping up about incorrect formatting in the stylesheet itself (typing 1p for Width instead of 10 would result in an InvalidCast).

Now as promised, let's look at the code that loads the stylesheet itself and returns the properties that a particular control seeks under a particular CssClass:

C#
public Hashtable GetStyle( System.Windows.Forms.Control c )
{
     System.Windows.Forms.Control parentForm = c.Parent;
     while( parentForm != null && !(parentForm is System.Windows.Forms.Form) )
     {
         parentForm = parentForm.Parent;
     }
    if( parentForm == null ) return null;
    string stylesheet = EnsurePropertiesExists(parentForm).Stylesheet;
    if( stylesheet.Length < 1 || !File.Exists(stylesheet) ) return null;
    
    XmlDocument x = new XmlDocument();
    try
    {
        x.Load(stylesheet);
    }
    catch( IOException ex )
    {
        System.Diagnostics.Debug.Write("Error opening" + 
          " stylesheet document for "+c.Name+": "+ex.ToString());
        return null;
    }

    string cssClass = EnsurePropertiesExists(c).CssClass;
    
    XmlNodeList nodes = x.SelectNodes(string.Format("/stylesheet" + 
                                "/class[@name='{0}']/*",cssClass));
    if( nodes.Count < 1 )
    {
        EnsurePropertiesExists(c).CssClass = string.Empty;
        throw new Exception(string.Format(
            @"Stylesheet: {0}
              CssClass: {1}
              This style class does not exist or 
              does not have any properties",stylesheet,cssClass));
    }
    
    Hashtable style = new Hashtable();
    foreach( XmlNode node in nodes )
    {
        style[node.Name] = node.InnerText.TrimEnd('\n','\r','\t',' ');
    }
    return style;
}

This works in a few steps. First it finds the Form parent of the control. This may be a few levels up as a control can be in GroupBoxes or Tabs or other sorts of containers. Then, it loads the stylesheet of the Form parent. Notice that there's room here to load the stylesheets of all the parents and merge them together. This would allow for the cool "Cascading" feature of CSS, but is beyond the scope of this first article. If a stylesheet is successfully loaded as an XML document, get the CssClass that this control is looking for. This is done using an XPath query. XPath is a wonderful query language and it is summarized nicely here: .NET and XML: Part 1—XPath Queries. Suffice it to say that this particular XPath query returns all the child nodes of the class with name CssClass of the control in question. The GetStyle method then packages all the properties defined in this class into a Hashtable and returns this to whichever control was interested in asking in the first place.

The CssButton method above uses this GetStyle method and looks through the Hashtable setting whatever properties it is compatible with. This happens at design time when a Button's CssClass property is set. The designer will call SetCssClass and go through everything explained above. However, what happens at runtime? The important part of this is to have each control load its properties at runtime. While it is nice to see the controls the way they will look when you run the form, if you change something in the stylesheet, the control should not have to have its CssClass property reset in order to propagate the changes. That would defeat the whole purpose of a centrally defined stylesheet. The way I made sure that controls load their CssClass at runtime is to hook into the Form's Load event. This can be done inside the SetStylesheet method. The Form can iterate through its components and call the CssButton, CssTextBox or CssWhatever methods to set the style at runtime:

C#
public void SetStylesheet(System.Windows.Forms.Form f, string value)
{
    ...
    f.Load += new EventHandler(CssFormLoad);
}
        
private void CssFormLoad(object sender, EventArgs e)
{
    foreach( Control c in ((Form)sender).Controls )
    {
        // only apply style if the Control specified a CssClass
        if( EnsurePropertiesExists(c).CssClass.Length < 1 ) continue;
        
        switch( c.GetType().FullName )
        {
            case "System.Windows.Forms.Button":
                CssButton(c);
                break;
            case "System.Windows.Forms.TextBox":
                CssTextBox(c);
                break;
            default:
                break;
        }
    }
}

This replicates everything explained above but at runtime. Again, a better data structure certainly exists to take the place of the straightforward switch and separate methods I made use of.

In closing, let's look at the format I picked for the stylesheet. Nothing fancy, stylesheet.xml file looks like this. I used it for the example illustrated at the top of this article. The image on the left is before I set any of the CssClass properties. The image on the right is afterwards.

XML
<stylesheet>
    <class name="RedButton">
        <WIDTH>40</WIDTH>
        <HEIGHT>40</HEIGHT>
        <FLATSTYLE>Popup</FLATSTYLE>
        <BACKCOLOR>Red</BACKCOLOR>
    </CLASS>
    <class name="WideButton">
        <WIDTH>200</WIDTH>
    </CLASS>
    <class name="PasswordTextBox">
        <PASSWORDCHAR>#</PASSWORDCHAR>
    </CLASS>
    <class name="MultilineTextBox">
        <MULTILINE>true</MULTILINE>
        <WIDTH>200</WIDTH>
        <HEIGHT>40</HEIGHT>
    </CLASS>
    <class name="SharedStyle">
        <FORECOLOR>White</FORECOLOR>
        <BACKCOLOR>Blue</BACKCOLOR>
        <FLATSTYLE>Flat</FLATSTYLE>
        <BORDERSTYLE>None</BORDERSTYLE>
    </CLASS>
</STYLESHEET>

That's it. If any of that was confusing at all, please don't hesitate to drop me a line. This is my first try at this and I appreciate your patience for any unclear explanation.

Points of Interest

This was an eye opening project for me. With a more thorough implementation, you can save many man hours spent aligning, resizing, coloring and standardizing all sorts of controls on Windows Forms. With careful data structure organization, this can be extended to allow for layout manipulation and many other uses. Currently, I think XML may be a better format for the stylesheet itself. On the other hand, one could take advantage of Firefox's open source CSS engine to parse out a typical .css file for the stylesheet.

History

First submission. A basic proof of concept for applying style to Windows Forms from XML formatted stylesheets.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionWhat about UserControl Pin
Coquito Ito Ito8-May-06 23:26
Coquito Ito Ito8-May-06 23:26 
AnswerRe: What about UserControl Pin
eggshelly30-Jan-07 6:23
eggshelly30-Jan-07 6:23 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.