Link to home
Start Free TrialLog in
Avatar of rwallacej
rwallacej

asked on

Make serialized XML "well presented"

Hi
I'd like some help on this problem please. I serialize an object made up of various other objects. The classes are marked as <Serializable()>  so they can be saved/loaded to / from XML.

So the object is saved something like this where FieldA, FieldB, FieldC and FieldD are properties:
      <TheClass>
        <FieldA>123</FieldA>
        <FieldB>_TotalLoadMW>234</FieldB>
        <FieldC>345</FieldC>
        <FieldD>456</FieldD>
etc.
etc.
      <TheClass>
This is OK when there is a small amount of fields, but I'd rather it was presented as follows so as to not be on as many lines.  Theres a lot of lines...the user would like to "see" the XML generated and even edit it...

<TheClass FieldA="123" FieldB="234" FieldC="345" FieldD="456"/>

Can one customise the serializing of files and still let them deserialize automatically?  How is this done....I'd appreciate samples.

Thanks
Avatar of abel
abel
Flag of Netherlands image

If it is all about presentation (viewing) your XML, one possible, different (and likely easier) approach is to change the presentation by using an XSLT transform on the result. Where you have an object to the XML output, all you need to do is associate an XSLT with it which changes elements into attributes. You can keep the original output for deserializing.

You can also do this by adding an XSLT PI to the XML output. I don't thing that would bother the deserialization process (as PI's that are not known are generally ignored).

If you want to go down this path I can provide a small XSLT file for you that does exactly this: transform elements to attributes.

Cheers,
-- Abel--
Avatar of rwallacej
rwallacej

ASKER

I think I understand what you are suggesting.

It is all about viewing the XML and letting the user edit it if possible

An example would be great, thank-you
Can you handle adding the XSLT to your XML? Here's an XSLT file (version 1.0, I don't assume you have Saxon.NET, which is currently the only available .NET processor for XSLT 2.0 and Microsoft.NET only does XSLT 1.0, which is does well, I may add).

I don't know how your XSLT skills are. If you have seen XSLT before, this is trivial, otherwise it may seem a bit daunting at first sight because of all @ and * and / and | around.

The XSLT below looks specifically for children of TheClass nodes. Presumably, this is not the real name and presumably this name will change between classes (I don't assume you have only one serializable class, do you?). To make that more generic, I need to see a little real-world output of the classes. More specifically, I need to know how to uniquely identify an element for being a Class element or a Field element (and not accidentally add other elements that are on the same level). Can you provide such an XML example?

Cheers,
-- Abel --

<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 
    <!-- copy everything as-is -->
    <xsl:template match="@* | node()">
        <xsl:copy>
            <xsl:apply-templates select="@* | node()"/>
        </xsl:copy>
    </xsl:template>
 
    <!-- change these node into attributes -->
    <xsl:template match="TheClass/*">
        <xsl:attribute name="{name()}">
            <xsl:value-of select="." />
        </xsl:attribute>
    </xsl:template>
 
</xsl:stylesheet>

Open in new window

But as you can see, it only takes a couple of lines of XSLT. Even if you don't know it, you might be surprised by the simplicity it takes for copying everything as is but changing particular elements into attributes. If you look besides the mandatory clutter, you'll find that the only meat is between lines 12 and 16.
I have never used XSLT so this does look rather daunting.

TheClass is not the real name, it is the "container" object containing fields which are instances of another class. I'm not that "bad" :-)  You are right in that I don't just have one serializable class.

Here is a sample of the XML serialized produced (again teh properties are random but it gives the structure; I have done search/replace in notepad)  

The are some collections in it e.g.
"PG" has a collection of "PGS".
"RT" and "MD" & "MML" are fields/properties of the "PGS" class

"BT" is a collection of "ClassZ" etc.

Some fields are strings, some are integers, other doubles.  All the numbers here are zero; the real they wouldn't be

If you require any more explanations I can add to this.

Thanks again

<?xml version="1.0" enCDding="utf-16"?>
<TheClass xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <PG>
    <PGS>
      <RT>Description 1</RT>
      <MD>A</MD>
      <MML>0</MML>
      <CD>0</CD>
      <FI>0</FI>
      <ART>2</ART>
      <EF>0</EF>
      <EF2>0</EF2>
      <ISRT>1</ISRT>
    </PGS>
    <PGS>
      <RT>Description 2</RT>
      <MD>B</MD>
      <MML>0</MML>
      <CD>0</CD>
      <FI>0</FI>
      <ART>2</ART>
      <EF>0</EF>
      <EF2>0</EF2>
      <ISRT>1</ISRT>
    </PGS>
  </PG>
  
  <BT>
    <ClassZ>
      <CX2>0</CX2>
      <CX>0</CX>
      <CT>0</CT>
      <Category>The category</Category>
    </ClassZ>
    <ClassZ>
      <CX2>0</CX2>
      <CX>0</CX>
      <CT>0</CT>
      <Category>The category</Category>
    </ClassZ>
  </BT>
  
  <SSC>
    <YetAnotherClassDC>
      <ST>0</ST>
      <DT>0</DT>
      <PS>0</PS>
      <PD>0</PD>
    </YetAnotherClassDC>
    <YetAnotherClassDC>
      <ST>0</ST>
      <DT>0</DT>
      <PS>0</PS>
      <PD>0</PD>
    </YetAnotherClassDC>
    <YetAnotherClassDC>
      <ST>0</ST>
      <DT>0</DT>
      <PS>0</PS>
      <PD>0</PD>
    </YetAnotherClassDC>
  </SSC>
  
  <SSP>
    <YetAnotherClass>
      <ST>0</ST>
      <DT>0</DT>
      <AnotherClass>
        <IPB>0</IPB>
        <ITD>0</ITD>
      </AnotherClass>
      <RP>0</RP>
    </YetAnotherClass>
    
    <YetAnotherClass>
      <ST>0</ST>
      <DT>0</DT>
      <AnotherClass>
        <IPB>0</IPB>
        <ITD>0</ITD>
      </AnotherClass>
    </YetAnotherClass>
  </SSP>
  
  <Tag>
    <MV>0</MV>
    <MiV>0</MiV>
    <DefaultValue>0</DefaultValue>
  </Tag>
</TheClass>

Open in new window

Could I summarize that you want any atomic value (int, double, date(?), string) as an attribute and any complex class (which has children that has other classes or atomic values) as a normal child like it is now?

Do I understand it correctly that atomic values (the ones you want as attribute) never have children? In other words, are deepest in the chain?

So, the start / bottom of the above would become, after transformation something like:

<TheClass 
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:xsd="http://www.w3.org/2001/XMLSchema">
 
  <PG>
    <PGS @RT="Description 1" @MD="A" @MML="0" ...>
    <PGS @RT="Description 2" @MD="B" @MML="0" ...>
  </PG>
  ...
 
  <SSP>
    <YetAnotherClass @ST="0" @DT="" @RP="0">
      <AnotherClass @IPB="0" @ITD="0" />
    </YetAnotherClass>
    <YetAnotherClass @ST="0" @DT="" @RP="0">
      <AnotherClass @IPB="0" @ITD="0" />
    </YetAnotherClass>
  </SSP>
 
  <Tag @MV="0" @MiV="0" @DefaultValue="0" />
 
</TheClass>

Open in new window

I'd answer yes to these quesions, but I now have another question - I'm wondering how atomic values (int, double, date) could have children ? - I'd have though a property of a class could only have children if it was a complex class? How could an integer have a child..sorry if I'm missing something really obvious?
No, you don't miss anything. But in C# (or VB, what do you use?) it is not always trivial when something is serialized to an atomic value or not. For instance, a DateTime class has many fields, but could be serialized using a single number (ticks, or Universal Date Time format). A string also has many fields (properties) but will be serialized to an atomic value (you don't see the field "length" appearing in the serialized data, right?).

Although this is obvious for int, double, bit, byte, long etc, which are defined as atomic types in the C# system, it is not so obvious for some of the other types. In addition, you might find your type:

public class MyType
{
    public int myField = 1;
}

an atomic type (it only has an integer to serialize), yet the serializer will not make MyType atomic, instead it will do so for the field myField.

Since you answered yet I'll come up with an XSLT file for you. Then I'll help you how you can use it with your code.
good to know all this thank-you, and that I didn't miss something. Its VB.net I'll writing in, although I can understand C# too
Ok, it has become slightly more complicated. The trick was that not all elements that have to become attributes are direct children (descendants) but can also appear after a complex type (SSP/YetAnotherClass is an example where RP comes after AnotherClass) and in XSLT terms it is not possible to add an attribute once you have created another element (you cannot go back).

Though it is still fairly standard XSLT, it may look a little harder for you now. When, however, you replace every * (asterisk) with the word "child" and the "@*" with the word "all attributes" and in your head you think as a tree-traversal it all becomes quite reasonable.

In XSLT the processor does the work and the thinking for you. Hence you hardly need any if-statements. You just say "grab these elements and try to find a matching rule". Inside the rules (which you define) you can tell the processor what should happen with that particular element.

Without going into too many details of a lovely and very versatile language XSLT, here's what it looks like in practice:

<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 
	<!-- match everything -->
	<xsl:template match="*">
		
		<!-- copy the current node (does not copy content) -->
		<xsl:copy>
			
			<!-- copy attributes if any -->
			<xsl:copy-of select="@*" />
			
			<!-- apply all elements without children -->
			<xsl:apply-templates select="*[not(*)]"/>
			
			<!-- apply all elements with children and attributes -->
			<xsl:apply-templates select="*[*]"/>
			
		</xsl:copy>
	</xsl:template>
 
	<!-- change nodes without children into attributes -->
	<xsl:template match="*[not(*)]">
		
		<!-- create attribute with name as name of elementt -->
		<xsl:attribute name="{name()}">
			<!-- and contents of element -->
			<xsl:value-of select="." />
		</xsl:attribute>
		
	</xsl:template>
 
</xsl:stylesheet>

Open in new window

As you can see I added abundant comments to make it easier to grasp.

Now we need to apply this to your XML file. How would you want to do that, programmatically (with XmlWriter?) or statically, from a commandline with any available XSLT processor (saxon, Microsoft, Altova, libxslt etc)?
Oh, and before we really continue it is probably best that you have a look at the outcome when the XSLT is applied to your XML (note that your XML contains an error in the XML prolog: the encoding is not correct). it doesn't matter how any of the elements are named:

<?xml version="1.0" encoding="utf-8"?>
<TheClass xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <PG>
    <PGS RT="Description 1" MD="A" MML="0" CD="0" FI="0" ART="2" EF="0" EF2="0" ISRT="1" />
    <PGS RT="Description 2" MD="B" MML="0" CD="0" FI="0" ART="2" EF="0" EF2="0" ISRT="1" />
  </PG>
  <BT>
    <ClassZ CX2="0" CX="0" CT="0" Category="The category" />
    <ClassZ CX2="0" CX="0" CT="0" Category="The category" />
  </BT>
  <SSC>
    <YetAnotherClassDC ST="0" DT="0" PS="0" PD="0" />
    <YetAnotherClassDC ST="0" DT="0" PS="0" PD="0" />
    <YetAnotherClassDC ST="0" DT="0" PS="0" PD="0" />
  </SSC>
  <SSP>
    <YetAnotherClass ST="0" DT="0" RP="0">
      <AnotherClass IPB="0" ITD="0" />
    </YetAnotherClass>
    <YetAnotherClass ST="0" DT="0">
      <AnotherClass IPB="0" ITD="0" />
    </YetAnotherClass>
  </SSP>
  <Tag MV="0" MiV="0" DefaultValue="0" />
</TheClass>

Open in new window

This is the type of XML output I'm looking for - great; I was thinking of using the XMLWriter class  if possible please
ASKER CERTIFIED SOLUTION
Avatar of abel
abel
Flag of Netherlands image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
PS: I kept the code as small as possible. If you continue with XSLT there's sure to be one day that you want to use the XSLT document() function or write extension functions. In that case, you need to add/change the following:

XsltSettings xsltSettings = new XsltSettings(true, true);
xslt.Load("Xslt/ee1.xslt", xsltSettings, null);

Truly excellent solution, thank-you.

The deserializing of transformed XML works too which is a bonus.

As a postnote for the benefit of others who may come to use this, here's simple code I wrote for an ASP.net page to display the outputs of above
        ' create output writer 
        Dim xmlSettings As New XmlWriterSettings()
        xmlSettings.Indent = True
 
        Dim xmlWriter As XmlWriter = xmlWriter.Create(Server.MapPath("~/Xslt/output2.xml"), xmlSettings)
 
        ' load xslt and do transform 
        Dim xslt As New XslCompiledTransform(True)
        Dim xsltSettings As New XsltSettings(True, True)
        xslt.Load(Server.MapPath("~/Xslt/ee1.xslt"), xsltSettings, Nothing)
        xslt.Transform(Server.MapPath("Xslt/input.xml"), Nothing, xmlWriter)
        xmlWriter.Close()
 
 
        ' display original XML and transformed XML on separate textboxes
        Me.TextBox1.Text = File.ReadAllText(Server.MapPath("Xslt/input.xml"))
        Dim output As String = File.ReadAllText(Server.MapPath("Xslt/output2.xml"))
        Me.TextBox2.Text = output
 
        ' deserialize the file Transformed by xslt
        Dim oXS As XmlSerializer
        oXS = New XmlSerializer(GetType(MyClass))
 
        Dim i As StreamReader = New StreamReader(Server.MapPath("Xslt/output2.xml"))
        Dim ins As MyClass = oXS.Deserialize(i)
        i.Close()
 
        'display deserialized object
        Me.TextBox3.text = ins.ToString

Open in new window

Glad it worked out well and I'm glad too that deserializing works too :)
and thanks for the points!
you're welcome, thanks :-)