I’ve finally managed to get my XML serialization library to a point where I’ve been able to start implementing it within another program. My first target is BASeBlock, though the aim is to also use it with new projects in preference to both Binary Serialization as well as manually constructing and reading XML documents or other file formats. By supporting both the interface-based approach, where classes themselves can understand and implement their serialization, as well as having the ability to define “Providers” which can do so separate from the class implementation, the library should be usable for those with various design goals and preferences in terms of class design.
In implementing and adding “support” for the IXmlPersistable interface to BASeBlock, however, I hit an interesting snag- Arrays. Many classes serialize Arrays, which evidently support ISerializable and thus work with that, but my XML persistence library doesn’t know anything about Arrays. Worse still, in the particular instance I encountered, the array was actually an array of arrays ( float[][]), so converting to a List and back while possible would be a huge mess.
I decided that if something was worth doing, it was worth doing properly, and support for easily saving and loading arrays via the “Helper” class was important. Further still, it would need to act recursively and support multidimensional arrays.
The “StandardHelper” class I created within the library is effectively a class that provides some of the gruntwork for turning a class into it’s XElement serialization and back again. For array support, the idea is straight forward- create two methods for reading and writing a System.Array, and then edit SaveElement to properly recognize when the Type parameter is an Array and use that method appropriately.
The strategy I chose for saving the data tends to cause the XML data to be quite large in comparison but it also makes it a bit easier to manipulate by hand. Effectively the Node created will have an attribute specifying the Rank of the Array (the number of dimensions) as well as an attribute specifying the dimension bounds, as a comma-delimited string attribute. The actual Element data is saved within nested elements that have an attribute that specify the indices of the element, and the actual contents of that node will be the result of saving the actual element type parameter to a XElement, which means that any type supported by the library’s “SaveElement” method will work, which effectively means that it automatically supports arrays of arrays such as float[][] or float[,][] and so on. Interestingly the part I found most difficult was in traversing all the array elements. I ended up using an approach that effectively treats each dimension as a place value and increments through all of them one by one via a “carry-bit”, and then when all bounds are the upper bound, it has completed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
/// <summary> /// Saves a System.Array into a XElement XML Node with the specified node name and returns the result. /// </summary> /// <param name="pArrayData">Array Data to save.</param> /// <param name="pNodeName">Node name to use.</param> /// <exception cref="ArgumentNullException">If either input parameter is null.</exception> /// <returns></returns> public static XElement SaveArray(System.Array pArrayData, String pNodeName) { if(pArrayData==null) throw new ArgumentNullException("pArrayData"); if(pNodeName==null) throw new ArgumentNullException("pNodeName"); XElement BuildResult = new XElement(pNodeName); //dimensions get's saved as a attribute. BuildResult.Add(new XAttribute("Rank", pArrayData.Rank)); //now we have a Set of "Dimension" Elements, each filled with the elements for that rank. int[] indices = new int[pArrayData.Rank]; int[] lowerbounds = new int[pArrayData.Rank]; int[] upperbounds = new int[pArrayData.Rank]; //set to the lower bound of the array. for (int i = 0; i < indices.Length; i++) { indices[i] = lowerbounds[i] = pArrayData.GetLowerBound(i); upperbounds[i] = pArrayData.GetUpperBound(i); } String BoundString = String.Join(",", from p in upperbounds select (p + 1).ToString()); XElement DimensionElement = new XElement("Dimensions"); DimensionElement.Add(new XAttribute("Bounds", BoundString)); bool SavingDimensions = true; while (SavingDimensions) { //Reflection "magic" is necessary here as far as I can tell; retrieve the SaveElement method of StandardHelper, then build a definition //for the Generic Method using the Type of the System.Array Element Type we were passed. MethodInfo GenericCall = typeof (StandardHelper).GetMethod("SaveElement").MakeGenericMethod(pArrayData.GetType().GetElementType()); XElement ElementBuilt = (XElement) GenericCall.Invoke(null, new object[] {pArrayData.GetValue(indices), (Object) "Data"}); //add the element, tagged with the Indices. DimensionElement.Add(new XElement("Element", new XAttribute("Index", String.Join(",", from p in indices select p.ToString())), ElementBuilt)); //if all indexes are at the upper bound, break the loop. bool atMax = true; for (int i = 0; i < indices.Length; i++) { if (indices[i] < upperbounds[i]) { atMax = false; break; } } if (atMax) break; //set carry bit. //iterate from the first index to the last index. //if carry bit is true // if current index is at the maximum value // set index to 0. // set carry bit // next iteration // otherwise, add 1 to current index, set carry bit to false. bool fCarry = true; for (int i = 0; i < indices.Length; i++) { if (fCarry) { if (indices[i] == upperbounds[i]) { indices[i] = 0; } else { indices[i]++; fCarry = false; } } } } BuildResult.Add(DimensionElement); return BuildResult; } |
For any save, we obviously want to be able to read it back in. In this instance I took the approach of having it return a System.Array, with the idea that the calling code ought to know what the actual type of the array is to cast it appropriately. It does however accept a Type Parameter which specifies the actual element type of the Array, as this is useful for the task of constructing the array. This data could feasibly have been stored as part of the metadata of the actual Array container XELement along with dimensions and upper bound, but it seems, again, reasonable to expect that the calling code will have some idea what the appropriate data type is.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
/// <summary> /// reads a System.Array from the given XElement. /// </summary> /// <typeparam name="T">Type of Elements of the Array.</typeparam> /// <param name="SourceElement">XElement from which to read Array Data.</param> /// <returns>A System.Array populated from the contents of the given XElement.</returns> public static System.Array ReadArray<T>(XElement SourceElement) { //read the "rank" attribute. int ArrayRank = SourceElement.GetAttributeInt("Rank"); int[] DimensionSizes = new int[ArrayRank]; //now get the Dimensions XElement DimensionElement = SourceElement.Element("Dimensions"); String SBounds = DimensionElement.Attribute("Bounds").Value; //split into a string array... String[] ArrayBounds = SBounds.Split(','); for (int i = 0; i < DimensionSizes.Length; i++) { if (!int.TryParse(ArrayBounds[i], out DimensionSizes[i])) { DimensionSizes[i] = 0; } } //now we have the required dimension sizes, so we can create a System.Array. System.Array BuildArray = Array.CreateInstance(typeof (T), DimensionSizes); //now we interate through all descendants of the "Dimensions" node. int[] elementindex = new int[DimensionSizes.Length]; foreach (XElement ReadElement in DimensionElement.Descendants("Element")) { String IndexStr = ReadElement.GetAttributeString("Index"); String[] IndexStrings = IndexStr.Split(','); for (int loopelement = 0; loopelement < elementindex.Length; loopelement++) { if (!int.TryParse(IndexStrings[loopelement], out elementindex[loopelement])) elementindex[loopelement] = 0; } //alright- first, read in this element. T readresult = StandardHelper.LoadElement<T>(ReadElement.Descendants().First()); //once read, assign it to the appropriate array index. BuildArray.SetValue((object) readresult, elementindex); } //assigned successfully- return result. return BuildArray; } |
Both of these methods currently lack any sophisticated Error handling or Exception throwing as one would more reasonably expect from a library; for example, trying to read an int saved to an XElement and read that XElement via ReadArray would indubitably throw an Exception related to missing Attributes. Such mismatched data most likely should result in a strongly-typed Exception such as a “MismatchedElementDataException” that can be traced to the library itself.
The most interesting endeavour is the usage of Reflection. Though it is a bit goofy to use sometimes it really does allow for powerful constructs to be used, and I particularly find it powerful when dealing with Generic types, as one can construct Generic method definitions or even Generic Class types with the appropriate information and then return data fluently to callers. In this instance it is primarily used to construct a generic method reference to the SaveElement
Again, the main goal of this library is to effectively serve as a reasonable alternative to the existing Serialization framework. I am using BASeBlock as a testbed for experimentation currently, but the goal is to provide a safe and less “black-box” alternative to the built-in IFormatter and BinaryFormatter serialization features of .NET and provide XML capabilities through a similar approach, as I found the class-design constraints of using the existing XML Serialization to be limiting.
Have something to say about this post? Comment!