I just spent a few hours backfitting my wwAppConfiguration class which is part of the West Wind Web Store, so it works properly with .NET 2.0 and the new config file format that ASP.NET 2.0 uses by default.
Actually ASP.NET 2.0 includes improved configuration section access that supports read and write access, but the wwAppConfiguration class still provides a number of useful features above and beyond what ASP.NET 2.0 provides, the strongest feature (IMHO) being the fact that you can get a strongly typed interface for your configuration settings.
Anyway, even if ASP.NET 2.0 addressed all those needs there’s still the issue of backwards compatibility. A 1.x app upgraded to a 2.x project ends up with the new configuration file format.
Version 2.0 broke the original implementation with something as simple as adding a namespace reference to the ASP.NET web.config file:
<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
This namespace reference is added for VS.NET to be able to provide a configuration schema to web.config editing, but the namespace definitely affects any manual XML parsing anybody might have been doing on the web.config file.
Seems simple enough, but this actually broke any of the code that is not using the ConfigSections manager classes directly. Anything using the XML DOM directly broke based on this innocuous namespace reference addition.
Whenever it comes to dealing with default namespaces in XML documents I feel like a complete idiot because I can never remember where you need to explicitly have to apply the prefix and where it’s not required. I end up going through a bunch of trial and error to get this right.
Is it just me, or shouldn’t a non prefixed namespace automatically be treated like the default namespace without having to explicitly redeclare it? I remember this same issue popping up when MSXML 4.0 came out and broke backwards compatibility with older MSXML parsers for this same logic. The logic is WC3 correct, but that doesn't make it right or logical <g>... Bitch, bitch, bitch...
The problem here is that the XMLDOM parser doesn’t automatically use a default namespace like the one above and push it down into the childnodes of this document. Instead you have to assign the namespace explicitly and assign a namespace prefix to it, then actually use that prefix explicitly on any nodes referenced by XPATH query. If you have existing code runs XPATH queries without namespace references at all (because there weren’t any explicitly assigned namespaces previously) cleaning up this code is quite a mess.
Although no namespace prefix is used, not using a namespace prefix on any XPATH queries results in nodes not being found. So where things like this worked before:
XmlNode Node = Dom.DocumentElement.SelectSingleNode(
ConfigSection + "/" +
"add[@key='" + Field.Name + "']",
null);
you now have to explicitly use a namespace object, and prefix on every element expression. Here’s what this looks like:
XmlDocument Dom = new XmlDocument();
try
{
Dom.Load(Filename);
}
catch
{
// *** Can't open or doesn't exist - so create it
if (!this.WriteKeysToConfig(Filename) )
return;
// *** Now load again
Dom.Load(Filename);
}
// *** Retrieve XML Namespace information to assign default
// *** Namespace explicitly.
this.GetXmlNamespaceInfo(Dom);
XmlNode Node = Dom.DocumentElement.SelectSingleNode(
this.XmlNamespacePrefix + ConfigSection + "/" +
this.XmlNamespacePrefix + "add[@key='" + Field.Name + "']",
XmlNamespaces);
// … do something with the node..
XmlNamespaces is a an XmlNamespacesManager object which picks up all of the namespaces of the XML document. Basically the idea is that you can add the ‘default’ namespace to the existing namespaces and assign a prefix to it. So whenever a DOM is loaded the following code does the trick:
/// <summary>
/// Used to load up the default namespace reference and prefix
/// information. This is required so that SelectSingleNode can
/// find info in 2.0 or later config files that include a namespace
/// on the root element definition.
/// </summary>
/// <param name="Dom"></param>
private void GetXmlNamespaceInfo(XmlDocument Dom)
{
// *** Load up the Namespaces object so we can
// *** reference the appropriate default namespace
if (Dom.DocumentElement.NamespaceURI == null || Dom.DocumentElement.NamespaceURI == "")
{
this.XmlNamespaces = null;
this.XmlNamespacePrefix = "";
}
else
{
if (Dom.DocumentElement.Prefix == null || Dom.DocumentElement.Prefix == "")
this.XmlNamespacePrefix = "ww";
else
this.XmlNamespacePrefix = Dom.DocumentElementPrefix;
XmlNamespaces = new XmlNamespaceManager(Dom.NameTable);
XmlNamespaces.AddNamespace(this.XmlNamespacePrefix,
Dom.DocumentElement.NamespaceURI);
this.XmlNamespacePrefix += ":";
}
}
Ok, my XML may have been rusty but it took quite a while to figure this part out. Maybe there’s an easier way to do this, but I couldn’t get this to work any other way.
Note that I have to deal with the two scenarios of where there is a namespace provided and where there isn’t separately. The consistency of how this works in the DOM really sucks. Luckily this wrapper takes care of the logistics since null for the namespace manager and empty for the prefix work fine. The other alternative would have been to attach a namespace explicitly.
Once this code was in place then there was still the job left of going through the document and cleaning up all the references and making sure that the namespace manager is used and the prefix applied to all element references.
Adding nodes in turn DOESN’T require the namespace prefix, but it does require the namespace reference. Here’s what the code looks like to create a new configuration section:
private XmlNode CreateConfigSection(XmlDocument Dom,string ConfigSection)
{
// *** Create the actual section first and attach to document
XmlNode AppSettingsNode = Dom.CreateNode(XmlNodeType.Element,
ConfigSection,Dom.DocumentElement.NamespaceURI);
XmlNode Parent = Dom.DocumentElement.AppendChild(AppSettingsNode);
// *** Now check and make sure that the section header exists
if (ConfigSection != "appSettings")
{
XmlNode ConfigSectionHeader = Dom.DocumentElement.SelectSingleNode(this.XmlNamespacePrefix + "configSections",
this.XmlNamespaces);
if (ConfigSectionHeader == null)
{
// *** Create the node and attributes and write it
XmlNode ConfigSectionNode = Dom.CreateNode(XmlNodeType.Element,
"configSections",Dom.DocumentElement.NamespaceURI);
// *** Insert as first element in DOM
ConfigSectionHeader = Dom.DocumentElement.InsertBefore(ConfigSectionNode,
Dom.DocumentElement.ChildNodes[0]);
}
// *** Check for the Section
XmlNode Section = ConfigSectionHeader.SelectSingleNode(this.XmlNamespacePrefix + "section[@name='" + ConfigSection + "']",
this.XmlNamespaces);
if (Section == null)
{
Section = Dom.CreateNode(XmlNodeType.Element,"section",
null);
XmlAttribute Attr =Dom.CreateAttribute("name");
Attr.Value = ConfigSection;
XmlAttribute Attr2 = Dom.CreateAttribute("type");
Attr2.Value = "System.Configuration.NameValueSectionHandler,System,Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
Section.Attributes.Append(Attr);
Section.Attributes.Append(Attr2);
ConfigSectionHeader.AppendChild(Section);
}
}
return Parent;
}
Note that reading requires the namespace prefix, but writing does not. Note that that each of the write operations writes the document’s namespace into the new nodes.
I doubt anybody will find this post very interesting, but I’m posting it for my own sake, so I can find it again in a few months the next time I run into a namespace issues with XML documents. <g>
Other Posts you might also like