Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

GUI Skinning System for Windows Forms .NET

0.00/5 (No votes)
22 Dec 2008 1  
A new way of skinning your application!

GUISS Screenshot

Introduction

Ever since I started developing Windows Forms applications, I always missed the possibility to have different user interfaces for the same application. I've considered several techniques ranging from image-based skinning to creating a custom window framework (e.g., Mozilla's XUL). Image based skinning isn't very dynamic, and creating a framework would require a lot of development.

In my search for dynamic skinning, I've come up with the idea of using .NET libraries to provide the necessary forms. In this article, I'll go into the details of my solution.

Using the code

The following figure briefly illustrates how GUISS works:

Schematic1

We have three components: the parent application, the GUISS library, and the skin library. The order of processing is like this (mind that this is very simplified):

  • The parent application instantiates a new Forcepoint.GUISS.Skin class.
  • To load a Skin Library, Forcepoint.GUISS.Skin.LoadSkin(string fileName) is called.
  • The parent application creates a new instance of one of its classes that inherit FormParent.
  • The newly instantiated class is used as a parameter for Forcepoint.GUISS.Skin.CreateForm(FormParent formParent).
  • The GUISS library 'extracts' a FormWindow that matches the given FormParent and returns a fully functional FormWindow that has the same properties, methods etc., as a regular form.
  • GUISS checks if all the controls required by the FormParent are available on the FormWindow.
  • GUISS hooks all the necessary events to the controls.
  • To get a differently skinned FormWindow, load a different skin library.

Everything you need to use GUISS can be found in the Forcepoint.GUISS namespace.

Creating the parent application

In short, parent applications (which I will often abbreviate to "PA") contain the logical code needed to run the application. Skin libraries are the exact opposites, they just contain the forms needed by the parent application.

Instead of having regular forms in your solution, you'll have one or more FormParent classes. These are just standard classes, but they inherit from Forcepoint.GUISS.FormParent. The FormParent abstract class has several properties/functions that you must override. These are:

  • string FormName
  • Dictionary<string,> DeclareRequiredControls()
  • List<subscribedevent> DeclareEventsToSubscribeTo()
  • Dictionary<string,> DeclareWatchedObjects()

In code, it will look like this:

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;

using Forcepoint.GUISS;

namespace ParentApplication
{
    public class MainForm : FormParent
    {
        public override string FormName
        {
            get { return "MainForm"; }
        }

        public override Dictionary<string,> DeclareRequiredControls()
        {
            Dictionary<string,> tmp = new Dictionary<string,>();

            tmp.Add("rtb_Text", typeof(RichTextBox));

            tmp.Add("btn_Open", null);
            tmp.Add("btn_Save", null);
            tmp.Add("btn_SaveAs", null);

            return tmp;
        }
        public override List<subscribedevent> DeclareEventsToSubscribeTo()
        {
            List<subscribedevent> tmp = new List<subscribedevent>();

            tmp.Add(new SubscribedEvent(null, "Load", 
                    new Subscriber.GenericEventHandler(MainForm_Load)));

            //The btn_..._Click's aren't shown in this code block.
            tmp.Add(new SubscribedEvent("btn_Open", "Click", 
                        new Subscriber.GenericEventHandler(btn_Open_Click)));
            tmp.Add(new SubscribedEvent("btn_Save", "Click", 
                        new Subscriber.GenericEventHandler(btn_Save_Click)));
            tmp.Add(new SubscribedEvent("btn_SaveAs", "Click", 
                        new Subscriber.GenericEventHandler(btn_SaveAs_Click)));

            return tmp;
        }
        public override Dictionary<string,> DeclareWatchedObjects()
        {
            //Will be discussed later in this article.
        }
        
        ...
        
    }
}

So, how do the FormParent and the FormWindow interact? Take a look at the schema above the code block. The parent application requests a FormWindow by calling the Skin.CreateForm(FormParent formParent) function. Once the parent application calls this function, the GUISS library calls the overridden functions you've just seen in the code block above.

First, GUISS checks if the FormParent's FormName property is the same as that of the FormWindow. After that, GUISS verifies if all the Controls required by the FormParent (DeclareRequiredControls()) are on the FormWindow. If not, an error is thrown. Once that's done, GUISS hooks the appropriate events to all controls (DeclareEventsToSubscribeTo()). Finally, GUISS calls DeclareWatchedObjects(), which we will discuss later in this article.

If all went well, Skin.CreateForm(FormParent formParent) will return a FormWindow. This is a class that inherits System.Windows.Forms.Form, so it behaves the same.

Creating a skin library

Skin libraries consist of classes that inherit from FormWindow, which in turn inherit Form. Therefore, you can use the Visual Studio designer. You don't need to override anything, the FormWindow class only adds certain functions and methods you can (and should) use. This is an example of how the class would look like:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

using Forcepoint.GUISS;

