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.
[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
:
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
:
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
:
[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;
}
public void SetCssClass(System.Windows.Forms.Control c, string value)
{
EnsurePropertiesExists(c).CssClass = value;
if( value.Length < 1 )
{
return;
}
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
:
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
:
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 GroupBox
es or Tab
s 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:
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 )
{
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.
<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.