Introduction
This is a technique I devised recently because I got tired of writing similar code for working with similar XML files and having to retrofit new functionality into older applications as my needs evolve. It can also help to improve standardization of XML that you create -- for instance, always following a particular schema when writing a DataTable
out to XML.
It assists with creating, parsing, and updating XML and can be used with sections of an XML document, whereas other techniques (e.g. serialization) may not be as flexible. I haven't tried to use it with a Microsoft-centric config file (I've never used one).
Background
I like XML, especially attributes, and I prefer to create my own configuration files -- for a variety of reasons, mostly because that's just the kind of guy I am. There are situations that are common to a number of applications and I feel that it is better to have a common configuration layout for such situations rather than to define a different one for each application. One such situation is the desire to persist the size and position of an application's Forms in the configuration when a Form closes and restore it when the Form is loaded again. Working with XML to do such things also requires checking to see whether or not the desired elements and values exist and often the parsing of string
values to other datatype
s.
Another facet of working with XML is that of creating the schema when it doesn't yet exist; constantly having to check for null
and creating elements and attributes as required.
After writing the code to do this and then copying-and-pasting that code a few too many times, I decided that there had to be a better way.
Solution
The primary tasks I wanted to accomplish were to encapsulate ensuring that the desired elements and values exist in the XML before I try to access them and the parsing of values to the appropriate datatype
so I can work with the XML in as type-safe a manner as I can.
Obviously, it doesn't take a rocket scientist to see that an XmlElement
can be wrapped in a class with a property for each attribute. Consider the following elements:
<Name />
<Name></Name>
<Name>InnerText</Name>
Every XmlNode
has a name and some content (which may be an empty string
). They are accessed by the Name
and InnerText
properties (at least when using the classes in .NET's System.Xml
namespace, other frameworks may have other terms). Therefore, I defined the IXmlNode
interface:
public interface IXmlNode
{
string NodeName { get ; }
string InnerText { get ; set ; }
}
Note that the property here is NodeName
to indicate that it is the name of the node; this could be thought of as the type of the XmlElement
. In most cases, I also have a Name
attribute to differentiate between nodes of the same type, so I extended that interface to:
public interface INamedXmlNode : IXmlNode
{
string Name { get ; set ; }
}
An example of an XmlElement
that I use is:
<Window Name="dlgEdit" Top="100" Left="100" Width="1000"
Height="800" WindowState="Normal" />
What I want to do is wrap this XmlElement
in a class so that I can easily access the attributes and content in a type-safe manner.
XmlNodeAttribute
I decided that I wanted to have an Attribute
to mark which properties of a class should be backed by the XmlNode
. Having the attribute provide a default value for missing XmlAttributes
is also very handy so I derived it from DefaultValueAttribute
.
[System.AttributeUsageAttribute(System.AttributeTargets.Property ,
AllowMultiple=false , Inherited=false)]
public sealed partial class XmlNodeAttribute : System.ComponentModel.DefaultValueAttribute
{
public XmlNodeAttribute
(
)
: base
(
null
)
{
return ;
}
public XmlNodeAttribute
(
object DefaultValue
)
: base
(
DefaultValue
)
{
return ;
}
}
Foreshadowing
Here is a snippet from WindowNode.cs (included in the zip file) that shows how properties can be written so that they are backed by the wrapped XmlNode
:
public sealed partial class WindowNode : PIEBALD.Types.NamedXmlNode
{
[PIEBALD.Types.XmlNodeAttribute("")]
public int
Top
{
get
{
return ( this.GetValue<int> ( "Top" ) ) ;
}
set
{
this.SetValue<int> ( "Top" , value ) ;
return ;
}
}
[PIEBALD.Types.XmlNodeAttribute(System.Windows.Forms.FormWindowState.Normal)]
public System.Windows.Forms.FormWindowState
WindowState
{
get
{
return ( this.GetValue<System.Windows.Forms.FormWindowState>
( "WindowState" ) ) ;
}
set
{
this.SetValue<System.Windows.Forms.FormWindowState>
( "WindowState" , value ) ;
return ;
}
}
}
The property is written in terms of the desired datatype
and the underlying code handles the conversion to and from the string
s in the wrapped XmlNode
. The name of the property must match the name of the Attribute
of the wrapped XmlNode
.
XmlNode
XmlNode
is the class that provides the basic functionality for derived classes. It holds references to the wrapped XmlNode
, the XmlDocument
, and any parsers it will need.
public partial class XmlNode : IXmlNode
{
protected readonly System.Xml.XmlDocument document ;
protected readonly System.Xml.XmlNode node ;
private readonly System.Collections.Generic.Dictionary
<string,PIEBALD.Types.Parsomatic.ParseDelegate> dic =
new System.Collections.Generic.Dictionary
<string,PIEBALD.Types.Parsomatic.ParseDelegate>() ;
}
Constructor
There are a number of overloaded constructors, but I'll only discuss the primary one here, the others all call this one. The only required parameter is the XmlDocument
. If the Node
is null
, the Document
itself will be used. If the Path
has a value, then it will be used to select the desired descendent element to wrap. Any elements specified by Path
that do not exist in the document will be created.
public XmlNode
(
System.Xml.XmlDocument Document
,
System.Xml.XmlNode Node
,
string Path
)
{
if ( Document == null )
{
throw ( new System.ArgumentNullException
( "Document" , "You must supply an XmlDocument" ) ) ;
}
this.document = Document ;
if ( Node == null )
{
Node = this.document ;
}
if ( !System.String.IsNullOrEmpty ( Path ) )
{
foreach
(
string name
in
Path.Split ( '/' )
)
{
System.Xml.XmlNode temp = Node.SelectSingleNode ( name ) ;
if ( temp == null )
{
temp = this.document.CreateElement ( name ) ;
Node.AppendChild ( temp ) ;
}
Node = temp ;
}
}
this.node = Node ;
this.Initialize() ;
return ;
}
XmlNodeDemo1.cs demonstrates this constructor:
System.Xml.XmlDocument doc = new System.Xml.XmlDocument() ;
PIEBALD.Types.IXmlNode nod = new PIEBALD.Types.XmlNode
(
doc
,
null
,
"XmlNodeDemo1/DemoNodes/Node"
) ;
nod.InnerText = "XmlNodeDemo1" ;
System.Console.WriteLine ( doc.OuterXml ) ;
The output is:
<XmlNodeDemo1><DemoNodes><Node>XmlNodeDemo1</Node></DemoNodes></XmlNodeDemo1>
XmlNodeDemo2.cs demonstrates a simple derived class with two properties; the output is:
<XmlNodeDemo2><DemoNodes>
<Node DemoIntValue="" DemoBoolValue="False">
XmlNodeDemo2</Node></DemoNodes></XmlNodeDemo2>
The important thing to notice is that the XML and default values were created automatically, based on the given Path
.
Initialize
The Initialize
method iterates the properties of the class, looking for any with an XmlNodeAttribute
. If a matching attribute doesn't yet exist in the XmlNode
, then an attempt to create one with the default value is made. If there is no default value, then an Exception
is thrown. A parser for each property will also be added to the dictionary of parsers.
protected virtual void
Initialize
(
)
{
foreach
(
System.Reflection.PropertyInfo pi
in
this.GetType().GetProperties
(
System.Reflection.BindingFlags.Instance
|
System.Reflection.BindingFlags.Public
)
)
{
object[] atts = pi.GetCustomAttributes
( typeof(PIEBALD.Types.XmlNodeAttribute) , false ) ;
if ( atts.Length == 1 )
{
if ( this.node.Attributes [ pi.Name ] == null )
{
object value = ((PIEBALD.Types.XmlNodeAttribute) atts [ 0 ]).Value ;
if ( value == null )
{
throw ( new System.ArgumentNullException
( pi.Name , "This attribute requires a value" ) ) ;
}
System.Xml.XmlAttribute att = this.document.CreateAttribute ( pi.Name ) ;
att.Value = value.ToString() ;
this.node.Attributes.Append ( att ) ;
}
this.dic [ pi.Name ] = PIEBALD.Types.Parsomatic.Parser ( pi.PropertyType ) ;
}
}
return ;
}
If a class requires a parser that is not already in the Parsomatic, then one needs to be added. This can be done in a static
constructor, such as the one for WindowNode
:
static WindowNode
(
)
{
PIEBALD.Types.Parsomatic.AddType
( System.Windows.Forms.FormWindowState.Normal ) ;
return ;
}
See my Parsomatic[^] article if you have any questions.
GetValue and SetValue
XmlNode
provides these two methods to ease the burden of converting between string
and the desired type of the property. GetValue
accesses the parser associated with the attribute and SetValue
merely performs a ToString
. (SetValue
needn't be generic, but having it match GetValue
adds regularity to the interface.)
protected virtual T
GetValue<T>
(
string AttributeName
)
{
return ( (T) this.dic [ AttributeName ]
(
this.node.Attributes [ AttributeName ].Value
) ) ;
}
protected virtual void
SetValue<T>
(
string AttributeName
,
T Value
)
{
this.node.Attributes [ AttributeName ].Value = Value.ToString() ;
return ;
}
NodeName and InnerText
These methods simply access the properties of the wrapped XmlNode
. Note that NodeName
is readonly.
public virtual string
NodeName
{
get
{
return ( this.node.Name ) ;
}
}
public virtual string
InnerText
{
get
{
return ( this.node.InnerText ) ;
}
set
{
this.node.InnerText = value ;
return ;
}
}
NamedXmlNode
NamedXmlNode
extends XmlNode
by adding a Name
property. This is an example of what I was saying about standardizing how I layout XML -- if an XmlElement
needs a name, it gets a Name
attribute, not an ID
attribute or a Name
element or any other way of doing it.
[PIEBALD.Types.XmlNodeAttribute("")]
public virtual string
Name
{
get
{
return ( this.GetValue<string> ( "Name" ) ) ;
}
set
{
this.SetValue<string> ( "Name" , value ) ;
return ;
}
}
XmlNodeDemo3.cs demonstrates a NamedXmlNode
, the output is:
<XmlNodeDemo3><DemoNodes><Node Name="XmlNodeDemo3">XmlNodeDemo3</Node>
</DemoNodes></XmlNodeDemo3>
XmlNodeCollection<T>
Often I have a number of Elements
of the same type, e.g. DataRows
in a DataTable
, and they should be represented by a collection; this class accomplishes that.
Obviously, the class must hold the members of the collection -- I'm using a List<T>
for that. And to wrap each member before I add it to the collection, I need a constructor for class T
.
The following static
constructor attempts to get the required constructor:
public partial class XmlNodeCollection<T> : PIEBALD.Types.XmlNode
where T : PIEBALD.Types.IXmlNode
{
private static readonly System.Reflection.ConstructorInfo constructor ;
static XmlNodeCollection
(
)
{
constructor = typeof(T).GetConstructor
(
System.Reflection.BindingFlags.Public
|
System.Reflection.BindingFlags.Instance
,
null
,
new System.Type[] { typeof(System.Xml.XmlDocument) ,
typeof(System.Xml.XmlNode) }
,
null
) ;
if ( constructor == null )
{
throw ( new System.InvalidOperationException
( "No suitable constructor found" ) ) ;
}
return ;
}
private readonly System.Collections.Generic.List<T> nodes =
new System.Collections.Generic.List<T>() ;
}
Constructor, Initialize, and Add
Because XmlNodeCollection IS_AN XmlNode
, the constructor(s) will call the base constructor(s). Then any existing member elements with the specified NodeName
will be wrapped and added to the collection.
public XmlNodeCollection
(
System.Xml.XmlDocument Document
,
System.Xml.XmlNode Node
,
string Path
,
string Element
)
: base
(
Document
,
Node
,
Path
)
{
this.Initialize ( Element ) ;
return ;
}
protected virtual void
Initialize
(
string Element
)
{
if ( !System.String.IsNullOrEmpty ( Element ) )
{
foreach
(
System.Xml.XmlNode node
in
this.node.SelectNodes ( Element )
)
{
this.Add ( node ) ;
}
}
return ;
}
protected virtual T
Add
(
System.Xml.XmlNode Node
)
{
object[] parms = new object [ 2 ] ;
parms [ 0 ] = this.document ;
parms [ 1 ] = Node ;
return ( this.Add ( (T) constructor.Invoke ( parms ) ) ) ;
}
protected virtual T
Add
(
T Node
)
{
this.nodes.Add ( Node ) ;
this.Count = this.nodes.Count ;
return ( Node ) ;
}
Count, indexer, and Nodes
Count
is a property which is backed by a Count
attribute of the wrapped XmlNode
. The indexer allows accessing collection members by index. Nodes returns a readonly copy of the List<T>
.
[PIEBALD.Types.XmlNodeAttribute(0)]
public int
Count
{
get
{
return ( this.GetValue<int> ( "Count" ) ) ;
}
private set
{
this.SetValue<int> ( "Count" , value ) ;
return ;
}
}
public virtual T
this
[
int Index
]
{
get
{
return ( this.nodes [ Index ] ) ;
}
}
public System.Collections.Generic.IList<T>
Nodes
{
get
{
return ( this.nodes.AsReadOnly() ) ;
}
}
XmlNodeDemo4.cs demonstrates simple use of XmlNodeCollection
:
System.Xml.XmlDocument doc = new System.Xml.XmlDocument() ;
PIEBALD.Types.XmlNodeCollection<PIEBALD.Types.XmlNode> nod =
new PIEBALD.Types.XmlNodeCollection<PIEBALD.Types.XmlNode>
(
doc
,
null
,
"XmlNodeDemo4/DemoNodes"
,
"Node"
) ;
System.Console.WriteLine ( doc.OuterXml ) ;
nod.Add ( "Member" ) ;
nod.Add ( "Member" ) ;
nod.Add ( "Member" ) ;
System.Console.WriteLine ( doc.OuterXml ) ;
Its output is:
<XmlNodeDemo4><DemoNodes Count="0" /></XmlNodeDemo4>
<XmlNodeDemo4><DemoNodes Count="3"><Member /><Member />
<Member /></DemoNodes></XmlNodeDemo4>
NamedXmlNodeCollection<T>
NamedXmlNodeCollection<T>
is very similar to XmlNodeCollection<T>
except for that it collects INamedXmlNodes
in a Dictionary<string,T>
.
public partial class NamedXmlNodeCollection<T> : PIEBALD.Types.XmlNode
where T : PIEBALD.Types.INamedXmlNode
{
private static readonly System.Reflection.ConstructorInfo constructor ;
static NamedXmlNodeCollection
(
)
{
constructor = typeof(T).GetConstructor
(
System.Reflection.BindingFlags.Public
|
System.Reflection.BindingFlags.Instance
,
null
,
new System.Type[] { typeof(System.Xml.XmlDocument) ,
typeof(System.Xml.XmlNode) }
,
null
) ;
if ( constructor == null )
{
throw ( new System.InvalidOperationException
( "No suitable constructor found" ) ) ;
}
return ;
}
private readonly System.Collections.Generic.Dictionary<string,T> nodes =
new System.Collections.Generic.Dictionary<string,T>
(
System.StringComparer.CurrentCultureIgnoreCase
) ;
}
Constructor, CheckItems, Initialize, and Add
These are very similar to the ones for XmlNodeCollection
except for the addition of CheckItems
, which ensures that members with the provided names (Items
) exist before proceeding.
public NamedXmlNodeCollection
(
System.Xml.XmlDocument Document
,
System.Xml.XmlNode Node
,
string Path
,
string Element
,
params string[] Items
)
: base
(
Document
,
Node
,
Path
)
{
this.CheckItems ( Element , Items ) ;
this.Initialize ( Element ) ;
return ;
}
protected virtual void
CheckItems
(
string Element
,
string[] Items
)
{
if
(
!System.String.IsNullOrEmpty ( Element )
&&
( Items != null )
&&
( Items.Length > 0 )
)
{
System.Collections.Generic.HashSet<string> names =
new System.Collections.Generic.HashSet<string>() ;
foreach
(
System.Xml.XmlNode nod
in
this.node.SelectNodes ( Element )
)
{
if ( nod.Attributes [ "Name" ] != null )
{
names.Add ( nod.Attributes [ "Name" ].Value ) ;
}
}
foreach
(
string item
in
Items
)
{
if
(
!System.String.IsNullOrEmpty ( item )
&&
!names.Contains ( item )
)
{
System.Xml.XmlNode nod = this.document.CreateElement ( Element ) ;
System.Xml.XmlAttribute att = this.document.CreateAttribute ( "Name" ) ;
att.Value = item ;
nod.Attributes.Append ( att ) ;
this.node.AppendChild ( nod ) ;
}
}
}
return ;
}
protected virtual void
Initialize
(
string Element
)
{
if ( !System.String.IsNullOrEmpty ( Element ) )
{
foreach
(
System.Xml.XmlNode node
in
this.node.SelectNodes ( Element )
)
{
this.Add ( node ) ;
}
}
return ;
}
protected virtual T
Add
(
System.Xml.XmlNode Node
)
{
object[] parms = new object [ 2 ] ;
parms [ 0 ] = this.document ;
parms [ 1 ] = Node ;
return ( this.Add ( (T) constructor.Invoke ( parms ) ) ) ;
}
protected virtual T
Add
(
T Node
)
{
this.nodes [ Node.Name ] = Node ;
this.Count = this.nodes.Count ;
return ( Node ) ;
}
Count, indexer, Names, and Nodes
Count
is the same. This indexer uses the Name
of the member rather than the index. Names
returns the collection of member Names. Nodes
returns the members in the collection.
[PIEBALD.Types.XmlNodeAttribute(0)]
public int
Count
{
get
{
return ( this.GetValue<int> ( "Count" ) ) ;
}
private set
{
this.SetValue<int> ( "Count" , value ) ;
return ;
}
}
public virtual T
this
[
string Name
]
{
get
{
return ( this.nodes [ Name ] ) ;
}
}
public System.Collections.Generic.IEnumerable<string />
Names
{
get
{
return ( this.nodes.Keys ) ;
}
}
public System.Collections.Generic.IEnumerable<T>
Nodes
{
get
{
return ( this.nodes.Values ) ;
}
}
XmlNodeDemo5.cs demonstrates simple use of NamedXmlNodeCollection
:
System.Xml.XmlDocument doc = new System.Xml.XmlDocument() ;
PIEBALD.Types.NamedXmlNodeCollection<PIEBALD.Types.NamedXmlNode> nod =
new PIEBALD.Types.NamedXmlNodeCollection<PIEBALD.Types.NamedXmlNode>
(
doc
,
null
,
"XmlNodeDemo5/DemoNodes"
,
"Node"
,
"Member1"
,
"Member2"
,
"Member3"
) ;
System.Console.WriteLine ( doc.OuterXml ) ;
Its output is:
<XmlNodeDemo5><DemoNodes Count="3"><Node Name="Member1" />
<Node Name="Member2" /><Node Name="Member3" /></DemoNodes></XmlNodeDemo5>
Putting It All Together: XmlNodeDemo6
XmlNodeDemo6
demonstrates how this technique can be used to simplify persisting and restoring a Form
's window size and position.
This is a very simple WinForms
application that persists its window size and position in an XML file when it closes and, if the file exists at startup, restores the persisted values. If the file doesn't exist at startup, then the proper nodes are created with default values.
System.Xml.XmlDocument doc = new System.Xml.XmlDocument() ;
System.IO.FileInfo fi = new System.IO.FileInfo ( "XmlNodeDemo6.xml" ) ;
if ( fi.Exists )
{
doc.Load ( fi.FullName ) ;
}
PIEBALD.Types.NamedXmlNodeCollection<PIEBALD.Types.WindowNode> nod =
new PIEBALD.Types.NamedXmlNodeCollection<PIEBALD.Types.WindowNode>
(
doc
,
null
,
"XmlNodeDemo6/Windows"
,
"Window"
,
"frmXmlNodeDemo6"
) ;
System.Windows.Forms.Application.Run
(
new frmXmlNodeDemo6.frmXmlNodeDemo6()
{
WindowNode = nod [ "frmXmlNodeDemo6" ]
}
) ;
System.IO.File.WriteAllText ( fi.FullName , doc.OuterXml ) ;
(Just in case you're wondering, this is not the way I usually read and write XML files.)
The important thing to notice is that the actual WinForm
code doesn't need to be altered to add this functionality; I use an implant[^].
WindowNode.Implant
This code adds the ability to persist and restore a Form
's window size and position to XML without altering the rest of the WinForms
code. Adding this ability is as simple as implanting this code and then setting the resulting WindowNode
property of the target Form
(as shown above).
The implant contains the following field and property:
protected PIEBALD.Types.WindowNode windownode = null ;
public virtual PIEBALD.Types.WindowNode
WindowNode
{
set
{
this.FormClosing -= this.PersistWindow ;
this.Load -= this.RestoreWindow ;
if ( ( this.windownode = value ) != null )
{
this.FormClosing += this.PersistWindow ;
this.Load += this.RestoreWindow ;
}
return ;
}
}
RestoreWindow
This method attempts to retrieve the values from the XmlNode
when the form loads, but if an Exception
is encountered (usually due to a parsing failure), then the current value will be retained and persisted.
protected virtual void
RestoreWindow
(
object sender
,
System.EventArgs e
)
{
if ( this.windownode != null )
{
try
{
this.Top = this.windownode.Top ;
}
catch
{
this.windownode.Top = this.Top ;
}
try
{
this.WindowState = this.windownode.WindowState ;
}
catch
{
this.windownode.WindowState = this.WindowState ;
}
if ( this.WindowState == System.Windows.Forms.FormWindowState.Minimized )
{
this.WindowState = System.Windows.Forms.FormWindowState.Normal ;
}
}
return ;
}
PersistWindow
This method persists the form's current WindowState
to the XmlNode
and, if the state is normal, also persists the size and position:
protected virtual void
PersistWindow
(
object sender
,
System.EventArgs e
)
{
if ( this.windownode != null )
{
if ( ( this.windownode.WindowState = this.WindowState ) ==
System.Windows.Forms.FormWindowState.Normal )
{
this.windownode.Top = this.Top ;
this.windownode.Left = this.Left ;
this.windownode.Width = this.Width ;
this.windownode.Height = this.Height ;
}
}
return ;
}
The resultant XML is:
<XmlNodeDemo6><Windows Count="1"><Window Name="frmXmlNodeDemo6"
Top="348" Left="490" Width="300" Height="300" WindowState="Normal" />
</Windows></XmlNodeDemo6>
(Actual values may differ.)
You can move and resize the form and see the results in XmlNodeDemo6.xml after exiting the application.
Compare That to TheOldWay.txt
In the zip file, I have included the _Load
and _Closing
methods from my RegexTester
as an example of one way I used to enable persisting a Form
's size and position.
Notice that for that application, I use a boolean Maximized
attribute, this is an example of non-standard behaviour that I want to eliminate. There are also two enumerations in use that I store as int
s -- using my new technique I can persist them properly.
More Advanced Technique
I demonstrate the use of a family of classes that define a more-complex structure -- a DataTableNode
, which contains a collection of ExtendedPropertyNodes
, a collection of DataColumnNodes
, and a collection of DataRowNodes
. A DataRowNode
contains a collection of DataFieldNodes
. Both programs produce the following (whitespace added for clarity):
<DataTables>
<DataTable Database="" Name="TestTable">
<ExtendedProperties Count="1">
<ExtendedProperty Name="Timestamp" IsNull="False" DataType="System.DateTime"
Align="Unknown">2011-05-14 11:04:56</ExtendedProperty>
</ExtendedProperties>
<DataColumns Count="2">
<DataColumn Name="Col1" IsKey="False" MaxWidth="8" IsNull="False"
DataType="System.String" Align="Left">Column 1</DataColumn>
<DataColumn Name="Col2" IsKey="False" MaxWidth="8" IsNull="False"
DataType="System.Int32" Align="Right">Column 2</DataColumn>
</DataColumns>
<DataRows Count="1">
<DataRow>
<DataFields Count="2">
<DataField Name="Col1" IsNull="False" DataType="System.String"
Align="Left">Answer</DataField>
<DataField Name="Col2" IsNull="False" DataType="System.Int32"
Align="Right">42</DataField>
</DataFields>
</DataRow>
</DataRows>
</DataTable>
</DataTables>
This is not how I usually create a DataTable
in XML, but I do have an application that reads the definitions (but not data) of several DataTables
with these classes.
You can run XmlNodeDemo7
or XmlNodeDemo8
and redirect the output to XmlNodeDemo9.xml, then when you run XmlNodeDemo9
, it will attempt to read and display the above XML-encoded DataTable
.
TestTable
Timestamp = 2011-05-14 11:04:56
Column 1 Column 2
-------- --------
Answer 42
Transsubstantiate
These classes are why I wrote my Transsubstantiate [^] utility. The constructors for NamedXmlNode
and several of the other classes don't add anything of substance, so I don't want to have to write or copy-and-paste them.
I included the files created by Transsubstantiate
and Implant in case you don't want to create them yourself. If you supply the "full
" parameter to build.bat, it will remove the supplied files and attempt to recreate them.
In Closing
I haven't worked with this extensively yet, so there are likely still some efficiency and robustness issues. Suggestions for improvements are welcome.
History
- 2011-05-14 First submitted