Enterprise Library Configuration (Part 1/2): Customized Appsettings and the EntLib Configuration Manager tool.
So I've finally gotten comfortable enough with the configuration management tool to post some examples. I'll be doing a 2-part series on the Enterprise Library configuration framework and how you can incorporate your own application configuration into the tool and also customize the configuration offered by your custom frameworks.
I'd also like to point out the posts of Hisham Baz and Scott Densmore who have done some similar work and has a few other walk-throughs on both the Configuration topic and others.
I've included two projects in the downloadable solution. The first is essentially a customized appsettings node that can be edited in the configuration tool. The second is an overly simplified Authorization manager, which shows how to extend the existing frameworks and provide a user-friendly way to edit the configuration data for those extensions.
Update: I missed four lines of code in my example download code (sent an older version to James) and as of 11:51 PM EST on 2/2/2005, I've sent the update to James to get to his site. If you download it before he uploads the update, simply take the code in the blog post for the AppConfigNode.cs file and replace the code in the download with this code. Sorry for the inconvenience.
Update 2: It's now 9:45am on 2/3/2005 and James has updated the download.
My good friend James was nice enough to host the example's source for me, so, grab the code, sit back, and let's take a look.
Customized Application Configuration Nodes
Each customized configuration node requires several classes in order to be completely integrated with the configuration management tool. The first, as explained in the EntLib Quick Starts, is the container class, usually named with a postfix of "Data." This class is the class that is actually serialized to the config file, and is fairly simple.
1 using System;
2 using System.Xml.Serialization;
3 using EntLib = Microsoft.Practices.EnterpriseLibrary.Configuration;
4
5 namespace EntLibConfigDemo {
6 /// <summary>
7 /// This class is the serializable representation of our configuration data.
8 /// </summary>
9 ///
10 [XmlRoot()]
11 public class AppConfigData {
12
13 public AppConfigData() {
14 }
15
16 public AppConfigData(string name) {
17 this._name = name;
18 }
19
20 private string _url;
21
22 /// <summary>
23 /// The URL for the application
24 /// </summary>
25 [XmlElement("Url")]
26 public string Url {
27 get {
28 return _url;
29 }
30 set {
31 _url = value;
32 }
33 }
34
35 private int _maxUsers;
36
37 [XmlElement("MaxUsers")]
38 public int MaxUsers {
39 get {
40 return _maxUsers;
41 }
42 set {
43 _maxUsers = value;
44 }
45 }
46
47 private string _database;
48
49 [XmlElement("Database")]
50 public string Database {
51 get {
52 return _database;
53 }
54 set {
55 _database = value;
56 }
57 }
58
59 private string _name;
60
61 [XmlElement("Name")]
62 public string Name {
63 get {
64 return _name;
65 }
66 set {
67 _name = value;
68 }
69 }
70
71 /// <summary>
72 /// Used to demonstrate the configuration manager's ability to display enums easily
73 /// </summary>
74 public enum AppState {
75 Unknown,
76 Up,
77 Down
78 }
79
80 private AppState _state;
81
82 [XmlElement("State")]
83 public AppState State {
84 get {
85 return _state;
86 }
87 set {
88 _state = value;
89 }
90 }
91 }
92 }
Note that the "State" property is defined by an Enum type, and in the configuration tool you will find that this data is represented by a drop-down list as you would expect. As stated earlier, this class is really just a data container, and the next class, the AppConfigNode, defines how this class is displayed in the configuration tool and what happens when certain events like node renames or removes happen in the tool. These events allow you to utilize other configuration nodes, like the database in this example, and be assured that when the user renames or removes them you can handle those changes appropriately. Here's the source for the AppConfigNode class, which we'll dig into more once you've read over it.
1 using System;
2 using System.ComponentModel;
3 using System.Drawing.Design;
4
5 using Microsoft.Practices.EnterpriseLibrary.Configuration.Design;
6 using Microsoft.Practices.EnterpriseLibrary.Data.Configuration;
7 using Microsoft.Practices.EnterpriseLibrary.Data.Configuration.Design;
8 using Microsoft.Practices.EnterpriseLibrary.Configuration.Design.Validation;
9
10 namespace EntLibConfigDemo {
11 /// <summary>
12 /// AppConfigNode is the configuration-time representation of the AppConfigData class.
13 /// The ServiceDependency attribute informs the configuration manager that this class
14 /// needs to instantialte an ILinkNodeService to make sure that other configuration nodes
15 /// on which this node depends can be found.
16 /// </summary>
17 /// <remarks>
18 /// Apply attributes from the System.ComponentModel namespace to tbe public properties to control their
19 /// behavior in the configuration tool
20 /// </remarks>
21 [ServiceDependency(typeof(ILinkNodeService))]
22 public class AppConfigNode: ConfigurationNode {
23
24 #region Constructors
25
26 public AppConfigNode():this(new AppConfigData("Application Configuration")) {
27 }
28
29 public AppConfigNode(AppConfigData data) {
30 _appConfigData = data;
31 }
32
33 #endregion
34
35 #region Public properties
36
37 private AppConfigData _appConfigData;
38
39 [Browsable(false)]
40 public AppConfigData Data {
41 get {
42 return _appConfigData;
43 }
44 }
45
46 [Category("General"), Description("Url of the system"), Required()]
47 public string Url {
48 get {
49 return _appConfigData.Url;
50 }
51 set {
52 _appConfigData.Url = value;
53 }
54
55 }
56
57 [Category("General"), ReadOnly(true)]
58 public override string Name {
59 get { return base.Name; }
60 set { base.Name = value; }
61 }
62
63 [Category("General"), Description("Maximum Users Allowed")]
64 public int MaxUsers {
65 get {
66 return _appConfigData.MaxUsers;
67 }
68 set {
69 _appConfigData.MaxUsers = value;
70 }
71 }
72
73 private InstanceNode _database;
74
75 [Category("Database"), Description("Database to use")]
76 [Editor(typeof(ReferenceEditor), typeof(UITypeEditor))]
77 [ReferenceType(typeof(InstanceNode))]
78 [Required]
79 public InstanceNode Database {
80 get {
81 return _database;
82 }
83 set {
84 ILinkNodeService service = (ILinkNodeService)GetService(typeof(ILinkNodeService));
85 System.Diagnostics.Debug.Assert(null!=service, "Could not get the ILinkNodeService");
86 this._database = (InstanceNode)service.CreateReference(Database, value, new ConfigurationNodeChangedEventHandler(this.DatabaseRemoved), new ConfigurationNodeChangedEventHandler(this.DatabaseRenamed));
87 this.Data.Database = _database.Name;
88 }
89 }
90
91 [Category("General"), Description("Application state")]
92 public AppConfigData.AppState State {
93 get {
94 return _appConfigData.State;
95 }
96 set {
97 _appConfigData.State = value;
98 }
99 }
100
101 #endregion
102
103 #region Configuration Tool Event Handlers
104
105 private void DatabaseRemoved(object sender, ConfigurationNodeChangedEventArgs e) {
106 this._database = null;
107 }
108
109 private void DatabaseRenamed(object sender, ConfigurationNodeChangedEventArgs e) {
110 this._appConfigData.Database = e.Node.Name;
111 }
112
113 #endregion
114
115 #region Private helper methods
116
117
118 /// <summary>
119 /// Used to create a blank database node if none exist in the application.
120 /// </summary>
121 private void CreateDatabaseSettingsNode() {
122 if (!DatabaseSettingsNodeExists()) {
123 AddConfigurationSectionCommand cmd = new AddConfigurationSectionCommand(Site, typeof(DatabaseSettingsNode), DatabaseSettings.SectionName);
124 cmd.Execute(Hierarchy.RootNode);
125 }
126 }
127
128
129 /// <summary>
130 /// Attempts to find a database settings node in the database and returns if it was successful
131 /// </summary>
132 /// <returns>True if a DatabaseSettingsNode exist, and False if it doesn't.</returns>
133 private bool DatabaseSettingsNodeExists() {
134 DatabaseSettingsNode node = (DatabaseSettingsNode)Hierarchy.FindNodeByType(typeof(DatabaseSettingsNode));
135 return (null!=node);
136 }
137 #endregion
138
139 #region Configuration Manager-related Overrides
140
141 /// <summary>
142 /// This is called by the framework the each time an instance of this node is created from a previous saved state.
143 /// This is used to find any dependent nodes in the configuration file and load their values for the user of this node.
144 /// </summary>
145 public override void ResolveNodeReferences() {
146 DatabaseSettingsNode node = (DatabaseSettingsNode)Hierarchy.FindNodeByType(typeof(DatabaseSettingsNode));
147 System.Diagnostics.Debug.Assert(null!=node, "How is it that the datbase node doesn't exist?");
148 InstanceCollectionNode instCollNode = (InstanceCollectionNode)Hierarchy.FindNodeByType(typeof(InstanceCollectionNode));
149 this.Database = (InstanceNode)Hierarchy.FindNodeByName(instCollNode, this._appConfigData.Database);
150 }
151
152
153 /// <summary>
154 /// This method is called after the main node is created and gives the node an opportunity to add any default child nodes if necessary.
155 /// </summary>
156 protected override void AddDefaultChildNodes() {
157 base.AddDefaultChildNodes ();
158 this.CreateDatabaseSettingsNode();
159 }
160
161 /// <summary>
162 /// This method is called when the configuration node is added to the heirarchy.
163 /// Used in this case to set the name of the node, as it is a single-instance node that cannot be renamed by the user.
164 /// </summary>
165 protected override void OnSited() {
166 base.OnSited ();
167 Site.Name = "Application Configuration";
168 }
169
170
171 /// <summary>
172 /// Called when the node is renamed by the user.
173 /// </summary>
174 /// <param name="e">A <see cref="ConfigurationNodeChangedEventArgs"/> instance, which is used to get the new name of the node.</param>
175 protected override void OnRenamed(ConfigurationNodeChangedEventArgs e) {
176 base.OnRenamed (e);
177 this.Data.Name = e.Node.Name;
178 }
179
180 #endregion
181 }
182 }
There's a lot going on in this class, so we're going to take some time to understand it better.
System.ComponentModel Attributes
The first thing to notice are the attributes attached to the public properties of the class. Most of these attributes are from the System.ComponentModel namespace, and are the same attributes you would use when create a design surface for a custom control. The Browsable(false) attribute tells the designer not to display the attribute in the designer, and the Category(), Description(), Required(), and ReadOnly() attributes should be self-explanatory. Many of the EntLib classes use SRCategory() and SRDescription() attributes, which use the as-yet-unreleased StringResource custom tool to derive their values. Hopefully that will be released to the public soon, because I like the concept of the StringResource tool and the SR* attributes.
One other set of attributes to note are the Editor() attribute on the Database property, which specifies that the run-time configuration tool should use a special control to display the choices for this type. The ReferenceEditor class will filter the nodes based on the type provided, and only display those nodes that exist in the current config file.
Update before posting: Someone has now release a String Resource Tool.
The ILinkNodeService and Referencing Other Node Types
Note that the Database property is of type InstanceNode, and that, when set, we use the ILinkNodeService instance provided by a call to the base class's GetService method to inform the configuration manager that we'd like a reference to that node by calling the CreateReference method. Further, we pass this method a few delegates to handle changes in that node, including removal or renaming of the node. In this way, whenever the user modifies our referenced node, we are informed of the change and can handle it appropriately.
ConfigurationNode Overrides
Our class overrides several members of the ConfigurationNode class to handle certain additional events that happen on all configuration nodes:
- ResolveNodeReferences() - This method is called to provide the ConfigurationNode an opportunity to find it's dependent nodes when it is loaded in the configuration manager. Because our configuration node uses a Database instance node, we take this opportunity to find the referenced node in the configuration heirarchy and set our Database property appropriately. Note that, in theory, the FindNodeByName call could fail if the user manually edits a configuration file, but, in that case, our Database property should just be null and (if it were a required field) would force the user to pick a new node before the file would be saved.
- AddDefaultChildNodes() - This method is called to make sure that, if your configuration node requires another node, you have an opportunity to create it if no node of that type exists. Here, we call a private method that checks to see if any database nodes exist, and, if not, add a new, empty database instance to the configuration. You can demonstrate this if you simply add a new application and then our "Test Application Configuration" node in the configuration manager. You'll notice that the entire Data Access Application Block configuration section is automatically added to the configuration file.
- OnSited() - This is called when your node is added to the configuration file. In our case, we only want one instance of our settings node per application, and we don't want the user to be able to edit the name of the node, so we take this opportunity to set the name of the node in the configuration tree to "Application Configuration."
- OnRenamed() - Called when the user renames the node. Of course, our node is not renamable and, therefore, this method would never truly be called. However, I kept it in the code so you would be aware of its existence and use.
That's about it for the AppConfigNode class. Hopefully you have a better idea of the magic behind the node. Now, we just have to get the configuration manager to know how to serialize/deserialize our class and how we expect users to add it to our configuration file. These tasks are accomplished by the AppConfigManager class:
1 using System;
2 using System.Configuration;
3 using System.Windows.Forms;
4 using Microsoft.Practices.EnterpriseLibrary.Configuration.Design;
5 using Microsoft.Practices.EnterpriseLibrary.Configuration;
6
7
8 namespace EntLibConfigDemo {
9 /// <summary>
10 /// Summary description for AppConfigManager.
11 /// </summary>
12 public class AppConfigManager: IConfigurationDesignManager {
13
14 #region Constructors
15
16 public const string SectionName = "AppConfigData";
17 public AppConfigManager() {
18 // No constructor logic required
19 }
20
21 #endregion
22
23
24 #region IConfigurationDesignManager Members
25
26 /// <summary>
27 /// Registers configuration nodes and commands with the configuration manager
28 /// </summary>
29 /// <param name="serviceProvider"></param>
30 public void Register(IServiceProvider serviceProvider) {
31 // First, register the note type and data type with the node creation service
32 RegisterNodeTypes(serviceProvider);
33
34 // Map the XML section name in the config file to our node type
35 RegisterXmlIncludeTypes(serviceProvider);
36
37 // Create the new "Test Application Configuration" menu item for the application
38 CreateCommands(serviceProvider);
39 }
40
41 /// <summary>
42 /// Adds our data to the configuration dictionary
43 /// </summary>
44 /// <param name="serviceProvider"></param>
45 /// <param name="configurationDictionary"></param>
46 public void BuildContext(IServiceProvider serviceProvider, ConfigurationDictionary configurationDictionary) {
47 AppConfigNode node = GetAppConfigNode(serviceProvider);
48 if (node != null) {
49 AppConfigData settings = node.Data;
50 configurationDictionary[SectionName] = settings;
51 }
52 }
53
54 /// <summary>
55 /// Called when the configuration manager finds an instance of the configuration node in the config file.
56 /// Maps the data from the configuration file to the UI Configuration Node and adds it to the root node.
57 /// </summary>
58 /// <param name="serviceProvider"></param>
59 public void Open(IServiceProvider serviceProvider) {
60 ConfigurationContext configurationContext = ServiceHelper.GetCurrentConfigurationContext(serviceProvider);
61 if (configurationContext.IsValidSection(SectionName)) {
62 AppConfigNode appConfigNode = null;
63 try {
64 AppConfigData data = configurationContext.GetConfiguration(SectionName) as AppConfigData;
65 if (data != null) {
66 // If an instance of our data type was successfully loaded, add our UI node to the root configurationNode
67 appConfigNode = new AppConfigNode(data);
68 ConfigurationNode configurationNode = ServiceHelper.GetCurrentRootNode(serviceProvider);
69 configurationNode.Nodes.Add(appConfigNode);
70 }
71 }
72 catch (ConfigurationException e) {
73 ServiceHelper.LogError(serviceProvider, appConfigNode, e);
74 }
75 }
76
77 }
78
79
80 /// <summary>
81 /// Called when saving the configuration file. Used to map the configuration node to its XML data representation and save the data to disk.
82 /// </summary>
83 /// <param name="serviceProvider"></param>
84 public void Save(IServiceProvider serviceProvider) {
85 ConfigurationContext configurationContext = ServiceHelper.GetCurrentConfigurationContext(serviceProvider);
86 if (configurationContext.IsValidSection(SectionName)) {
87 AppConfigNode appConfigNode = GetAppConfigNode(serviceProvider);
88 AppConfigData data = appConfigNode.Data;
89 if (data != null) {
90 try {
91 configurationContext.WriteConfiguration(SectionName, data);
92 }
93 catch (InvalidOperationException e) {
94 ServiceHelper.LogError(serviceProvider, appConfigNode, e);
95 }
96 }
97 }
98 }
99
100 #endregion
101
102
103 #region Private implementation details
104
105 /// <summary>
106 /// Used to inform the Configuration Manager of our configuration node and data types.
107 /// </summary>
108 /// <param name="serviceProvider"></param>
109 private void RegisterNodeTypes(IServiceProvider serviceProvider) {
110 // Get the node creation service so we can add our node type.
111 INodeCreationService nodeCreationService = ServiceHelper.GetNodeCreationService(serviceProvider);
112 Type nodeType = typeof(AppConfigNode);
113 // Creates a new entry to add to the node creation service. The NoMultiples method creates a new entry that cannot have multiple instances in the same root node.
114 NodeCreationEntry entry = NodeCreationEntry.CreateNodeCreationEntryNoMultiples(new AddChildNodeCommand(serviceProvider, nodeType), nodeType, typeof(AppConfigData), "Application Configuration Data");
115 nodeCreationService.AddNodeCreationEntry(entry);
116 }
117
118 /// <summary>
119 /// Register the relation between our section name and the configuration node used to display it.
120 /// </summary>
121 /// <param name="serviceProvider"></param>
122 private void RegisterXmlIncludeTypes(IServiceProvider serviceProvider) {
123 IXmlIncludeTypeService xmlIts = (IXmlIncludeTypeService)serviceProvider.GetService(typeof(IXmlIncludeTypeService));
124 System.Diagnostics.Debug.Assert(null!=xmlIts, "Could not find IXmlIncludeTypeService");
125 xmlIts.AddXmlIncludeType(SectionName,typeof(AppConfigNode));
126 }
127
128 /// <summary>
129 /// Create new menuitem to allow the user to add an instance of our configuration node.
130 /// </summary>
131 /// <param name="serviceProvider"></param>
132 private static void CreateCommands(IServiceProvider serviceProvider) {
133 IUIHierarchyService hierarchyService = ServiceHelper.GetUIHierarchyService(serviceProvider);
134 IUIHierarchy currentHierarchy = hierarchyService.SelectedHierarchy;
135 bool containsNode = currentHierarchy.ContainsNodeType(typeof(AppConfigNode));
136 IMenuContainerService menuService = ServiceHelper.GetMenuContainerService(serviceProvider);
137 ConfigurationMenuItem item = new ConfigurationMenuItem("Test Application Configuration", new AddConfigurationSectionCommand(serviceProvider, typeof(AppConfigNode), SectionName), ServiceHelper.GetCurrentRootNode(serviceProvider), Shortcut.None, "Test Application Configuration", InsertionPoint.New);
138 item.Enabled = !containsNode;
139 menuService.MenuItems.Add(item);
140 }
141
142 /// <summary>
143 /// Used to find the instance of the AppConfigNode for use in Load and Save methods
144 /// </summary>
145 /// <param name="serviceProvider"></param>
146 /// <returns></returns>
147 private static AppConfigNode GetAppConfigNode(IServiceProvider serviceProvider) {
148 IUIHierarchy hierarchy = ServiceHelper.GetCurrentHierarchy(serviceProvider);
149 if (hierarchy == null) return null;
150
151 return hierarchy.FindNodeByType(typeof(AppConfigNode)) as AppConfigNode;
152 }
153
154 #endregion
155
156 }
157 }
IConfigurationDesignManager Ties It All Together
So you've got your data class, and you've got your node class, but you still can't see it in the configuration manager. Although there are a few tricks left after this class is implemented, they are nothing more than attributes and post-build events. It is the IConfigurationDesignManager interface that lets you register your classes in the configuration manager and create new menu items in the appropriate places for those classes. So, let's dig in:
- Register() - Most of the UI-level hookups happen in this method. We register our Node types, which tells the Configuration Manager which visual classes we support in our assembly. We register our XmlIncludeTypes, which tell the configuration tool how to serialize and deserialize our data classes. And then, we add a new menu command to allow our uses to click on a "Test Application Configuration" menu item to add our new node to their configuration file. These things all happen in the private implementation details section, which, because of its importance to the grand scheme, I will go into in more detail below.
- BuildContext() - Simply adds our data to the configuration dictionary. Even Scott Densmore doesn't think the context is all that interesting.
- Open() - When the configuration manager finds the XML representation of one of our Data classes in the configuration file, it calls the Open method to let our manager map the data class to the appropriate visual node element.
- Save() - When saving the configuration file, this method is called and we write our data back out to the configuration file. If any exceptions are thrown during the write, we log the exception so the configuration manager can display it to the user to fix.
The Guts (Private Implementation Details)
These methods are important as they are what drives the addition of our classes into the config tool in the IConfigurationDesignManager.Register() method, so let's look a little more carefully at these:
- RegisterNodeTypes() - This method retrieves the default instance of the NodeCreationService provided by the ServiceProvider passed to us (which will be implemented by the configuration management tool). Once we have a node creation service, we register our AppConfigNode type as available for use, and mapping our AppConfigNode and our AppConfigData types together.
- RegisterXmlIncludeTypes() - Similar to the RegisterNodeTypes method, we retrieve an instance of the XmlIncludeTypeService provided by the configuration manager and register our AppConfigData type to be mapped to the given section name.
- CreateCommands() - Get the UI Heirarchy service, find the currently selected node in the heirarchy, and check to see if we already have an instance of our node (the containsNode variable). We'll use this information later to enable or disable our menu item based on this result. Then, get the IMenuContainerService instance and add a new menu command to the New menu (InsertionPoint.New) at the root node level (ServiceHelper.GetCurrentRootNode()) that will execute the AddConfigurationSectionCommand.ExecuteCore() method to add our config section, using the display text "Test Application Configuration" and enabled/disabled based on if we have our node in the configuration already (containsNode).
What's left? (Answer: Attributes and Build Events)
Two things remain to get your assembly registered with the UI:
- Add an attribute to your AssemblyInfo.cs file that tells the configuration manager UI which class(es) in your assembly implement the IConfigurationDesignManager interface. The relevant sections of the file are:
-
1 using System.Reflection;
2 using System.Runtime.CompilerServices;
3 using Microsoft.Practices.EnterpriseLibrary.Configuration.Design;
...
60 [assembly: ConfigurationDesignManager(typeof(EntLibConfigDemo.AppConfigManager))]
-
- Push the DLL and any required reference DLLs to the EntLib\Bin directory (usually C:\Program Files\Microsoft Enterprise Library\Bin) so that the configuration manager can find them when it's loaded. As I develop a new configuration management implementation, I tend to like to be able to easily debug these classes so I also push the PDB files. If you run into issues, just add a call to System.Diagnostics.Debugger.Launch() in your code and then, when the configuration tool loads your assembly and fires off the relevant method, you can force the debugger to attach to the configuration manager and find your error. The file copy is accomplished using a post-build event on the project (mine is completely generic except for the location of EntLib, and so these two lines can be added to pretty much any project to move the target DLL to the correct directory):
-
copy $(TargetPath) "C:\Program Files\Microsoft Enterprise Library\bin"
copy $(ProjectDir)$(OutDir)*.pdb "C:\Program Files\Microsoft Enterprise Library\bin"
-
So that walks you through everything you need to know to add a single-instance configuration node to the root of your configuration file and allow your end-user to edit it easily in the configuration manager. OK, so it's nowhere close to everything, but I believe this should give you enough understanding to go "code spelunking" on your own.
Good luck. The next installment will cover extending existing frameworks with your own implementations, which require custom configuration sections of their own to be integrated with the tool. I'll use an AuthorizationProvider as an example, and you'll see that, once you grok the tool, it's not difficult to add your own configuration sections anywhere you need them.