Easy Configuration
A constantly recurring requirement is a simple yet
comprehensive method of configuring an object at run-time. For example, a form,
control or even a simple object may be adjusted by the user or subject to user
preferences that cannot be set at design-time. One method of course is to build
a configuration system into all of the objects that need it but this would mean
that all the objects may have to derive from a specific object or implement a
standard interface. These methods present subtle problems however. In the case
of objects derived from a standard configurable object, the inheritance tree can
be at best complex and at worst, downright annoying due to the lack of multiple
inheritance in C# and VB.NET. Implementing an interface can require that the
same code be reproduced several times in several places which is ok but can be a
maintenance problem if some aspect of the configuration code needs to change.
The problem.
Before looking at the solution, consider the problem of
creating a configuration system that will allow you to save and restore
properties of your application and it's member classes. The Form class has some
850-odd member properties and fields that are private, protected and public.
Deciding which ones should be saved and which ones left to the default behaviour
is obviously a brain teaser. That's just for the standard form. A form
constructed with the designer usually has several, if not a whole lot of,
private members that represent the controls used in the form. Buttons,
CheckBoxes, TextBoxes all have their own different properties so how is it
possible to create a single, simple configuration system that can remember the
settings of important items from instance to instance of the code.
Given any two Windows Forms applications, the data for
preserving the state of forms and their components are so different that it may
seem impossible to predict how two different applications might keep a record of
themselves unless we can give the configuration serialization system a hint
about what to keep and what to leave for the defaults.
The answer to this dilemma is to use the .NET system to it's
best advantage and enable configuration through reflection. Using this method,
any object that exposes properties can be provided with a reliable and re-usable
configuration system. Of course, the configuration data stored can be of any
format but by far one of the easiest to understand, edit and store is XML. This
article therefore proposes a universal configuration system that has all these
attributes.
Big hints
A configuration system that saves everything is almost as
useless as no configuration system at-all. We must therefore be able to hint to
the system exactly what we feel to be important and just ignore the rest. Think
about how easy it would be if we could tell an application "Next time you run, I
want you to remember the position and size of the main form and restore the old
settings for me." Think also about the idea of wishing to save the content of a
TextBox that is a child of the form and restore that too. You have to specify
which properties and indeed, which properties of private fields of a running
class should be accessed.
For this task it's possible to rely on the wonderful
Attribute system of .NET to provide us with hints for what should and should not
be maintained from instance to instance of the application. Custom attributes
are ridiculously simple to write and enable us to tag classes, fields,
properties and many other program entities with metadata that can be read by the
reflection system at runtime. Furthermore, the fact that a field is private is
no barrier to obtaining the data from it or altering it's contents using
reflection.
Two attributes have been created for this article. The
ConfigurableAttribute is applicable to fields and enables you to specify
that a field such as the private TextBox member of a form should be considered
for config serialization. The second attribute,
ConfigurablePropertyAttribute, may be applied to a class in order to
specify which of its properties should be saved or to a field that has already
got a ConfigurableAttribute to specify which of the field's properties should be
saved. An example of using these attributes is seen in
this listing.
The attributes that provide this metadata are shown in
this listing.
Upon reflection
When a class and it's controls have been suitably tagged we
can use this metadata to get the information required at runtime. Including, if
necessary, the contents or properties of private fields.
The process is fairly convoluted and goes like this...
The configuration system has a file name and a section name.
This enables you to store more than one section in a single file. To write the
configuration the only data needed is the object who's configuration should be
saved. Configuration discovery is done in two phases. In the first phase, the
fields of the class are checked to see if they have any interesting properties.
To accomplish this, reflection is used to obtain a list of all public and
private members from the object. This list is presented in the form of a
MemberInfo array in response to the GetMembers method
exposed by the System.Type object. Each member can be checked to see if it's a
field, components in a form will be fields, and if the field has been tagged
with a ConfigurableAttribute. If these conditions are satisfied we can check for
the properties that are interesting by checking again for the
ConfigurablePropertyAttributes that may be attached to the field. Each of these
attributes are used to discover and read the contents of the properties which
can be written to the XML file. The second phase revisits the object that was
passed in to discover what properties are to be saved by looking for
ConfigurablePropertyAttributes that may be attached to the class. Each of these
are read and saved in the XML file again. This listing shows
the writing process.
Reading the configuration back from the XML file is very
similar. The discovery process outlined above is used to determine which items
are of interest and the value stored in the XML file is extracted by
constructing an XPath which navigates through the XML to the value desired.
Reflection is used to place the stored value into the destination property.
The reading process is shown in this listing
In both writing and reading of properties the type-converter
is used to convert between the value of the property and it's text
representation. It is therefore possible to extend this system to any custom
property by providing a suitable TypeConverter for it.
Automating the configuration
An application that needs to save it's configuration needs
only to write the configuration when it closes and read it in when it loads.
Furthermore, because the configuration is in XML you can fiddle with it using a
simple text editor, perhaps to create an initial configuration of your choice.
The code page contains the EasyConfig component source and an
example application that maintains it's position and size on screen plus some
settings in it's controls.
You can find the Source Code files here.
Return to the main index.