namespace TextEditor.DarkSkin
{
    public partial class MainForm : FormWindow
    {
        public MainForm()
        {
            InitializeComponent();

            RegisterControl(rtb_Text.Name, rtb_Text);
            RegisterControl("btn_Open", btn_Open);
            RegisterControl("btn_Save", btn_Save);
            RegisterControl("btn_SaveAs", btn_SaveAs);
        }

        private void MainForm_Load(object sender, EventArgs e)
        {
            //This will be discussed later on in this article.
            this.WatchedObjects["current_Text"].OnSet += 
              new EventHandler<watchedobjectseteventargs>(current_Text_OnSet);
            this.WatchedObjects["current_Text"].OnGet += 
              new EventHandler<watchedobjectgeteventargs>(current_Text_OnGet);
        }
       
        ...
        
    }
}

How they interact

The FormWindow must 'register' the controls required by the FormParent. This can be done through the RegisterControl() and RegisterAllControls() methods. One or both of these methods must be called right after InitializeComponent() in the FormWindow's constructor.

For example, the FormParent requires the btn_SaveAs control on the FormWindow. It also requires it to be a RichTextBox. The FormWindow must register this control by using RegisterControl() or RegisterAllControls(). The FormParent class can access these controls via the FormWindow.GetRegisteredControl() function:

string ExampleString = 
 ((RichTextBox)this.FormWindow.GetRegisteredControl(
 "btn_SaveAs").ControlObject).Text;

You don't always know what type the control is when you use GetRegisteredControl. If this is the case, you can use a WatchedObject.

this.WatchedObjects["current_Text"].OnSet += 
  new EventHandler<watchedobjectseteventargs>(current_Text_OnSet);
this.WatchedObjects["current_Text"].OnGet += 
  new EventHandler<watchedobjectgeteventargs>(current_Text_OnGet);

When WatchedObject["current_Text"].Object is get or set, the appropriate events are called. Both the parent application and the skin library can set these events.

For example (these methods are both in the skin library):

void current_Text_OnSet(object sender, WatchedObjectSetEventArgs e)
{
    this.rtb_Text.Text = e.NewValue.ToString();
}
void current_Text_OnGet(object sender, WatchedObjectGetEventArgs e)
{
    e.ValueToReturn = this.rtb_Text.Text;
}

When the object is set (which is done by the parent application), the Text property of the rtb_Text control is set to the new value. Note that the parent application doesn't have to know anything about the control in the skin library. Now, when the object is gotten by the parent application, the skin library returns the Text property of the rtb_Text control.

Points of interest

FormWindow control registering

As mentioned previously, there are two methods used for registering controls. These are RegisterControl() and RegisterAllControls().

RegisterAllControls iterates recursively through all the controls. It does this by walking through FormWindow.Controls, FormWindow.Controls.Controls, FormWindow.Controls.Controls.Controls etc. This works well for Panels and some other controls, but not for ToolStripMenuItem and alike. To resolve this problem, RegisterControl() comes in handy.

RegisterControl("ToolStripMenuItem_SaveAs", ToolStripMenuItem_SaveAs);
//even easier:
//RegisterControl(ToolStripMenuItem_SaveAs.Name, ToolStripMenuItem_SaveAs);

Security

The parent application requires certain security permissions to be set by the skin library. Take a look at the following code on how to get the permissions:

[STAThread]
static void Main()
{
    ...
    Skin Skin = new Skin();
    //Set the security checks.
    Skin.SecurityChecks.Add(typeof(FileIOPermissionAttribute), 
         new SecurityCheckDelegate(SecurityCheck_FileIOPermission));

    Skin.LoadSkin(Application.StartupPath + @"\Skins\" + sp.listBox1.Text);

    Application.Run(Skin.CreateForm(new MainForm()));
}

private static bool SecurityCheck_FileIOPermission(object permissionAttribute)
{
    //PermissionAttribute is an attribute in the Skin Library.
    
    FileIOPermissionAttribute fiopa = 
       (FileIOPermissionAttribute)permissionAttribute;
    //This basically means: Request Refusal 
    //for having Unrestricted access to the FileSystem.
    //In other words: block all access.
    if (fiopa.Action == SecurityAction.RequestRefuse && fiopa.Unrestricted == true)
        return true;
    else
        return false;
}

The skin library sets security permissions at the bottom of the AssemblyInfo.cs file:

//This will fail the security check. Unrestricted must be true to pass.
[assembly: FileIOPermissionAttribute(SecurityAction.RequestRefuse, 
                                     Unrestricted=false)]

Final words

I hope this article made some things clear. I strongly advise you to take a good look at the demo project. Try to replicate the same 'techniques' in the demo for use in your own project. There's also a reference library available. It is the same as the help file provided with the binaries. If you have any questions, feel free to place a message at the bottom of this article.

Finally, GUISS also has a page at Sourceforge. You can go there to get the latest news and updates.

History

  • 22-12-2008: Submitted article accompanying GUISS 0.2.0.1.

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