Friday, July 25, 2008

More Visual Studio 2008 App Configuration

Much simpler than generating C# code, the class I use to build the XML elements for the .CONFIG file is shown below. Most of the methods take a WinForms TreeView object or some part of it as an input parameter to be able to traverse its nodes and create the XMLElements with the needed Attributes :


using System;
using System.Text;
using System.Xml;
using System.Windows.Forms;
using System.IO;

namespace CustomConfigBuilder
{
public class ConfigXMLBuilder
{
private void appendChildConfigAttributes(XmlDocument doc, XmlElement element, TreeNode node)
{
foreach (TreeNode childNode in node.Nodes)
if (childNode.Tag.ToString() == Shared._attributeImageIndex.ToString())
{
XmlAttribute attr = doc.CreateAttribute(childNode.Text);
element.Attributes.Append(attr);
}
}

private void appendChildConfigElements(XmlDocument document,
XmlElement parentXmlElement,
TreeNode parentTreeNode)
{
foreach (TreeNode node in parentTreeNode.Nodes)
if (node.Tag.ToString() == Shared._elementImageIndex.ToString())
{
if (!Shared.HasChildAttributes(node))
continue;

XmlElement element = document.CreateElement(node.Text + "s");
parentXmlElement.AppendChild(element);

XmlElement elementAdd = document.CreateElement("Add");
element.AppendChild(elementAdd);

appendChildConfigElements(document, elementAdd, node);
}

appendChildConfigAttributes(document, parentXmlElement, parentTreeNode);
}

///
/// Create the actual XML configuration Text for the Web/App Config file.
///

public string BuildXMLConfigDocument(TreeView tvConfigData, string SaveXmlTo)
{
XmlDocument docConfig = new XmlDocument();
docConfig.LoadXml("<" +
tvConfigData.Nodes[Shared._rootConfigNodeName].Text +
"> tvConfigData.Nodes[Shared._rootConfigNodeName].Text + ">");

appendChildConfigElements(docConfig,
docConfig.DocumentElement,
tvConfigData.Nodes[Shared._rootConfigNodeName]);

try
{
if (SaveXmlTo.Length > 0 && (!SaveXmlTo.ToUpper().Equals("SAVE XML TO:")))
docConfig.Save(SaveXmlTo);
}
catch (System.Exception exception)
{
MessageBox.Show(exception.Message);
}

StringBuilder sb = new StringBuilder();
StringWriter sw = new StringWriter(sb);
docConfig.Save(sw);

// txtXMLConfigResults.Text = sb.ToString();

return sb.ToString();
}


}
}

Visual Studio .Net Configuration Tool

I had occasion to have to author some configuration settings at a client and struggled at first with the nesting of the data and getting it out at runtime in an elegant way. The .Net Framework 2.0 has a much improved set of classes for interfacing complex configurations, but it can take a few minutes to learn them all, so I wrote a tool for for the whole operation which uses CodeDom to generate the classes and simple XMLDocument operations for the config file contents.

While I found a ton of tutorials online for how to write it all by hand, nothing automated. Attached are some screenshots of the tool in use :








So once compiled into the target application, the developer can get at the settings by calling the generated method with this signature:

public static MyConfigSection GetSettings()

Here is the whole generated set of classes :



namespace MyApp.NameSpace {
using System.Configuration;
using System;


public class MyConfigSection : System.Configuration.ConfigurationSection {

[System.Configuration.ConfigurationProperty("Teams")]
public virtual TeamCollection Teams {
get {
return ((TeamCollection)(this["Teams"]));
}
}

static MyConfigSection GetSettings() {
return ((MyConfigSection)(System.Configuration.ConfigurationManager.GetSection(MyConfigSection)));
}
}

public class Team : System.Configuration.ConfigurationElement {

[System.Configuration.ConfigurationProperty("Name")]
public virtual string Name {
get {
return ((string)(this["Name"]));
}
}

[System.Configuration.ConfigurationProperty("Players")]
public virtual PlayerCollection Players {
get {
return ((PlayerCollection)(this["Players"]));
}
}
}

public class TeamCollection : System.Configuration.ConfigurationElementCollection {

protected override string ElementName {
get {
return "Team";
}
}

protected override System.Configuration.ConfigurationElement CreateNewElement() {
return new Team();
}

protected override object GetElementKey(System.Configuration.ConfigurationElement element) {
return ((Team)(element)).Name;
}
}

public class Player : System.Configuration.ConfigurationElement {

[System.Configuration.ConfigurationProperty("Name")]
public virtual string Name {
get {
return ((string)(this["Name"]));
}
}

[System.Configuration.ConfigurationProperty("Position")]
public virtual string Position {
get {
return ((string)(this["Position"]));
}
}
}

public class PlayerCollection : System.Configuration.ConfigurationElementCollection {

protected override string ElementName {
get {
return "Player";
}
}

protected override System.Configuration.ConfigurationElement CreateNewElement() {
return new Player();
}

protected override object GetElementKey(System.Configuration.ConfigurationElement element) {
return ((Player)(element)).Name;
}
}
}


Here is the Class where the CodeDom namespace objects create the config classes:

using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Configuration;
using System.Text;
using System.Windows;
using System.Windows.Forms;
using Microsoft.CSharp;
using System.IO;

namespace CustomConfigBuilder
{
public static class Shared
{
public const int _elementImageIndex = 1;
public const int _attributeImageIndex = 0;
public const string _rootConfigNodeName = "RootConfigNode";

public static bool HasChildAttributes(TreeNode node)
{
foreach (TreeNode childNode in node.Nodes)
if (childNode.Tag.ToString() == Shared._attributeImageIndex.ToString())
return true;

return false;
}
}

public class configClassBuilder
{

#region "Methods for building classes"
private static void addBaseGetCollectionIndexerFunction(string name, CodeTypeDeclaration parent)
{
CodeMemberProperty propertyIndexer = new CodeMemberProperty();
propertyIndexer.HasGet = true;
propertyIndexer.HasSet = false;
propertyIndexer.Attributes = MemberAttributes.Public;
propertyIndexer.Type = new CodeTypeReference(name);
propertyIndexer.Name = "Item";
propertyIndexer.Parameters.Add(
new CodeParameterDeclarationExpression(
new CodeTypeReference(typeof(int)), "index"));

CodeMethodReferenceExpression baseGet =
new CodeMethodReferenceExpression(new CodeThisReferenceExpression(), "BaseGet");

propertyIndexer.GetStatements.Add(
new CodeMethodReturnStatement(
new CodeCastExpression(
new CodeTypeReference(name),
new CodeMethodInvokeExpression(
baseGet,
new CodeSnippetExpression[] { new CodeSnippetExpression("index") }))));
parent.Members.Add(propertyIndexer);
}


private void addConfigurationElementsCollection(CodeNamespace codeNS,
string name,
CodeTypeDeclaration parent,
CodeTypeDeclaration itemInCollection)
{
CodeTypeReference configPropertyReference =
new CodeTypeReference(typeof(ConfigurationProperty));

// For a collection
CodeTypeDeclaration configElements = new CodeTypeDeclaration(name + "Collection");
configElements.BaseTypes.Add(typeof(ConfigurationElementCollection));
codeNS.Types.Add(configElements);

// Make the Collection a property of the Config Section.
CodeMemberProperty property = new CodeMemberProperty();
property.Name = name + "s";
property.Type = new CodeTypeReference(configElements.Name);
property.HasGet = true;
property.HasSet = false;
property.Attributes = MemberAttributes.Public;
CodeAttributeArgument codeAttributeArgument = new CodeAttributeArgument();
codeAttributeArgument.Value = new CodeSnippetExpression("\"" + name + "s\"");
property.CustomAttributes.Add(
new CodeAttributeDeclaration(configPropertyReference,
new CodeAttributeArgument[] { codeAttributeArgument }));
// Add the elements collection to whatever parent section type is.
parent.Members.Add(property);

CodeIndexerExpression indexer =
new CodeIndexerExpression(new CodeThisReferenceExpression(),
new CodeExpression[] { new CodeSnippetExpression("\"" + name + "s\"") });

CodeCastExpression castIndexer =
new CodeCastExpression(new CodeTypeReference(configElements.Name), indexer);

property.GetStatements.Add(new CodeMethodReturnStatement(castIndexer));

addCreateNewElementMethod(name, configElements);

// protected override object GetElementKey(ConfigurationElement element)
CodeMemberMethod methodGetKey = new CodeMemberMethod();
methodGetKey.Attributes = MemberAttributes.Override | MemberAttributes.Family;
methodGetKey.Name = "GetElementKey";
methodGetKey.Parameters.Add(
new CodeParameterDeclarationExpression(
new CodeTypeReference(typeof(ConfigurationElement)), "element"));
methodGetKey.ReturnType = new CodeTypeReference(typeof(object));
CodeCastExpression castReturn =
new CodeCastExpression(new CodeTypeReference(itemInCollection.Name),
new CodeVariableReferenceExpression("element"));
CodePropertyReferenceExpression propertyOfItem =
new CodePropertyReferenceExpression(castReturn,
itemInCollection.Members[0].Name);

methodGetKey.Statements.Add(new CodeMethodReturnStatement(propertyOfItem));
configElements.Members.Add(methodGetKey);

// Add the "this" indexer from the Base Collection.
addBaseGetCollectionIndexerFunction(name, parent);

// Properties of the Collection
addElementNameProperty(name, configElements);
}


private void addFieldsToElement(TreeNode treeNode, CodeTypeDeclaration elementDataType)
{
foreach (TreeNode attribute in treeNode.Nodes)
if (attribute.Tag.ToString() == Shared._attributeImageIndex.ToString())
addAttributeToElement(elementDataType, attribute.Text);
}

private void addImportsToNamespace(CodeNamespace ns)
{
CodeNamespaceImport configNS = new CodeNamespaceImport("System.Configuration");
ns.Imports.Add(configNS);
configNS = new CodeNamespaceImport("System");
ns.Imports.Add(configNS);
}

private void addClientSettingsAccessor(CodeTypeDeclaration settingsType)
{
CodeMemberMethod settingsGetter = new CodeMemberMethod();
settingsGetter.Name = "GetSettings";
settingsGetter.ReturnType = new CodeTypeReference(settingsType.Name);
settingsGetter.Attributes = MemberAttributes.Static | MemberAttributes.Public ;

CodeMethodInvokeExpression invokeGet =
new CodeMethodInvokeExpression(new CodeTypeReferenceExpression(typeof(ConfigurationManager)),
"GetSection",
new CodeSnippetExpression[] { new CodeSnippetExpression(settingsType.Name) });

CodeCastExpression castReturn =
new CodeCastExpression(new CodeTypeReference(settingsType.Name), invokeGet);

settingsGetter.Statements.Add(new CodeMethodReturnStatement(castReturn));

settingsType.Members.Add(settingsGetter);
}

private void addAttributeToElement(CodeTypeDeclaration elementType, string name)
{
CodeTypeReference configPropertyReference =
new CodeTypeReference(typeof(ConfigurationProperty));

CodeMemberProperty property = new CodeMemberProperty();
property.Name = name;
property.Type = new CodeTypeReference(typeof(string));
property.HasGet = true;
property.HasSet = false;
property.Attributes = MemberAttributes.Public;

CodeIndexerExpression propertyIndexer =
new CodeIndexerExpression(new CodeThisReferenceExpression(),
new CodeExpression[] { new CodeSnippetExpression("\"" + name + "\"") });

CodeCastExpression castIndexer =
new CodeCastExpression(new CodeTypeReference(typeof(string)), propertyIndexer);

CodeMethodReturnStatement returnStatement = new CodeMethodReturnStatement(castIndexer);

property.GetStatements.Add(returnStatement);

CodeAttributeArgument codeAttributeArgument = new CodeAttributeArgument();
// codeAttributeArgument.Name = "Name";
codeAttributeArgument.Value = new CodeSnippetExpression("\"" + name + "\"");

property.CustomAttributes.Add(
new CodeAttributeDeclaration(configPropertyReference,
new CodeAttributeArgument[] { codeAttributeArgument }));

elementType.Members.Add(property);
}

private void addCreateNewElementMethod(string name, CodeTypeDeclaration configElements)
{
CodeMemberMethod methodCreate = new CodeMemberMethod();
methodCreate.ReturnType = new CodeTypeReference(typeof(ConfigurationElement));
methodCreate.Name = "CreateNewElement";
methodCreate.Attributes = MemberAttributes.Override | MemberAttributes.Family;
methodCreate.Statements.Add(
new CodeMethodReturnStatement(
new CodeObjectCreateExpression(name, new CodeExpression[] { })));
configElements.Members.Add(methodCreate);
}

private static void addElementNameProperty(string name, CodeTypeDeclaration configElements)
{
CodeMemberProperty propEleName = new CodeMemberProperty();
propEleName.Name = "ElementName";
propEleName.Attributes = MemberAttributes.Override | MemberAttributes.Family;
propEleName.HasSet = false;
propEleName.HasGet = true;
propEleName.Type = new CodeTypeReference(typeof(string));
propEleName.GetStatements.Add(new CodeMethodReturnStatement(
new CodeSnippetExpression("\"" + name + "\"")));
configElements.Members.Add(propEleName);
}

private CodeTypeDeclaration addConfigurationElement(CodeNamespace codeNS, string name)
{
// For an item in the collection
CodeTypeDeclaration configElement = new CodeTypeDeclaration(name);
configElement.BaseTypes.Add(typeof(ConfigurationElement));
codeNS.Types.Add(configElement);
return configElement;
}

public string RunBuild(TreeView tvConfigData, string SaveTo)
{
CodeNamespace configgedNamespace = new CodeNamespace("MyApp.NameSpace");

CodeTypeDeclaration configSectionDec = new CodeTypeDeclaration(tvConfigData.Nodes[0].Text);
configSectionDec.IsClass = true;

configSectionDec.BaseTypes.Add(typeof(ConfigurationSection));

configgedNamespace.Types.Add(configSectionDec);

addImportsToNamespace(configgedNamespace);

foreach (TreeNode tn in tvConfigData.Nodes[Shared._rootConfigNodeName].Nodes)
{
CodeTypeDeclaration newElement = addConfigurationElement(configgedNamespace, tn.Text);

// Add Fields to the new configuration element.
addFieldsToElement(tn, newElement);
// Add a collection class to hold all the instances of the element.
addConfigurationElementsCollection(configgedNamespace, tn.Text, configSectionDec, newElement);

addClientSettingsAccessor(configSectionDec);

// Add Any Child Elements to the new configuration element.
foreach (TreeNode childNode in tn.Nodes)
if (childNode.Tag.ToString() == Shared._elementImageIndex.ToString())
{
CodeTypeDeclaration newChildElement =
addConfigurationElement(configgedNamespace, childNode.Text);

addFieldsToElement(childNode, newChildElement);

addConfigurationElementsCollection(configgedNamespace,
childNode.Text,
newElement,
newChildElement);
}
}

CSharpCodeProvider provider = new CSharpCodeProvider();

System.CodeDom.Compiler.CodeGeneratorOptions opts =
new System.CodeDom.Compiler.CodeGeneratorOptions();

if (SaveTo.Length > 0 && !SaveTo.Contains("Save "))
{
StreamWriter sw = new StreamWriter(SaveTo);
opts.BlankLinesBetweenMembers = true;
provider.GenerateCodeFromNamespace(configgedNamespace, sw, opts);
sw.Close();
}

StringBuilder sb = new StringBuilder();
StringWriter strWriter = new StringWriter(sb);
provider.GenerateCodeFromNamespace(configgedNamespace, strWriter, opts);
// txtClassResults.Text = sb.ToString();
strWriter.Close();
return sb.ToString();
}


#endregion
}
}


And here is the declaration of the new configuration Type in the App.Config ;




<configuration>
<configSections>
<section name="MyConfigSection" type="MyApp.NameSpace.MyConfigSection, CustomConfigBuilder" />
</configSections>

<MyConfigSection>
<Teams>
<add Name="Dallas Cowboys">
<Players>
<add Name="Terrell Owens" Position="Wide Receiver" />
</Players>
</add>
</Teams>
</MyConfigSection>
</configuration>