XML Serialization - XmlCDataSection as Serialization.XmlText - c#

I am having problems serializing a cdata section using c#
I need to serialize XmlCDataSection object property as the innertext of the element.
The result I am looking for is this:
<Test value2="Another Test">
<![CDATA[<p>hello world</p>]]>
</Test>
To produce this, I am using this object:
public class Test
{
[System.Xml.Serialization.XmlText()]
public XmlCDataSection value { get; set; }
[System.Xml.Serialization.XmlAttributeAttribute()]
public string value2 { get; set; }
}
When using the xmltext annotation on the value property the following error is thrown.
System.InvalidOperationException:
There was an error reflecting property
'value'. --->
System.InvalidOperationException:
Cannot serialize member 'value' of
type System.Xml.XmlCDataSection.
XmlAttribute/XmlText cannot be used to
encode complex types
If I comment out the annotation, the serialization will work but the cdata section is placed into a value element which is no good for what I am trying to do:
<Test value2="Another Test">
<value><![CDATA[<p>hello world</p>]]></value>
</Test>
Can anybody point me in the right direction to getting this to work.
Thanks, Adam

Thanks Richard, only now had chance to get back to this. I think I have resolved the problem by using your suggestion. I have created a CDataField object using the following:
public class CDataField : IXmlSerializable
{
private string elementName;
private string elementValue;
public CDataField(string elementName, string elementValue)
{
this.elementName = elementName;
this.elementValue = elementValue;
}
public XmlSchema GetSchema()
{
return null;
}
public void WriteXml(XmlWriter w)
{
w.WriteStartElement(this.elementName);
w.WriteCData(this.elementValue);
w.WriteEndElement();
}
public void ReadXml(XmlReader r)
{
throw new NotImplementedException("This method has not been implemented");
}
}

The way Test is defined, your data is a CData object. So the serialisation system is trying to preserve the CData object.
But you want to serialise some text data as a CData section.
So first, the type of Test.value should be String.
You then need to control how that field is serialised, but there does not appear to be any inbuilt method or attribute to control how strings are serialised (as string, maybe with entities for reserved characters, or as CDATA). (Since, from an XML infoset perspective all of these are the same, this is not surprising.)
You can of course implemented IXmlSerializable and just code the serialisation of the Test type yourself which gives you complete control.

This basically shorter version of Jack answer with better error messages:
[XmlIgnore]
public string Content { get; set; }
[XmlText]
public XmlNode[] ContentAsCData
{
get => new[] { new XmlDocument().CreateCDataSection(Content) };
set => Content = value?.Cast<XmlCDataSection>()?.Single()?.Data;
}

Just found an alternative from here:
[XmlIgnore]
public string Content { get; set; }
[XmlText]
public XmlNode[] CDataContent
{
get
{
var dummy = new XmlDocument();
return new XmlNode[] {dummy.CreateCDataSection(Content)};
}
set
{
if (value == null)
{
Content = null;
return;
}
if (value.Length != 1)
{
throw new InvalidOperationException(
String.Format(
"Invalid array length {0}", value.Length));
}
var node0 = value[0];
var cdata = node0 as XmlCDataSection;
if (cdata == null)
{
throw new InvalidOperationException(
String.Format(
"Invalid node type {0}", node0.NodeType));
}
Content = cdata.Data;
}
}
}

I had very same problem as Adam. However this Answer does not helped me at 100% :) but gives me a clue. So I'va created a code like below. It generates XML like this:
<Actions>
<Action Type="reset">
<![CDATA[
<dbname>longcall</dbname>
<ontimeout>
<url>http://[IPPS_ADDRESS]/</url>
<timeout>10</timeout>
</ontimeout>
]]>
</Action>
<Action Type="load">
<![CDATA[
<dbname>longcall</dbname>
]]>
</Action>
</Actions>
Code:
public class ActionsCDataField : IXmlSerializable
{
public List<Action> Actions { get; set; }
public ActionsCDataField()
{
Actions = new List<Action>();
}
public XmlSchema GetSchema()
{
return null;
}
public void WriteXml(XmlWriter w)
{
foreach (var item in Actions)
{
w.WriteStartElement("Action");
w.WriteAttributeString("Type", item.Type);
w.WriteCData(item.InnerText);
w.WriteEndElement();
w.WriteString("\r\n");
}
}
public void ReadXml(XmlReader r)
{
XmlDocument xDoc = new XmlDocument();
xDoc.Load(r);
XmlNodeList nodes = xDoc.GetElementsByTagName("Action");
if (nodes != null && nodes.Count > 0)
{
foreach (XmlElement node in nodes)
{
Action a = new Action();
a.Type = node.GetAttribute("Type");
a.InnerText = node.InnerXml;
if (a.InnerText != null && a.InnerText.StartsWith("<![CDATA[") && a.InnerText.EndsWith("]]>"))
a.InnerText = a.InnerText.Substring("<![CDATA[".Length, a.InnerText.Length - "<![CDATA[]]>".Length);
Actions.Add(a);
}
}
}
}
public class Action
{
public String Type { get; set; }
public String InnerText { get; set; }
}

Related

Deserialize XML with elements that differ by attribute

I have this snippet of XML (actually it's XBRL, but that is based on XML)
<xbrl>
<context id="Context_Duration" />
<context id="Context_Instant_Begin" />
<context id="Context_Instant_End" />
<ConstructionDepotSpecification>
<ConstructionDepotLabel contextRef="Context_Duration">depot</ConstructionDepotLabel>
<ConstructionDepotBalance contextRef="Context_Instant_Begin">45000</ConstructionDepotBalance>
<ConstructionDepotBalance contextRef="Context_Instant_End">42000</ConstructionDepotBalance>
</ConstructionDepotSpecification>
</xbrl>
(additional content and xml namespaces declarations removed for clarity)
I want to deserialize this to a class, but I'm not sure how to handle the ConstructionDepotBalance elements. If I define a property ConstructionDepotBalance it will just take the value of the first element, so I think I should create two properties instead, one for begin value and one for end value.
So the class should look like this
[XmlRoot(ElementName = "xbrl")]
public partial class Taxonomy
{
[XmlElement]
public List<ConstructionDepotSpecification> ConstructionDepotSpecification { get; set; }
}
public partial class ConstructionDepotSpecification
{
public string ConstructionDepotLabel { get; set; }
public long? ConstructionDepotBalanceBegin { get; set; }
public long? ConstructionDepotBalanceEnd { get; set; }
}
So the element with attribute Context_Instant_Begin should be deserialized to ConstructionDepotBalanceBegin and the other element with attribute Context_Instant_End should be deserialized to ConstructionDepotBalanceEnd.
Is this possible to achieve? Should I use an IXmlSerializable implementation for this?
My first approach:
You could parse the XML-String first and replace
[ConstructionDepotBalance contextRef="Context_Instant_Begin"]
with
[ConstructionDepotBalanceBegin]
(Same with ConstructionDepotBalanceEnd).
In the second step you deserialze the XML string.
I experimented a bit with the IXmlSerializable interface and came up with this implementation:
public void ReadXml(XmlReader reader)
{
reader.ReadStartElement();
while (!reader.EOF)
{
var ctx = reader.GetAttribute("contextRef");
if (ctx == "Context_Duration")
{
string propName = reader.Name;
var propInfo = GetType().GetProperty(propName);
Type propType = propInfo.PropertyType;
if (propType.GenericTypeArguments.Length > 0)
{
propType = propType.GenericTypeArguments[0];
}
var value = reader.ReadElementContentAs(propType, null);
propInfo.SetValue(this, value);
}
else if (ctx == "Context_Instant_Begin")
{
string propName = reader.Name + "Begin";
var propInfo = GetType().GetProperty(propName);
var value = reader.ReadElementContentAsLong();
propInfo.SetValue(this, value);
}
else if (ctx == "Context_Instant_End")
{
string propName = reader.Name + "End";
var propInfo = GetType().GetProperty(propName);
var value = reader.ReadElementContentAsLong();
propInfo.SetValue(this, value);
}
if (reader.NodeType == XmlNodeType.EndElement)
{
reader.ReadEndElement();
break;
}
}
}
Not sure if it's the best solution for this problem, but for now it does what I want.

Is Dyanamic De-serialization of XML possible? [duplicate]

I have a XML document provided by client applications to my C# application. This is how a client sends the XML file:
<?xml version="1.0" encoding="utf-8"?>
<SomeAccount>
<parentId>2380983</parentId>
<!-- more elements -->
</SomeAccount>
And a C# class that supports the XML deserialization:
[XmlRoot]
public class SomeAccount
{
[XmlElement("parentId")]
public long ParentId { get; set; }
//rest of fields...
}
But there are some clients whose system send the XML in this way (note the upper case in LeParentId):
<?xml version="1.0" encoding="utf-8"?>
<SomeAccount>
<LeParentId>2380983</LeParentId>
<!-- similar for the other elements -->
</SomeAccount>
How can I make this field (and others) to support both XML names parentId and LeParentId?
This is the method I'm currently using for XML deserialization:
public sealed class XmlSerializationUtil
{
public static T Deserialize<T>(string xml)
{
if (xml == null)
return default(T);
XmlSerializer serializer = new XmlSerializer(typeof(T));
StringReader stringReader = new StringReader(xml);
return (T)serializer.Deserialize(stringReader);
}
}
I tried to add [XmlElement] twice in the field, one per element name, but that didn't work.
Take 2 - let's implement this ourselves using the unknown element handling event (see the comments below for some limitations though):
public class XmlSynonymDeserializer : XmlSerializer
{
public class SynonymsAttribute : Attribute
{
public readonly ISet<string> Names;
public SynonymsAttribute(params string[] names)
{
this.Names = new HashSet<string>(names);
}
public static MemberInfo GetMember(object obj, string name)
{
Type type = obj.GetType();
var result = type.GetProperty(name);
if (result != null)
return result;
foreach (MemberInfo member in type.GetProperties().Cast<MemberInfo>().Union(type.GetFields()))
foreach (var attr in member.GetCustomAttributes(typeof(SynonymsAttribute), true))
if (attr is SynonymsAttribute && ((SynonymsAttribute)attr).Names.Contains(name))
return member;
return null;
}
}
public XmlSynonymDeserializer(Type type)
: base(type)
{
this.UnknownElement += this.SynonymHandler;
}
public XmlSynonymDeserializer(Type type, XmlRootAttribute root)
: base(type, root)
{
this.UnknownElement += this.SynonymHandler;
}
protected void SynonymHandler(object sender, XmlElementEventArgs e)
{
var member = SynonymsAttribute.GetMember(e.ObjectBeingDeserialized, e.Element.Name);
Type memberType;
if (member != null && member is FieldInfo)
memberType = ((FieldInfo)member).FieldType;
else if (member != null && member is PropertyInfo)
memberType = ((PropertyInfo)member).PropertyType;
else
return;
if (member != null)
{
object value;
XmlSynonymDeserializer serializer = new XmlSynonymDeserializer(memberType, new XmlRootAttribute(e.Element.Name));
using (System.IO.StringReader reader = new System.IO.StringReader(e.Element.OuterXml))
value = serializer.Deserialize(reader);
if (member is FieldInfo)
((FieldInfo)member).SetValue(e.ObjectBeingDeserialized, value);
else if (member is PropertyInfo)
((PropertyInfo)member).SetValue(e.ObjectBeingDeserialized, value);
}
}
}
And now the actual code of the class would be:
[XmlRoot]
public class SomeAccount
{
[XmlElement("parentId")]
[XmlSynonymDeserializer.Synonyms("LeParentId", "AnotherGreatName")]
public long ParentId { get; set; }
//rest of fields...
}
To deserialize, simply use XmlSynonymDeserializer instead of the regular XmlSerializer. This should work for most of the basic needs.
Known limitations:
This implementation supports only elements with multiple names; extending it for attributes should be trivial
Support for handling of properties/fields in cases where the entities inherit from one another is not tested
This implementation does not check for programming bugs (having the attribute on read-only/constant field/properties, multiple members with the same synonyms and so on)
I know this is an old post, but maybe this will help anyone else having the same problem.
What you could use for this problem is XmlChoiceIdentifier.
[XmlRoot]
public class SomeAccount
{
[XmlIgnore]
public ItemChoiceType EnumType;
[XmlChoiceIdentifier("EnumType")]
[XmlElement("LeParentId")]
[XmlElement("parentId")]
public long ParentId { get; set; }
//rest of fields...
}
[XmlType(IncludeInSchema = false)]
public enum ItemChoiceType
{
LeParentId,
parentId
}
Now if you have a new xml version and a new XmlElement name you just add that name to the ItemChoiceType enum and a new XmlElement to the property.
If you need only one more name, here is a quick (and rather ugly) solution that we deployed in several cases in my work when we had only to read XMLs (this will be problematic for serializing back to an XML), because it's the simplest and easiest to understand:
[XmlRoot]
public class SomeAccount
{
[XmlElement("parentId")]
public long ParentId { get; set; }
[XmlElement("LeParentId")]
public long LeParentId { get { return this.ParentId; } set { this.ParentId = value; } }
//rest of fields...
}

Recursive adding XML into TreeView

I'm trying to import an XML file of nodes into the same node structure in a TreeView using C#. I have found a lot of example that use a single node structure, but have had a lot of issues traversing the XML file and populating the TreeView with it. This is a sample of the XML file:
<?xml version="1.0"?>
<xmlRoot>
<ProductGroup>
<Group>
<GroupName>Soda</GroupName>
<Classifications>
<Classification>
<ClassificationName>Regular</ClassificationName>
<Containers>
<Container>
<ContainerType>Can</ContainerType>
<ContainerName>SmallCan</ContainerName>
</Container>
<Container>
<ContainerType>bottle</ContainerType>
<ContainerName>SmallBottle</ContainerName>
</Container>
</Containers>
</Classification>
<Classification>
<ClassificationName>Diet</ClassificationName>
<Containers>
<Container>
<ContainerType>Can</ContainerType>
<ContainerName>SmallCan</ContainerName>
</Container>
</Containers>
</Classification>
</Classifications>
</Group>
<Group>
<GroupName>Water</GroupName>
<Classifications>
<Classification>
<ClassificationName>Regular</ClassificationName>
<Containers>
<Container>
<ContainerType>Bottle</ContainerType>
<ContainerName>EcoBottle</ContainerName>
</Container>
</Containers>
</Classification>
</Classifications>
</Group>
</ProductGroup>
</xmlRoot>
I've tried using something like this:
treProducts.Nodes.Clear();
XDocument xdoc = XDocument.Load("ProductDocument.xml");
foreach (XElement groupElement in xdoc.Descendants("Group"))
{
treProducts.Nodes.Add(groupElement.Element("GroupName").Value);
treProducts.SelectedNode = treProducts.Nodes[groupElement.Element("GroupName").Value];
foreach (XElement ClassificationElement in groupElement.Descendants("Classification"))
{
treProducts.SelectedNode.Nodes.Add(groupElement.Element("ClassificationName").Value);
treProducts.SelectedNode = treProducts.Nodes[groupElement.Element("ClassificationName").Value];
foreach (XElement ContainerElement in groupElement.Descendants("Container"))
{
treProducts.SelectedNode.Nodes.Add(ContainerElement.Element("ContainerName").Value);
}
}
}
I'm trying to get the tree to show:
Soda
Regular
SmallCan
SmallBottle
Diet
SmallCan
Water
Regular
EcoBottle
...but the tree is only showing Soda and it seems to skip the rest, except if I comment out the nested foreach statements, it will show Soda and Water.
There's something wrong with the syntax I'm using and I'm wondering if someone who understands Linq better can help see where the code is wrong.
You are using the wrong variable in your loop over the Classification Elements. Replace groupElement with ClassificationElement inside the loop.
Change:
foreach (XElement ClassificationElement in groupElement.Descendants("Classification"))
{
// groupElement.Element("ClassificationName") is null:
treProducts.SelectedNode.Nodes.Add(groupElement.Element("ClassificationName").Value);
...
}
to
foreach (XElement ClassificationElement in groupElement.Descendants("Classification"))
{
treProducts.SelectedNode.Nodes.Add(ClassificationElement.Element("ClassificationName").Value);
...
}
I suggest recursion
void AddNodes(XElement parentElement, TreeNode parent = null)
{
Queue<XElement> queue = new Queue<XElement>(parentElement.Elements());
while (queue.Count > 0)
{
TreeNode child = parent;
XElement element = queue.Dequeue();
if (!element.HasElements)
{
string value = element.Value;
element = (XElement)element.NextNode;
if (null != element && !element.HasElements)
value = element.Value;
if (null == parent)
treeView1.Nodes.Add(child = new TreeNode(value));
else
parent.Nodes.Add(child = new TreeNode(value));
child.Expand();
element = queue.Dequeue();
}
AddNodes(element, child);
}
}
AddNodes(XElement.Load("ProductDocument.xml"));
Note: dbc's answer is probably better if your XML structure is likely to change, but if you are given it as it currently stands, and it won't change - then this will slurp it right into the tree quickly, without a lot of overhead.
The reason this is complicated is that your tree node hierarchy does not correspond 1-1 to your XML hierarchy. In situations like this, I suggest introducing intermediate classes to provide a view into the base XML model data. In WPF these classes would be the View Model, but the windows forms TreeView doesn't support data binding. Despite this, the abstraction of a view model is useful here.
First, some basic view model interfaces and classes to tie together TreeNode and XElement hierarchies:
public interface ITreeNodeViewModel
{
string Name { get; }
string Text { get; }
object Tag { get; }
object Model { get; }
IEnumerable<ITreeNodeViewModel> Children { get; }
}
public abstract class TreeNodeViewModel<T> : ITreeNodeViewModel
{
readonly T model;
public TreeNodeViewModel(T model)
{
this.model = model;
}
public T Model { get { return model; } }
#region ITreeNodeProxy Members
public abstract string Name { get; }
public abstract string Text { get; }
public virtual object Tag { get { return this; } }
public abstract IEnumerable<ITreeNodeViewModel> Children { get; }
#endregion
#region ITreeNodeViewModel Members
object ITreeNodeViewModel.Model
{
get { return Model; }
}
#endregion
}
public abstract class XElementTreeNodeViewModel : TreeNodeViewModel<XElement>
{
public XElementTreeNodeViewModel(XElement node) : base(node) {
if (node == null)
throw new ArgumentNullException();
}
public XNamespace Namespace { get { return Model.Name.Namespace; } }
public override string Name
{
get { return Model.Name.ToString(); }
}
}
Next, a couple extension classes:
public static class TreeViewExtensions
{
public static void PopulateNodes(this TreeView treeView, IEnumerable<ITreeNodeViewModel> viewNodes)
{
treeView.BeginUpdate();
try
{
treeView.Nodes.PopulateNodes(viewNodes);
}
finally
{
treeView.EndUpdate();
}
}
public static void PopulateNodes(this TreeNodeCollection nodes, IEnumerable<ITreeNodeViewModel> viewNodes)
{
nodes.Clear();
if (viewNodes == null)
return;
foreach (var viewNode in viewNodes)
{
var name = viewNode.Name;
var text = viewNode.Text;
if (string.IsNullOrEmpty(text))
text = name;
var node = new TreeNode { Name = name, Text = text, Tag = viewNode.Tag };
nodes.Add(node);
PopulateNodes(node.Nodes, viewNode.Children);
node.Expand();
}
}
}
public static class XObjectExtensions
{
public static string TextValue(this XContainer node)
{
if (node == null)
return null;
//return string.Concat(node.Nodes().OfType<XText>().Select(tx => tx.Value)); c# 4.0
return node.Nodes().OfType<XText>().Select(tx => tx.Value).Aggregate(new StringBuilder(), (sb, s) => sb.Append(s)).ToString();
}
public static IEnumerable<XElement> Elements(this IEnumerable<XElement> elements, XName name)
{
return elements.SelectMany(el => el.Elements(name));
}
}
Next, view models for the three levels of your tree:
class ContainerViewModel : XElementTreeNodeViewModel
{
public ContainerViewModel(XElement node) : base(node) { }
public override string Text
{
get
{
return Model.Element(Namespace + "ContainerName").TextValue();
}
}
public override IEnumerable<ITreeNodeViewModel> Children
{
get { return Enumerable.Empty<ITreeNodeViewModel>(); }
}
}
class ClassificationViewModel : XElementTreeNodeViewModel
{
public ClassificationViewModel(XElement node) : base(node) { }
public override string Text
{
get
{
return Model.Element(Namespace + "ClassificationName").TextValue();
}
}
public override IEnumerable<ITreeNodeViewModel> Children
{
get
{
return Model.Elements(Namespace + "Containers").Elements<XElement>(Namespace + "Container").Select(xn => (ITreeNodeViewModel)new ContainerViewModel(xn));
}
}
}
class GroupViewModel : XElementTreeNodeViewModel
{
public GroupViewModel(XElement node) : base(node) { }
public override string Text
{
get
{
return Model.Element(Namespace + "GroupName").TextValue();
}
}
public override IEnumerable<ITreeNodeViewModel> Children
{
get
{
return Model.Elements(Namespace + "Classifications").Elements<XElement>(Namespace + "Classification").Select(xn => (ITreeNodeViewModel)new ClassificationViewModel(xn));
}
}
}
Now, building your tree becomes quite simple:
var xdoc = XDocument.Load("ProductDocument.xml");
var ns = xdoc.Root.Name.Namespace;
treeView1.PopulateNodes(xdoc.Root.Elements(ns + "ProductGroup").Elements(ns + "Group").Select(xn => (ITreeNodeViewModel)new GroupViewModel(xn)));
And the result:
Later, if you wish to add editing functionality to your tree, you can add the appropriate methods to ITreeNodeViewModel -- for instance, a setter method for Text. Since the ITreeNodeViewModel has saved itself in the TreeNode.Tag the appropriate methods will be available.

Is it possible to xml-serialize a property (in a xml serialized class) that returns a list of string objects?

I have a class, lets call it, Employee, that has a list of qualifications as property
[XmlRoot("Employee")]
public class Employee
{
private string name;
private IListManager<string> qualification = new ListManager<string>();
public Employee()
{
}
[XmlElement("Name")]
public string Name
{
get
{
return name;
}
set
{
if (value != null)
{
name = value;
}
}
}
public ListManager<string> QualificationList
{
get
{
return qualification as ListManager<string>;
}
}
[XmlElement("Qual")]
public string Qualifications
{
get
{
return ReturnQualifications();
}
}
private string ReturnQualifications()
{
string qual = string.Empty;
for (int i = 0; i < qualification.Count; i++)
{
qual += " " + qualification.GetElement(i);
}
return qual;
}
public override String ToString()
{
String infFormat = String.Format("{0, 1} {1, 15}", this.name, Qualifications);
return infFormat;
}
}
}
Then I have a method that serializes the above class tom XML by taking a list of Employees as patameter, the method is generic:
public static void Serialize<T>(string path, T typeOf)
{
XmlSerializer xmlSer = new XmlSerializer(typeof(T));
TextWriter t = new StreamWriter(path);
try
{
xmlSer.Serialize(t, typeOf);
}
catch
{
throw;
}
finally
{
if (t != null)
{
t.Close();
}
}
}
This is how I call the method XMLSerialize:
controller.XMLSerialize<Employee>(opnXMLFileDialog.FileName, employeeList);
The result of the method execution returns a file and is shown below:
<?xml version="1.0" encoding="utf-8"?>
<ArrayOfEmployees xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Employees>
<Name>g</Name>
</Employees>
</ArrayOfEmployees>
as you can see there is only the property name included in the file. How do I proceed from here and serialize the list of qualifications too? Any suggestionswill be greatly appreciated.
Thanks in advance.
In order to serialize a property, XmlSerializer requires that the property have both a public setter and public getter. So whatever property you choose to serialize your qualifications must have a setter as well as a getter.
The obvious solution would be for your QualificationList to have a setter as well as a getter:
public ListManager<string> QualificationList
{
get
{
return qualification as ListManager<string>;
}
set
{
qualification = value;
}
}
If for some reason this cannot be done -- perhaps because your ListManager<string> class is not serializable -- you could serialize and deserialize it as a proxy array of strings, like so:
[XmlIgnore]
public ListManager<string> QualificationList
{
get
{
return qualification as ListManager<string>;
}
}
[XmlElement("Qual")]
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public string[] QualificationArray
{
get
{
return Enumerable.Range(0, qualification.Count).Select(i => qualification.GetElement(i)).ToArray();
}
set
{
// Here I am assuming your ListManager<string> class has Clear() and Add() methods.
qualification.Clear();
if (value != null)
foreach (var str in value)
{
qualification.Add(str);
}
}
}
Then for the following employee list:
var employee = new Employee() { Name = "Mnemonics" };
employee.QualificationList.Add("posts on stackoverflow");
employee.QualificationList.Add("easy to remember");
employee.QualificationList.Add("hard to spell");
var list = new List<Employee>();
list.Add(employee);
The following XML is generated:
<?xml version="1.0" encoding="utf-16"?>
<ArrayOfEmployee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Employee>
<Name>Mnemonics</Name>
<Qual>posts on stackoverflow</Qual>
<Qual>easy to remember</Qual>
<Qual>hard to spell</Qual>
</Employee>
</ArrayOfEmployee>
Is that satisfactory?

How to define multiple names for XmlElement field?

I have a XML document provided by client applications to my C# application. This is how a client sends the XML file:
<?xml version="1.0" encoding="utf-8"?>
<SomeAccount>
<parentId>2380983</parentId>
<!-- more elements -->
</SomeAccount>
And a C# class that supports the XML deserialization:
[XmlRoot]
public class SomeAccount
{
[XmlElement("parentId")]
public long ParentId { get; set; }
//rest of fields...
}
But there are some clients whose system send the XML in this way (note the upper case in LeParentId):
<?xml version="1.0" encoding="utf-8"?>
<SomeAccount>
<LeParentId>2380983</LeParentId>
<!-- similar for the other elements -->
</SomeAccount>
How can I make this field (and others) to support both XML names parentId and LeParentId?
This is the method I'm currently using for XML deserialization:
public sealed class XmlSerializationUtil
{
public static T Deserialize<T>(string xml)
{
if (xml == null)
return default(T);
XmlSerializer serializer = new XmlSerializer(typeof(T));
StringReader stringReader = new StringReader(xml);
return (T)serializer.Deserialize(stringReader);
}
}
I tried to add [XmlElement] twice in the field, one per element name, but that didn't work.
Take 2 - let's implement this ourselves using the unknown element handling event (see the comments below for some limitations though):
public class XmlSynonymDeserializer : XmlSerializer
{
public class SynonymsAttribute : Attribute
{
public readonly ISet<string> Names;
public SynonymsAttribute(params string[] names)
{
this.Names = new HashSet<string>(names);
}
public static MemberInfo GetMember(object obj, string name)
{
Type type = obj.GetType();
var result = type.GetProperty(name);
if (result != null)
return result;
foreach (MemberInfo member in type.GetProperties().Cast<MemberInfo>().Union(type.GetFields()))
foreach (var attr in member.GetCustomAttributes(typeof(SynonymsAttribute), true))
if (attr is SynonymsAttribute && ((SynonymsAttribute)attr).Names.Contains(name))
return member;
return null;
}
}
public XmlSynonymDeserializer(Type type)
: base(type)
{
this.UnknownElement += this.SynonymHandler;
}
public XmlSynonymDeserializer(Type type, XmlRootAttribute root)
: base(type, root)
{
this.UnknownElement += this.SynonymHandler;
}
protected void SynonymHandler(object sender, XmlElementEventArgs e)
{
var member = SynonymsAttribute.GetMember(e.ObjectBeingDeserialized, e.Element.Name);
Type memberType;
if (member != null && member is FieldInfo)
memberType = ((FieldInfo)member).FieldType;
else if (member != null && member is PropertyInfo)
memberType = ((PropertyInfo)member).PropertyType;
else
return;
if (member != null)
{
object value;
XmlSynonymDeserializer serializer = new XmlSynonymDeserializer(memberType, new XmlRootAttribute(e.Element.Name));
using (System.IO.StringReader reader = new System.IO.StringReader(e.Element.OuterXml))
value = serializer.Deserialize(reader);
if (member is FieldInfo)
((FieldInfo)member).SetValue(e.ObjectBeingDeserialized, value);
else if (member is PropertyInfo)
((PropertyInfo)member).SetValue(e.ObjectBeingDeserialized, value);
}
}
}
And now the actual code of the class would be:
[XmlRoot]
public class SomeAccount
{
[XmlElement("parentId")]
[XmlSynonymDeserializer.Synonyms("LeParentId", "AnotherGreatName")]
public long ParentId { get; set; }
//rest of fields...
}
To deserialize, simply use XmlSynonymDeserializer instead of the regular XmlSerializer. This should work for most of the basic needs.
Known limitations:
This implementation supports only elements with multiple names; extending it for attributes should be trivial
Support for handling of properties/fields in cases where the entities inherit from one another is not tested
This implementation does not check for programming bugs (having the attribute on read-only/constant field/properties, multiple members with the same synonyms and so on)
I know this is an old post, but maybe this will help anyone else having the same problem.
What you could use for this problem is XmlChoiceIdentifier.
[XmlRoot]
public class SomeAccount
{
[XmlIgnore]
public ItemChoiceType EnumType;
[XmlChoiceIdentifier("EnumType")]
[XmlElement("LeParentId")]
[XmlElement("parentId")]
public long ParentId { get; set; }
//rest of fields...
}
[XmlType(IncludeInSchema = false)]
public enum ItemChoiceType
{
LeParentId,
parentId
}
Now if you have a new xml version and a new XmlElement name you just add that name to the ItemChoiceType enum and a new XmlElement to the property.
If you need only one more name, here is a quick (and rather ugly) solution that we deployed in several cases in my work when we had only to read XMLs (this will be problematic for serializing back to an XML), because it's the simplest and easiest to understand:
[XmlRoot]
public class SomeAccount
{
[XmlElement("parentId")]
public long ParentId { get; set; }
[XmlElement("LeParentId")]
public long LeParentId { get { return this.ParentId; } set { this.ParentId = value; } }
//rest of fields...
}

Categories

Resources