This is almost not worth a post but I was really disappointed with the content I found on the subject. The problem is a simple one. How to Parse a value from an XML document using an XPath.
The basic formula is simple, but in its current structure it will be a little heavyweight if you are making a lot of calls.
string parsedValue;
IXPathNavigable documentXml = aNewDocument;
string xPath = "\Document\Item\@name";
XPathNavigator navigator = documentXml.CreateNavigator();
XPathExpression expression = navigator.Compile(xPath);
XmlNamespaceManager manager = new XmlNamespaceManager(navigator.NameTable);
expression.SetContext(manager);
XPathNodeIterator iterator = navigator.Select(expression);
if (iterator.Count > 0)
{
iterator.MoveNext();
parsedValue = iterator.Current.Value;
}
Although this will do the trick I needed something a bit more elegant so I could extend it to provide more specialized features. Since I needed much of this same code in many different call patterns I decided to use a delegate pattern. There are two/three parts, the caller, optional typed implementation, and the root parser. Lets look at the new structure of the root parser. This parses the document and signals the caller as to whether or not there is a value to be retrieved
[DebuggerHidden]
private void ExecuteXPathOperation(IXPathNavigable documentXml, string xPath, Action func)
{
try
{
XPathNavigator navigator = documentXml.CreateNavigator();
XPathExpression expression = navigator.Compile(xPath);
XmlNamespaceManager manager = new XmlNamespaceManager(navigator.NameTable);
expression.SetContext(manager);
XPathNodeIterator iterator = navigator.Select(expression);
bool hasValue = false;
if (iterator.Count > 0)
{
hasValue = true;
iterator.MoveNext();
}
func(iterator, hasValue);
}
catch (Exception ex)
{
}
}
Note: The attribute [DebuggerHidden] is not really required but once you step through all this a few times, you will be ready to skip it. This will make that automatic.
Now that we have the root parser we need a Typed wrapper to parse values for a given type when the root parser signals a value is present. Here are a few examples:
String:
[DebuggerHidden]
private string ParseXmlStringValue(IXPathNavigable documentXml, string xPath)
{
string parsedValue = null;
ExecuteXPathOperation(documentXml, xPath, (iterator, hasValue) =>
{
if (hasValue)
parsedValue = iterator.Current.Value;
});
return parsedValue;
}
Numbers:
[DebuggerHidden]
private decimal ParseXmlNumericValue(IXPathNavigable documentXml, string xPath, decimal defaultValue)
{
string parsedValue = ParseXmlStringValue(documentXml, xPath);
decimal returnValue;
if (string.IsNullOrEmpty(parsedValue))
{
returnValue = defaultValue;
}
else
{
if (!decimal.TryParse(parsedValue, out returnValue))
returnValue = defaultValue;
}
return returnValue;
}
Uri/Url:
[DebuggerHidden]
private Uri ParseXmlUri(IXPathNavigable documentXml, string xPath)
{
Uri address = null;
string url = ParseXmlStringValue(documentXml, xPath);
if (!string.IsNullOrEmpty(url))
{
Uri.TryCreate(url, UriKind.Absolute, out address);
}
return address;
}
DateTime:
[DebuggerHidden]
private DateTime ParseXmlDateTime(IXPathNavigable documentXml, string xPath)
{
DateTime parsedValue = new DateTime();
ExecuteXPathOperation(documentXml, xPath, (iterator, hasValue) =>
{
if (hasValue)
parsedValue = DateTime.Parse(iterator.Current.Value);
});
return parsedValue;
}
As you can see these can build on each other or call on the root parser. Now we can use a very simple call to get a value. All the areas of concern have been seperated so this should be easy to maintain, extend, and test. You could easily refactor these to be extension methods off IXPathNavigable and depending on the call patterns you could refactor it to cache compiled XPathExpression objects and reuse them.
public DateTime ParseDocumentTimeStamp(IXPathNavigable documentXml)
{
const string xPath = "/Document/@timestamp";
return ParseXmlDateTime(documentXml, xPath);
}
Hope this helps. Enjoy.
Tags: xml, design patterns