
Introduction
When I began this article I envisioned that I would concentrate on demonstrating two things:
- My framework for storing and retrieving application configuration settings, and
- My approach for presenting those settings at runtime.
I changed my mind after running across the Configuration Management Application Blocks (CMAB) from the good folks at Microsoft. In case you don't already know, CMAB is a rather impressive .NET library for managing configuration data. (You can read more about the CMAB by navigating to this link).
So, instead of showing everyone how I had been reading and writing configuration data before I learned about the CMAB, I decided to concentrate instead on discussing how I present configuration settings to my users at runtime.
Regardless of how you choose to store configuration data (.INI file, .XML file, WIN32 registry, etc.), it's only a matter of time before someone asks you to expose that information to your end-users. It doesn't matter whether your software is targeted towards developers, managers, co-workers, or external customers. The truth of the matter is that, most people prefer to tweak their configuration settings via some sort of GUI. (I make an exception for all my hardcore Linux/Unix friends.)
I know what you are thinking; writing a dialog-box to present a few \ configuration settings seems pretty trivial - right? Well, in reality things aren't always as simple as they seem. For instance, I routinely come across scenarios such as:
- The need for a solution that is flexible enough to work with dynamically loaded libraries.
- A requirement that visual elements from multiple libraries be combined into a single, consistent user interface.
- The fact that some of the groups I get my libraries from, don't always know (or won't disclose) the internal configuration requirements of their code, until after my software goes into testing.
The code I am about to demonstrate is a quick solution to the problem I have just outlined. The code is contained within a .NET library and consists of two classes: AppConfigEditorPanel and AppConfigEditorForm. C'mon, two classes, you can handle this!
AppConfigEditorPanel is a UserControl that provides a context for presenting and managing configuration settings at runtime. The code for this class is as follows:
public class AppConfigEditorPanel : System.Windows.Forms.UserControl
{
public event System.EventHandler PropertyModified;
private bool m_dirtyFlag;
private System.ComponentModel.Container components = null;
internal bool DirtyFlag
{
get {return m_dirtyFlag;}
set {m_dirtyFlag = value;}
}
public AppConfigEditorPanel()
{
InitializeComponent();
}
public virtual void WritePropertyState()
{
}
public virtual void ReadPropertyState()
{
}
protected void FirePropertyModified()
{
DirtyFlag = true;
if (PropertyModified != null)
PropertyModified(this, EventArgs.Empty);
}
}
Deriving from AppConfigEditorPanel produces a GUI panel that you can customize by adding controls, no differently than you would normally do with a form. The only real difference is that this GUI will be contained within another form at runtime, as opposed to being displayed on it's own.
AppConfigEditorPanel has two virtual methods, both of which are called automatically by an instance of AppConfigEditorForm at runtime. WritePropertyState is called whenever the underlying configuration data should be saved. ReadPropertyState is called whenever the underlying configuration data should be read.
The DirtyFlag property is managed by the AppConfigEditorForm, and is used to track those pages that have had one or more of their properties changed by a user at runtime.
The PropertyModified event is listened to by the AppConfigEditorForm, and is used to indicate which AppConfigEditorPanels need to have their DirtyFlag property set.
The AppConfigEditorForm class is used to manage and contain all the instances of AppConfigEditorPanel at runtime. The code for this class is shown here:
public class AppConfigEditorForm : System.Windows.Forms.Form
{
ArrayList m_panelList;
private System.ComponentModel.Container components = null;
private System.Windows.Forms.StatusBar m_statusBar;
private System.Windows.Forms.TabControl m_tabControl;
private System.Windows.Forms.Panel m_panelButtons;
private System.Windows.Forms.Button m_buttonOK;
private System.Windows.Forms.Button m_buttonCancel;
private System.Windows.Forms.Button m_buttonApply;
public AppConfigEditorForm()
{
try
{
this.Cursor = Cursors.WaitCursor;
InitializeComponent();
ConfigurationManager.Initialize();
m_panelList = new ArrayList();
IDictionary table = (IDictionary)ConfigurationSettings.GetConfig(
"applicationConfigurationEditor");
if (table == null)
throw new ConfigurationException
("Missing 'applicationConfigurationEditor'
section!");
if (!table.Contains("panelCount"))
throw new ConfigurationException
("Malformed 'applicationConfigurationEditor'
section!");
int panelCount = int.Parse((string)table["panelCount"]);
for (int x = 0; x < panelCount; x++ )
{
string panelString = (string)table["panel" + x];
if (panelString == null)
throw new ConfigurationException
("Malformed 'applicationConfigurationEditor'
section!");
string[] panelSections =
panelString.Split(",".ToCharArray());
if (panelSections.Length != 3)
throw new ConfigurationException
("Malformed
'applicationConfigurationEditor'
section!");
AppConfigEditorPanel panel =
(AppConfigEditorPanel)Assembly.Load(
panelSections[0]).CreateInstance(
panelSections[0] + "." + panelSections[1]);
if (panel == null)
throw new ApplicationException(
"Failed to create '" +
panelSections[0] + "." + panelSections[1] +
"' panel!"
);
panel.Dock = DockStyle.Fill;
TabPage tp = new TabPage(panelSections[2]);
tp.Controls.Add(panel);
m_tabControl.TabPages.Add(tp);
m_panelList.Add(panel);
panel.ReadPropertyState();
panel.PropertyModified += new
System.EventHandler(_OnPropertyModified);
}
}
finally
{
this.Cursor = Cursors.Default;
}
}
private void AppConfigEditorForm_Closing(object sender,
System.ComponentModel.CancelEventArgs e)
{
if (this.DialogResult != DialogResult.OK &&
m_buttonApply.Enabled)
{
switch (MessageBox.Show(
this,
"There are unsaved changed.
Do you want to save them now?",
this.Text,
MessageBoxButtons.YesNoCancel,
MessageBoxIcon.Question
))
{
case DialogResult.Cancel :
e.Cancel = true;
return;
case DialogResult.No :
return;
case DialogResult.Yes :
break;
}
}
m_buttonApply.PerformClick();
this.Close();
}
private void m_buttonApply_Click(object sender,
System.EventArgs e)
{
try
{
this.Cursor = Cursors.WaitCursor;
foreach (AppConfigEditorPanel panel in m_panelList)
{
if (panel.DirtyFlag)
{
panel.WritePropertyState();
panel.DirtyFlag = false;
}
}
m_buttonApply.Enabled = false;
}
finally
{
this.Cursor = Cursors.Default;
}
}
private void _OnPropertyModified(object sender, System.EventArgs e)
{
m_buttonApply.Enabled = true;
}
}
The constructor looks at the App.config file for the applicationConfigurationEditor section, and uses that information to determine how many tab pages should be created, and where the code for each associated instance of AppConfigEditorPanel may be found.
The Closing event handler ensures that the user is prompted whenever the form is canceled with an unsaved property change.
The apply button handler is used to direct all the AppConfigEditorPanel instances that contain unsaved changes to save their state.
The PropertyModified event handler is used to enable the 'Apply' button whenever a user modifies a property anywhere within the form.
Using the code
Using the code is pretty simple: first create one or more AppConfigEditorPanel derived classes and populate them with controls and such. After that, derive from AppConfigEditorForm in your main application, make any customizations you require, and then add the code to call the form like this:
private void m_buttonSettings_Click(object sender, System.EventArgs e)
{
new SettingsForm().ShowDialog(this);
}
After that, open the App.config file for your application and add something like the following:
<configuration>
<configSections>
<section
name="applicationConfigurationEditor"
type="System.Configuration.SingleTagSectionHandler" />
</configSections>
<applicationConfigurationEditor
panelCount="2"
panel0="AppConfigEditor.TestGUI,TestPanel1,Application Settings"
panel1="AppConfigEditor.TestLib,TestPanel2,Library Settings"
/>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<qualifyAssembly
partialName="AppConfigEditor.TestLib"
fullName="AppConfigEditor.TestLib,
Version=1.0.0.0,Culture=neutral,PublicKeyToken=null" />
</assemblyBinding>
</runtime>
</configuration>
The configSections section is standard .NET, and is used to declare sections to the .NET configuration classes. This is how the .NET framework finds the various sections at runtime. In this case I have added a declaration for the applicationConfigurationEditor section.
Inside the applicationConfigurationEditor section itself, I have inserted parameters to tell the AppConfigEditorForm how many panels to load, and where those panels should come from. Each panel is configured with a string that contains the assembly that contains the panel, the class that produces the panel, and finally a title for the GUI. The elements of this string are delimited with a ',' character.
I have also shown an example of the runtime section, which is used to load assemblies (libraries) dynamically at runtime. It just so happens that the demo application loads an AppConfigEditorPanel from a library called AppConfigEditor.TestLib, so this section configures the .NET runtime to load that assembly.
The demo application
The demo application uses a form derived from AppConfigEditorForm to present some dummy configuration settings for the application and a library named TestLib. The form is configured using the App.config file to load one AppConfigEditorPanel from the application, and another from the library. The configuration data in the demo is managed via the CMAB from Microsoft, but you can use whatever method you are comfortable with in your own projects.
Conclusion
What I have shown here is a simple method for displaying configuration settings. The nice thing about my approach is that it has nothing to do with managing the settings themselves - it simply focuses on presenting them. That means you can use this code regardless of where or how you manage your configuration data.
Note that my approach also works with libraries licensed from third-party vendors. All you need to do is create a panel to present the configuration data and add the resulting code to your application. Don't forget to also add the appropriate lines to the App.config file.
Have fun! :o)