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:
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:
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)));
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()
{
}
...
}
}
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.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 Panel
s and some other controls, but not for ToolStripMenuItem
and alike. To resolve this problem, RegisterControl()
comes in handy.
RegisterControl("ToolStripMenuItem_SaveAs", 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();
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)
{
FileIOPermissionAttribute fiopa =
(FileIOPermissionAttribute)permissionAttribute;
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:
[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.