Customizing the XML Schema for Your Data

The predefined XML schema for configuration files fits the bill in most cases, but when you have complex and structured information to preserve across application sessions, none of the existing schemas appear to be powerful enough. At this point, you have two possible workarounds. You can simply avoid using a standard configuration file and instead use a plain XML file written according to the schema that you feel is appropriate for the data. Alternatively, you can embed your XML configuration data in the standard application configuration file but provide a tailor-made configuration section handler to read it. A third option exists. You could insert the data in the configuration file, register the section with a null handler (IgnoreSectionHandler), and then use another piece of code (for example, a custom utility) to read and write the settings.

Before we look more closely at designing and writing a custom configuration handler according to the XML schema you prefer, let’s briefly compare the various approaches. In terms of performance and programming power, all approaches are roughly equivalent, but some key differences still exist. In theory, using an ad hoc file results in the most efficient approach because you can create made-to-measure, and subsequently faster, code. However, this is only a possibility—if your code happens to be badly written, the performance of your whole application might still be bad. The System.Configuration classes are designed to serve as a general-purpose mechanism for manipulating settings. They work great on average but are not necessarily the best option when an effective manipulation of the settings is key to your code. On the other hand, the System.Configuration classes, and the standard configuration files, require you to write a minimal amount of code. The more customization you want, the more code you have to write, with all the risks (mostly errors and bugs) that this introduces.

As a rule of thumb, using the standard configuration files should be the first option to evaluate. Resort to custom files only if you want to control all aspects of data reading (for example, if you want to provide feedback while loading), if performance is critical, or if you just don’t feel comfortable with the predefined section handlers. Finally, although it’s reasonable to use the IgnoreSectionHandler handler in the context in which the .NET Framework uses it, I don’t recommend using IgnoreSectionHandler in user applications. A custom section handler or a custom file is preferable.

If you’re considering creating a custom file based on a customized XML schema, DataSet objects present an interesting option. Assuming that the data to be stored lends itself to being represented in a tabular format, you could write an XML configuration file using the Microsoft ADO.NET normal form and load that data into a DataSet object. Loading data requires a single call to the ReadXml method, and managing data is easy due to the powerful interface of the DataSet class. We’ll look at an example of the DataSet section handler next.

Note

In the section “Customizing Attribute Names,” on page 645, we analyzed a custom section handler inherited from the NameValueSectionHandler class. That trivial handler was simply aimed at overriding some of the standard features of one of the predefined handlers. A truly custom section handler is a more sophisticated object that uses an XML reader to access a portion of the configuration file and parse the contents.


Creating a DataSet Section Handler

Let’s look at a practical example of a new section handler named DatasetSectionHandler. This section handler reads XML data from a configuration file and stores it in a new DataSet object. The data must be laid out in a format that the ReadXml method can successfully process. The typical format is the ADO.NET normal form that we examined in Chapter 9.

Along with the custom section handler, let’s write an application that can handle configuration data through a DataSet object. Suppose you have a Windows Forms application that can be extended with plug-in modules. We won’t look at the details of how this could be done here; instead, we’ll focus on how to effectively store configuration data as XML. (In the section “Further Reading,” on page 655, you’ll find a reference to a recent article that addresses this topic fully.) We’ll analyze the plug-in engine for Windows Forms applications only, but the same pattern can be easily applied to Web Forms applications as well.

Extending Windows Forms Application Menus

