Thursday, September 25, 2008

XmlContentLoader

As a subject of my first technical post I've chosen a small utility class built on the top of Linq. I have a pleasure to work with this really useful and powerful .Net Framework 3.5 feature which is Linq. It's quite possible that in next few posts I'll be writing about many issues related to Linq (LinqToXml, new C#3.0 in action and many others...)

Few weeks ago my team got a task to build very simple wizard application based on Windows Forms. The main goal of the wizard was to free end-users from doing configuration our applications by editing configuration files by hand. It was really straightforward, 4 or 5 step wizard basically giving ability to set some connection strings, email server settings and some WCF stuff. My part of the task was to prepare a component responsible for loading and saving selected xml elements or attributes from configuration file. As you can imagine the file was pretty big but we needed to change only some parts of them.

I decided to build more general-purpose component which I called XmlContentLoader. Firstly I'll show you sample scenario of using it, than some interesting parts of the realization.

Assume than we have xml file looking like this:

<Root>
   <A>some string...</A>
   <AA>
       <AAA>yo</AAA>
   </AA>
   <B>true</B>
   <BB></BB>
   <C>
       <C1 c1Attribute="some string..." >1</C1>
   </C>
   <D dAttribute="2008.02.02">
   </D>
</Root>

Now we want to load some parts of the file into memory, change it and save back to file. Assume than we want to write as little code as possible.

So we create the class (or structure) responsible for holding data from xml file (please note that the class can already exist in our application) containing properties corresponding to appropriate xml file items (elements or attributes). The xml item is chosen by defining xpath query returning xml element and optional attribute name (in case of choosing xml attribute).

internal class XmlContentLoaderTest
{
   [XmlItem(@"/Root/A[1]")]
   [XmlItem(@"/Root/C/C1", "c1Attribute")]
   public string MyString { get; set; }

   [XmlItem(@"/Root/B[1]")]
   public bool MyBool { get; set; }

   [XmlItem(@"/Root/D[1]", "dAttribute")]
   public DateTime MyDateTime { get; set; }

   [XmlItem(@"/Root/C[1]/C1[1]")]
   public Decimal MyDecimal { get; set; }
}

As you can see above one property can be mapped to many xml items. Once we have the data structure, we can load, modify and save xml file content.

string filePath = "...";
var a = XmlContentLoader.Load<XmlContentLoaderTest>(filePath);

a.MyBool = !a.MyBool;
a.MyString = a.MyString + ".";
a.MyDecimal = a.MyDecimal + 1;
a.MyDateTime = a.MyDateTime.AddDays(1);

XmlContentLoader.Save(filePath, a);

It's all we can do with XmlContentLoader. Now let's see some details of the implementation.

XmlContentLoader has only 3 public methods.

public static T Load<T>(string filePath)
   where T : new()
{
   return Load(filePath, new T());
}

public static T Load<T>(string filePath, T @object)
{
   if (!File.Exists(filePath))           
       throw new FileNotFoundException(string.Format("File {0} does not exist", filePath));

   XDocument document = XDocument.Load(filePath);
   foreach (var p in GetPropertyToXmlItemsMappings(@object, document,
       new { Property = Type<PropertyInfo>(), XmlItems = Type<XmlItem[]>() }))
   {
       p.Property.SetValue(@object,
           Converters[p.Property.PropertyType].ConvertFromString(p.XmlItems.First().Value),
           null);
   }

   return @object;
}

public static void Save(string filePath, object @object)
{
   if (!File.Exists(filePath))           
       throw new FileNotFoundException(string.Format("File {0} does not exist", filePath));

   XDocument document = XDocument.Load(filePath);
   foreach (var p in GetPropertyToXmlItemsMappings(@object, document,
       new { Property = Type<PropertyInfo>(), XmlItems = Type<XmlItem[]>() }))
   {
       object propertyValue = p.Property.GetValue(@object, null);
       foreach (var c in p.XmlItems)
           c.Value = Converters[p.Property.PropertyType].ConvertToString(propertyValue);
   }

   document.Save(filePath);
}


private static Dictionary<Type, TypeConverter> Converters { get; set; }

private static T Type<T>()
{
   return default(T);
}

The implementation of both Load and Save methods is very similar. We load xml document content into the memory using XElement class (added in new LinqToXml API in .Net3.5), then we just iterate throught collection of items returned from GetPropertyToXmlItemsMappings method. Each loop iteration sets new property value or gets the value from property and stores in xml document object structure. What's worth notice, we use TypeConverter class to provide convertion between Syste.String type and property type.

We don't know yet what is the type of the collection item :) So let's see the implementation of GetPropertyToXmlItemsMappings method.

private static T[] GetPropertyToXmlItemsMappings<T>(object @object, XNode document,
   T mappingTypeShape)
{
   var q =
       from p in @object.GetType().GetProperties()
       where Attribute.IsDefined(p, typeof(XmlItemAttribute)) &&
             StoreTypeConverter(p.PropertyType) != null // check if type has TypeConverter
       select new
       {
           Property = p,
           XmlItems =
           (
               from XmlItemAttribute a in Attribute.GetCustomAttributes(p,
                   typeof(XmlItemAttribute))
               select GetXmlItem(document, a)
           ).ToArray() // Force execution 'GetXmlItem()' method 
                       // (check if all specified xml items exist)
       };

   return q.Cast<T>().ToArray(); // Force execution 'GetXmlItem()' method 
                                 // (check if all specified xml items exist)
}

private static XmlItem GetXmlItem(XNode document, XmlItemAttribute a)
{
   return a.IsAttribute
              ? new XmlItem(ExtractAttribute(document, a.ElementPath, a.AttributeName))
              : new XmlItem(ExtractElement(document, a.ElementPath));
}

internal class XmlItem
{
   private XElement Element { get; set; }
   private XAttribute Attribute { get; set; }
   internal string Value
   {
       get
       {
           if (Element != null)
               return Element.Value;
           return Attribute.Value;
       }
       set
       {
           if (Element != null)
               Element.Value = value;
           else
               Attribute.Value = value;
       }
   }

   public XmlItem(XElement element)
   {
       Element = element;
   }
   public XmlItem(XAttribute attribute)
   {
       Attribute = attribute;
   }
}

ReSharper is so smart to tell me: "Parameter 'mappingTypeShape' is never used". So, what for did I add it to the method signature ? Is it really needed or not? Yes, it is. Because I wanted to use an anonymous type inside the method body instead of defining additional type myself (what for if compiler can do that for me ? ;) ) but outside of the method I needed to know the type of collection item. I had to specify somehow the type. But how ? it's an anonymous type. I gave exactly the same type shape as I did inside the method. The compiler is smart enough to use the same generated anonymous type. Don't ask me why I did it this way ... :)

I think XmlContentLoader can be very useful utility when you need to change only some parts of xml file in typed way without necessity to use xml serialization. Additionally when you use PropertyGrid control you can build pretty nice application in just few lines of code.

You can find source code here.

No comments: