All EntLib versions come with source code, which is a great place to start digging around and understanding the framework. For configuration, since we are planning to write a custom configuration section, we will need to look at the Configuration and Configuration.Design sub namespaces as each block has them. For example, look for Microsoft.Practices.EnterpriseLibrary.Logging.Configuration and Microsoft.Practices.EnterpriseLibrary.Logging.Configuration.Design in the Logging block. Given all the overwhelming number of types defined in those namespaces, it can seem daunting first to tackle this.
It's actually not too difficult. I will illustrate the approach by just doing a custom configuration section with simple attributes, instead of a complicated configuration schema with nested hierarchies.
Consider an example where we need to connect to an external web service, which needs a configuration section with URL, User ID and Password.
1) First of all we will need to create a type that corresponds to the section in the configuration XML file. Note that this type extends the EntLib's SerializableConfigurationSection. All we need to do in this type is defining the section name and attribute names. And of course we need to define read/write properties for those attributes to read/persist from the configuration file.
public class FooSettings : SerializableConfigurationSection
{
// Defines the section name
public const string SectionName = "FooConfiguration";
// Defines attributes for the section
private const string URL_ATTR = "url";
private const string UID_ATTR = "userID";
private const string PASSWORD_ATTR = "password";
[ConfigurationProperty(URL_ATTR, IsRequired = true)]
public string Url
{
get { return (string)base[URL_ATTR]; }
set { base[URL_ATTR] = value; }
}
[ConfigurationProperty(UID_ATTR, IsRequired = true)]
public string UID
{
get { return (string)base[UID_ATTR]; }
set { base[UID_ATTR] = value; }
}
[ConfigurationProperty(PASSWORD_ATTR, IsRequired = true)]
public string Password
{
get { return (string)base[PASSWORD_ATTR]; }
set { base[PASSWORD_ATTR] = value; }
}
}
2) Secondly, we will need to create a type that represents the node on the EntLib console. This type extends the EntLib's ConfigurationSectionNode. Note that It's very similar to the FooSettings type defined above, in that there are three read/write properties that correspond to the Url, UID and Password.
[Image(typeof(FooNode), "ConfigNode_d.bmp")]
[SelectedImage(typeof(FooNode), "ConfigNode_h.bmp")]
public class FooNode : ConfigurationSectionNode
{
private string _url;
private string _uid;
private string _password;
public FooNode()
: base("Foo Configuration")
{}
[ReadOnly(true)]
public override string Name
{
get { return base.Name; }
}
[Required()]
[Browsable(true)]
public string Url
{
get { return _url; }
set { _url = value; }
}
[Required()]
[Browsable(true)]
public string UID
{
get { return _uid; }
set { _uid = value; }
}
[Required()]
[Browsable(true)]
public string Password
{
get { return _password; }
set { _password = value; }
}
}
What is interesting here is that this class as well as its propeties have some attributes attached to them. The SelectedImage() and Image() attributes simply define the bitmap images shown when the node is in selected or deselected state. The two bitmap images are embedded resources within the same VS.NET project. The Required() and Browsable() attributes at the properties are telling the EntLib UI that those properties are required and browsable(visible) on the UI. If you don't put a value for the property, a UI validation error will occur when you try to save the configuration.
3) So far so good. Now we need to have a way to let EntLib UI become aware of our custom configuration node. Here is where we need to extend EntLib's ConfigurationDesignManager to have our FooConifgurationDesignManager.
public class FooConfigurationDesignManager : ConfigurationDesignManager
{
public override void Register(IServiceProvider serviceProvider)
{
(new FooCommandRegistrar(serviceProvider)).Register();
}
protected override void OpenCore(IServiceProvider serviceProvider,
ConfigurationApplicationNode rootNode,
ConfigurationSection section)
{
if (section != null)
{
FooSettings settings = section as FooSettings;
Debug.Assert(settings != null,
"Check config section - not of the FooSettings type");
FooNode node = new FooNode();
// sync data on UI and in config file
node.Url = settings.Url;
node.UID = settings.UID;
node.Password = settings.Password;
SetProtectionProvider(section, node);
rootNode.AddNode(node);
}
}
protected override ConfigurationSectionInfo GetConfigurationSectionInfo(
IServiceProvider serviceProvider)
{
ConfigurationNode rootNode = ServiceHelper.GetCurrentRootNode(serviceProvider);
FooNode node = null;
if (rootNode != null)
node = (FooNode)rootNode.Hierarchy.FindNodeByType(rootNode, typeof(FooNode));
FooSettings settings = null;
if (node == null)
{
settings = null;
}
else
{
settings = new FooSettings();
// sync data on UI and in config file
settings.Url = node.Url;
settings.UID = node.UID;
settings.Password = node.Password;
}
string protectionProviderName = GetProtectionProviderName(node);
return new ConfigurationSectionInfo(node,
settings, FooSettings.SectionName, protectionProviderName);
}
}
The two overridden methods OpenCore() and GetConfigurationSectionInfo() are the hooks for EntLib UI to load and save from/to configuration file. You will notice the code in the two methods to sync up data held by FooNode and FooSettings - just remember that FooNode represents the UI view and FooSettings represents the configuration file. The remaining function, Register(), is the hook for us to provide the command in the EntLib context menu. Here we will use the following CommandRegistrar class to add the command "Foo Configuration":
public class FooCommandRegistrar : CommandRegistrar
{
public FooCommandRegistrar(IServiceProvider serviceProvider)
: base(serviceProvider)
{
}
public override void Register()
{
ConfigurationUICommand cmd = ConfigurationUICommand.CreateSingleUICommand(ServiceProvider,
"Foo Configuration",
"Foo Configuration",
new AddChildNodeCommand(ServiceProvider, typeof(FooNode)),
typeof(FooNode));
AddUICommand(cmd, typeof(ConfigurationApplicationNode));
}
}
With all these in place, we need to add the following to the AssemblyInfo.cs file:
[assembly: ConfigurationDesignManager(typeof(FooConfigurationDesignManager))]
EntLib UI, when being loaded, will use reflection API to query the assemblies in its folder ("c:\Program Files\Microsoft Enterprise Library 3.1 - May 2007\Bin\" in my case) and find out all the ConfigurationDesignManager types through this assembly attribute. So after the project is compiled, we need to drop the dll into the same folder as the EntLibConfig.exe file.
Whew, now we can see how this works in the EntLib UI. The following figure shows that the "Foo Configuration" command is in the context menu.
One cool feature of EntLib 3.1 configuration is that it works integrated with VS.NET 2005 IDE. The following figure shows just that.
You see that a Foo Configuration is created besides the standard logging application block. In the properties window on the lower-right corner, the values for Url, UID and Password are set. I have also defined two environments, QA and Prod. For QA, override is chosen for the foo configuration and you can also see that it's a different set of values. Note that you can encrypt this section with the standard providers. If you have gone through the code I listed above, you will realize that these two functions are provided by EntLib without us writing any code!
Here is what the app.config file looks like (I have ommitted the logging section for clarity):
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="FooConfiguration" type="CustomEntLibConfig.FooSettings, CustomEntLibConfig, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</configSections>
<FooConfiguration url="http://foo.com/test.asmx" userID="test"
password="test123" />
</configuration>
In case you're curious, here is what the QA delta file looks like.
<configuration>
<configSections>
<section name="EnvironmentMergeData" type="Microsoft.Practices.EnterpriseLibrary.Configuration.EnvironmentalOverrides.Configuration.EnvironmentMergeSection, Microsoft.Practices.EnterpriseLibrary.Configuration.EnvironmentalOverrides, Version=3.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
</configSections>
<EnvironmentMergeData environmentName="QA" environmentDeltaFile="app.qa">
<mergeElements>
<override nodePath="/Foo Configuration" overrideProperties="true">
<overridddenProperties>
<add name="Password" value="qa123" />
<add name="UID" value="qa" />
<add name="Url" value="http://qa.foo.com/test.asmx" />
</overridddenProperties>
</override>
</mergeElements>
</EnvironmentMergeData>
</configuration>
4) To use the configuration programmatically, here is a code snippet:
FooSettings node = ConfigurationManager.GetSection(FooSettings.SectionName) as FooSettings;
if (node != null)
Console.WriteLine("Url: {0}, UID: {1}, Password: {2}",
node.Url, node.UID, node.Password);
else
Console.WriteLine("Custom section not found");
Console.ReadLine();
I hope you find this helpful. Email me at hugh dot ang @ gmail dot com if you need the source code for the sample I have used here.
2 comments:
Keep the good stuff coming!
Very helpful.
Now, how do I get items to serialize as elements instead of attributes?
Time for a bit more of a hunt :)
Post a Comment