The sample application shown in Figure 15-2 allows users to add custom menu items below the first item on the Tools menu. Such menu items are linked to external plug-in modules. In this context, a plug-in module is simply a class dynamically loaded from an assembly. More generally, the plug-in class will need to implement a particular interface, or inherit from a given base class, because the application needs to have a consistent way to call into any plug-in class. (For more information and a complete example of extensible .NET Framework applications, check out the article referenced in the section “Further Reading,” on page 655. In our sample application, we’ll limit ourselves to creating a context-sensitive MessageBox call for each new registered plug-in.

Figure 15-2. A Windows Forms application that can be extended with plug-in modules that integrate with the menu.


At loading, the sample application calls the following routine to set up the menu:

private void SetupMenu()
{
   // Access the menu config file 
   string path = "TypicalWinFormsApp/PlugIns";
   DataSet configMenu = (DataSet) ConfigurationSettings.GetConfig(path);
         
   // Add dynamic items to existing popup menus
   if (configMenu != null)
      AddMenuToolsPlugIns(configMenu);
}

The configuration settings—that is, the menu items to be added to the Tools menu—are read from the configuration file using the ConfigurationSettings class, as usual. Nothing in the preceding code reveals the presence of a custom section handler and a completely custom XML schema for the settings. The only faint clue is the use of a DataSet object.

After it has been successfully loaded from the configuration file, the DataSet object is passed to a helper routine, AddMenuToolsPlugIns, which will modify the menu. We’ll return to this point in the section “Invoking Plug-In Modules,” on page 650; in the meantime, let’s review the layout of the configuration file.

The XML Layout of the Configuration Settings

The data corresponding to plug-in modules is stored in a section group named TypicalWinFormsApp. The actual section is named PlugIns. Each plug-in module is identified by an assembly name, a class name, and display text. The display text constitutes the caption of the menu item, whereas the assembly name and the class name provide for a dynamic method call. As mentioned, in a real-world scenario, you might force the class to implement a particular interface so that it’s clear to the calling application which methods are available for the object it is instantiating.

Here is a sample configuration file for the application shown in Figure 15-2:

<configuration>
   <configSections>
      <sectionGroup name="TypicalWinFormsApp">
         <section name="PlugIns" 
            type="XmlNet.CS.DatasetSectionHandler, 
               DatasetSectionHandler" />
      </sectionGroup>
   </configSections>
   <appSettings>
      <add key="LastLeftTopPosition" value="358,237" />
      <add key="LastSize" value="472,203" />
   </appSettings>
   <TypicalWinFormsApp>
      <PlugIns>
         <MenuTools>
            <Text>Add new tool...</Text>
            <Assembly>MyToolsPlugIns</Assembly>
            <Class>MyPlugIn.AddNewTool</Class>
         </MenuTools>
         <MenuTools>
            <Text>Special tool...</Text>
            <Assembly>MyToolsPlugIns</Assembly>
            <Class>MyPlugIn.SpecialTool</Class>
         </MenuTools>
      </PlugIns>
   </TypicalWinFormsApp>
</configuration>

I deliberately left a few standard application settings (the <appSettings> section) in this listing just to demonstrate that custom sections can happily work side by side with standard system and application settings. In particular, the sample application depicted in Figure 15-2 also supports the same save and restore features described in the section “Using Settings Through Code,” on page 634.

The <section> element points to the class XmlNet.CS.DatasetSectionHandler, which is declared and implemented in the DatasetSectionHandler assembly. The net effect of this section declaration is that whenever an application asks for a PlugIns section, the preceding section handler is involved, its Create method is called, and a DataSet object is returned. We’ll look at the implementation of the section handler in the section “Implementing the DataSet Section Handler,” on page 653.

Invoking Plug-In Modules

The AddMenuToolsPlugIns procedure modifies the application’s Tools menu, adding all the items registered in the configuration file. The following code shows how it works:

private void AddMenuToolsPlugIns(DataSet ds)
{
   DynamicMenuItem mnuItem;
   DataTable config; 

   // Get the table that represents the settings for the menu
   config = ds.Tables["MenuTools"];
   if (config == null)
      return; 

   // Add a separator
   if (config.Rows.Count >0)
      menuTools.MenuItems.Add("-");

   // Start position for insertions
   int index = menuTools.MenuItems.Count;

   // Populate the Tools menu
   foreach(DataRow configMenuItem in config.Rows)
   {
      mnuItem = new DynamicMenuItem(configMenuItem["Text"].ToString(), 
         new EventHandler(StdOnClickHandler));
      mnuItem.AssemblyName = configMenuItem["Assembly"].ToString();
      mnuItem.ClassName = configMenuItem["Class"].ToString();
      menuTools.MenuItems.Add(index, mnuItem);
      index += 1;
   }
}

The DataSet object that the section handler returns is built from the XML code rooted in <PlugIns>. This code originates a DataSet object with one table, named MenuTools. The MenuTools table has three columns: Text, Assembly, and Class. Each row in the table corresponds to a plug-in module.

The preceding code first adds a separator and then iterates on the rows of the table and adds menu items to the Tools menu, as shown in Figure 15-3. MenuTools is just the name of the Tools pop-up menu in the sample application.

Figure 15-3. Registered plug-in modules appear on the Tools menu of the application.


To handle a click on a menu item in a Windows Forms application, you need to associate an event handler object with the menu item. Visual Studio .NET does this for you at design time for static menu items. For dynamic items, this association must be established at run time, as shown here:

DynamicMenuItem mnuItem;
mnuItem = new DynamicMenuItem(
   configMenuItem["Text"].ToString(), 
   new EventHandler(StdOnClickHandler));

A menu item is normally represented by an instance of the MenuItem class. What is that DynamicMenuItem class all about then? DynamicMenuItem is a user-defined class that extends MenuItem with a couple of properties particularly suited for menu items that represent calls to plug-in modules. Here’s the class definition:

public class DynamicMenuItem : MenuItem
{
   public string AssemblyName; 
   public string ClassName;

public DynamicMenuItem(string text, EventHandler onClick) : 
   base(text, onClick)
   {}
}

The new menu item class stores the name of the assembly and the class to use when clicked. An instance of this class is passed to the event handler procedure through the sender argument, as shown here:

private void StdOnClickHandler(object sender, EventArgs e)
{
   // Get the current instance of the dynamic menu item
   DynamicMenuItem mnuItem = (DynamicMenuItem) sender;

   // Display a message box that proves we know the corresponding 
   // assembly and class name
   string msg = "Execute a method on class [{0}] from assembly [{1}]";
   msg = String.Format(msg, mnuItem.ClassName, mnuItem.AssemblyName);
   MessageBox.Show(msg, mnuItem.Text);
}

In a real-world context, you can use the assembly and class information to dynamically create an instance of the class using the Activator object that we encountered in Chapter 12, as follows:

// Assuming that the class implements the IAppPlugIn interface 
// asm is the assembly name, cls is the class name
IAppPlugIn o = (IAppPlugIn) Activator.CreateInstance(asm, cls).Unwrap()

// Assume that the IAppPlugIn interface has a method Execute()
o.Execute();

Figure 15-4 shows the message box that appears when you click a custom menu item in the sample application. All the information displayed is read from the configuration file.

Figure 15-4. The message box that appears when a custom menu item is clicked.


Implementing the DataSet Section Handler

To top off our examination of section handlers, let’s review the source code for the custom section handler that we’ve been using, shown here:

using System;
using System.Data;
using System.Xml;
using System.Configuration;

namespace XmlNet.CS
{
   public class DatasetSectionHandler : IConfigurationSectionHandler
   {
      // Constructor(s)
      public DatasetSectionHandler()
      {
      }

      // IConfigurationSectionHandler.Create
      public object Create(object parent, 
         object context, XmlNode section) 
      {
         DataSet ds;

         // Clone the parent DataSet if not null
         if (parent == null)
            ds = new DataSet(); 
         else
            ds = ((DataSet) parent).Clone();

         // Read the data using a node reader
         DataSet tmp = new DataSet();
         XmlNodeReader nodereader = new XmlNodeReader(section);
         tmp.ReadXml(nodereader);

         // Merge with the parent and return
         ds.Merge(tmp);
         return ds;
      }
   }
}

The DatasetSectionHandler class implements the IConfigurationSectionHandler and provides the default constructor. The most interesting part of this code is the Create method, which reads the current section specified through the section argument and then merges the resultant DataSet object with the parent, if a non-null parent object has been passed. Because configuration inheritance proceeds from top to bottom, the base DataSet object for merging is the parent.

The XML data to be parsed is passed via an XmlNode object—that is, an object that represents the root of an XML DOM subtree. To make an XML DOM subtree parsable by the DataSet object’s ReadXml method, you must wrap it in an XmlNodeReader object—that is, one of the XML reader objects that we encountered in Chapter 2 and Chapter 5. When called to action on the configuration file from the section “The XML Layout of the Configuration Settings,” on page 649, the XmlNode object passed to the handler points to the <PlugIns> node.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.53.202