This is my XML schema,
<Modules>
<Module name="Sales" path="../SalesInfo.xml" isEnabled="true" ... >
..............
</Module>
<Module name="Purchase" path="../PurchaseInfo.xml" isEnabled="true" ... >
..............
</Module>
................
</Module>
</Modules>
SalesInfo.XML
<Screens>
<Screen name="SalesOrder" ........ />
</Screen>
public class Module
{
public string Name
{
get;
set;
}
public string Type
{
get;
set;
}
.....
}
Here the modules and Screens are loaded on Request basis ("On Demand"). So i have to find the particular node when request comes(may be from menu click). Once the node is picked, it need to be converted to particular class. Eg when "sales" request comes, it should picked from XML and that need to converted to "Module" class. As one possible way is
Using XPathNavigator and find the Node.
Parse the "Finded Node" and convert to Particular class
This is bit complicated. I have to take care of all the attribute and its datatypes.
What i looking for
Find the Node
Convert the Node to My custom Class (I doesn't want to write code for Parser)
What is the Best approach. I'm working on C# 4.0.
EDIT:
XDocument document = XDocument.Load(path);
XElement mod = document.XPathSelectElement("Modules/Module[#name='PurchaseEnquiry']") as XElement;
Module purchaseModule = mod as Module; //This won't work, but i want like this.
Well... Not entirely sure what you are looking for here but I'll give it a shot.
You could read the content node by node and deserialize each node to your class like this:
public class XmlDataReader<T> : IEnumerable, IDisposable
{
private readonly XmlTextReader reader = null;
public XmlDataReader(string filename)
{
reader = new XmlTextReader(filename) {WhitespaceHandling = WhitespaceHandling.None};
reader.MoveToContent(); // Go to root node.
reader.Read(); // Go to first child node.
}
#region Implementation of IEnumerable
public IEnumerator GetEnumerator()
{
if (reader == null) yield break;
do
{
var ser = new XmlSerializer(typeof (T));
var subTree = reader.ReadSubtree();
subTree.MoveToContent();
var node = ser.Deserialize(subTree);
subTree.Close();
yield return node;
reader.Skip();
} while (!reader.EOF && reader.IsStartElement());
}
#endregion
#region Implementation of IDisposable
public void Dispose()
{
if (reader != null)
reader.Close();
}
#endregion
}
In your case you could use it like this:
var reader = new XmlDataReader<Module>("data.xml");
foreach (Module node in reader)
{
...
}
Related
I'm trying to build a basic dialogue system to use in Unity starting from a XML document that look like this:
<?xml version="1.0" encoding="utf-8"?>
<speech id="1">
<bubble id="1" isQuestion="no">
Hi!
</bubble>
<bubble id="2" isQuestion="no">
It's been a while!
</bubble>
<bubble id="3" isQuestion="no">
Have a look at my wares!
</bubble>
<bubble id="4" isQuestion="yes">
Do you want to trade?
<option id="1"> true </option>
<option id="2"> false </option>
</bubble>
<bubble id="5" isQuestion="no">
Goodbye!
</bubble>
</speech>
<speech id="2">
...
</speech>
The concept here is to store each line in a "bubble" node with the id attributes to locate the node in the speech and a Boolean variable to know if the bubble ask a question.
To read that I've tried something like this:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Xml;
public class DialogueManager
{
public List<Speech> LoadSpeechs(XmlDocument doc)
{
doc.Load();
List<Bouble> Boubles = new List<Bouble>();
List<Bouble> Speechs = new List<Speech>();
foreach (xmlNode node in doc.DocumentElement)
{
int id = node.Attributes[0].Value;
foreach (xmlNode node in doc.DocumentElement)
{
int id = node.Attributes[0].Value;
bool isQuestion = node.Attributes[1].Value;
string content = string.Parse(node["bouble"].InnerText);
bouble = new Bouble(id, isQuestion, value);
Boubles.Add(bouble);
}
speech = new Speech(id, Boubles);
Speechs.Add(speech);
}
return Speechs;
}
public class Speech
{
public int SpeechID { get; set; }
public Speech(int m_speechID, List<Bouble> bobules)
{
SpeechID = m_speechID;
Boubles = bobules;
}
}
public class Bouble
{
public bool IsQuesion { get; set; }
public int NodeID { get; set; }
public string Content { get; set; }
public Bouble(int m_nodeID, bool m_isQuestion, string m_value)
{
NodeID = m_nodeID;
IsQuesion = m_isQuestion;
Content = m_value;
}
}
}
The problem is that I get a tons of error, which are difficult to understand alone.
So here I am. I should mention that I'm not trying to realize something in particular just learning the paradigm and Unity so I prefer some comment and explanation on how this work and where I'm mistaken rather than other ways around, but all reply will be welcome :)
I'm planning on adding more attributes, but for now I'd like to figure how to make this work properly.
Based on my test, I find the following problem and you could make some changes.
First, we should have the root node when we want to read the xml files.
Here is my tested xml:
Second, we need to use type convert if we want to convert one type to another type.
Such as the following code:
int sid = Convert.ToInt32(node.Attributes[0].Value);
string content = Lnode.InnerText;(InnerText will return a string type, there is no need to convert again)
Third, we could use the following code convert "yes" or "no" to type bool.
text = Lnode.Attributes[1].Value.ToString();
if(text=="yes")
{
isquestion = true;
}
else
{
isquestion = false;
}
Fourth, we need to put the Speechs.Add(speech); to outside the inner loop.
Finally, you could refer to the following completed code example to convert xml to list.
Code:
public class DialogueManager
{
public List<Speech> LoadSpeechs(XmlDocument doc)
{
doc.Load("test.xml");
List<Bouble> Boubles = new List<Bouble>();
List<Speech> Speechs = new List<Speech>();
string text = string.Empty;
bool isquestion = false;
Speech speech = null;
foreach (XmlNode node in doc.DocumentElement)
{
if (node.Name == "speech")
{
int sid = Convert.ToInt32(node.Attributes[0].Value);
foreach (XmlNode Lnode in node.ChildNodes)
{
int bid = Convert.ToInt32(Lnode.Attributes[0].Value);
text = Lnode.Attributes[1].Value.ToString();
if(text=="yes")
{
isquestion = true;
}
else
{
isquestion = false;
}
string content = Lnode.InnerText;
Bouble bouble = new Bouble(bid, isquestion, content);
Boubles.Add(bouble);
}
speech = new Speech(sid, Boubles);
}
Speechs.Add(speech);
}
return Speechs;
}
public class Speech
{
public int SpeechID { get; set; }
public List<Bouble> Boubles { get; set; }
public Speech(int m_speechID, List<Bouble> bobules)
{
SpeechID = m_speechID;
Boubles = bobules;
}
}
public class Bouble
{
public bool IsQuesion { get; set; }
public int NodeID { get; set; }
public string Content { get; set; }
public Bouble(int m_nodeID, bool m_isQuestion, string m_value)
{
NodeID = m_nodeID;
IsQuesion = m_isQuestion;
Content = m_value;
}
}
}
Tested code and result:(I tested in console app)
static void Main(string[] args)
{
DialogueManager manager = new DialogueManager();
XmlDocument doc = new XmlDocument();
var list=manager.LoadSpeechs(doc);
foreach (DialogueManager.Speech speech in list)
{
Console.WriteLine(speech.SpeechID);
foreach (DialogueManager.Bouble bouble in speech.Boubles)
{
Console.WriteLine(bouble.NodeID);
Console.WriteLine(bouble.IsQuesion);
Console.WriteLine(bouble.Content);
}
}
Console.ReadKey();
}
First of all there is one issue with your XML file: You need a root element in a form like e.g.
<?xml version="1.0"?>
<Root xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<speech>
...
</speech>
<speech>
...
</speech>
</Root>
Then instead of parsing this all "manually" you could/should use XMLSerializer.
This allows you to completely flexible add more elements and attributes without having to change the parser method all the time. You rather already define the entire XML scheme structure directly in the class definitions via attributes
and do something like e.g.
// In general make your classes Serializable and use fields instead of properties
// This way you can also see them in the Unity Inspector
// See https://docs.unity3d.com/Manual/script-Serialization.html
[Serializable]
// That generates/interprets the general XML root element
[XmlRoot]
public class Root
{
// By matching this element name with the root name of the Speech class (see below)
// This is treated as array without the need for an additional wrapper tag
[XmlElement(ElementName = "speech")] public List<Speech> speeches = new List<Speech>();
}
[Serializable]
// By using the same ElementName as root for this
// It will not read/write an additional array wrapper tag but rather direct children of the Root
[XmlRoot(ElementName = "speech")]
public class Speech
{
// Makes this an attribute without the speech tag
// NOTE: I think you wouldn't really need these
// They are elements in an array so you could rely on the index itself
[XmlAttribute] public int id;
[XmlElement("bubble")] public List<Bubble> bubbles = new List<Bubble>();
}
[Serializable]
[XmlRoot(ElementName = "bubble")]
public class Bubble
{
[XmlAttribute] public int id;
// This treats the label as the actual text between the tags
[XmlText] public string label;
// NOTE: Here I thought your bool is quite redundant, you don't need it
// simply check if there are options elements -> if so it is automatically a question
// if there are no options anyway -> there is no question
public bool isQuestion => options.Count != 0;
// If there are none in your XML file the list will simply stay empty
[XmlElement(ElementName = "option")] public List<Option> options = new List<Option>();
}
[Serializable]
[XmlRoot(ElementName = "option")]
public class Option
{
[XmlAttribute] public int id;
[XmlText] public string label;
// Optionally you could use some return value like a bool or enum
// but again, you can also simply go by index
}
This requires your XML file be slightly changed and look like e.g.
<?xml version="1.0"?>
<Root xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<speech id="1">
<bubble id="1">Hallo</bubble>
<bubble id="2">World?
<option id="1">Yes</option>
<option id="2">Nope</option>
</bubble>
</speech>
<speech id="2">
<bubble id="1">Hi</bubble>
<bubble id="2">There!</bubble>
</speech>
</Root>
Then finally you can simply have two methods like e.g.
public class Example : MonoBehaviour
{
[Header("Input")]
[Tooltip("The FULL path to your file - for the example below I cheated ;) ")]
public string xmlFileUrl = Path.Combine(Application.streamingAssetsPath, "Example.xml");
[Header("Result")]
[Tooltip("The deserialized c# classes")]
public Root root;
// This allows you to call this method via the Inspector Context Menu
[ContextMenu(nameof(LoadFile))]
public void LoadFile()
{
// Open the file as a stream
using (var stream = File.Open(xmlFileUrl, FileMode.Open, FileAccess.Read, FileShare.Read))
{
// create an XMLSerializer according to the root type
var serializer = new XmlSerializer(typeof(Root));
// Deserialize the file according to your implemented Root class structure
root = (Root) serializer.Deserialize(stream);
}
}
[ContextMenu(nameof(WriteFile))]
public void WriteFile()
{
// Delete the existing file
if (File.Exists(xmlFileUrl)) File.Delete(xmlFileUrl);
// create the StreamingAsset folder if not exists
// NOTE: Later in your app you want to use the StreamingAssets only
// if your file shall be read-only!
// otherwise use persistentDataPath
if(!Directory.Exists(Application.streamingAssetsPath)) Directory.CreateDirectory(Application.streamingAssetsPath);
// Create a new file as stream
using (var stream = File.Open(xmlFileUrl, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write))
{
var serializer = new XmlSerializer(typeof(Root));
// serialize the current Root class into the XML file
serializer.Serialize(stream, root);
}
#if UNITY_EDITOR
// in the editor refresh the AssetDataBase of Unity
// so you see the added files in the Project View
UnityEditor.AssetDatabase.Refresh();
#endif
}
}
And now you have access to the root and all its properties directly like e.g.
var isQuestion = root.speeches[0].bubbles[1].isQuestion;
and you can do the entire preparation and editing also directly via the Inspector in Unity.
Then as a final little personal touch I would rather use
[Serializable]
[XmlRoot]
public class Root
{
[XmlElement(ElementName = nameof(Speech))] public List<Speech> speeches = new List<Speech>();
}
[Serializable]
[XmlRoot(ElementName = nameof(Speech))]
public class Speech
{
...
[XmlElement(nameof(Bubble))] public List<Bubble> bubbles = new List<Bubble>();
}
[Serializable]
[XmlRoot(ElementName = nameof(Bubble))]
public class Bubble
{
...
[XmlElement(ElementName = nameof(Option))] public List<Option> options = new List<Option>();
}
[Serializable]
[XmlRoot(ElementName = nameof(Option))]
public class Option
{
...
}
and in the XML use the uppercase class names
<?xml version="1.0"?>
<Root xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Speech id="1">
<Bubble id="1">Hallo</Bubble>
<Bubble id="2">World?
<Option id="1">Yes</Option>
<Option id="2">Nope</Option>
</Bubble>
</Speech>
<Speech id="2">
<Bubble id="1">Hi</Bubble>
<Bubble id="2">There!</Bubble>
</Speech>
</Root>
I am trying to construct a .xml file of the form
<Orders>
<Id type="System.Int32">1</Id>
<OrderItems>
<OrderItem>
<Id type="System.Int32">321</Id>
<Product type="System.String">Coffee</Product>
</OrderItem>
</OrderItems>
<Client type="System.String">Johnny</Client>
<Orders>
For Order model:
public class Order
{
public int Id { get; set; }
public List<OrderItem> Products { get; set; }
public string Client { get; set; }
}
Here, I create the Order element
public void SaveToFile(IEnumerable<Order> elementsList)
{
XmlDocument xmlDoc = new XmlDocument();
XmlDeclaration xmlDec = xmlDoc.CreateXmlDeclaration("1.0", "utf-8", string.Empty);
xmlDoc.PrependChild(xmlDec);
XmlElement elemRoot = xmlDoc.CreateElement("Orders");
xmlDoc.AppendChild(elemRoot);
XmlHelper<Order> xmlHelper = new XmlHelper<Order>();
foreach (var order in _orders)
{
xmlHelper.AddNodeToXmlDocument(xmlDoc, elemRoot, order);
}
xmlDoc.PreserveWhitespace = true;
xmlDoc.Save(_filePath);
}
And here, I am trying to construct the sub-elements. It works fine for Id and Client, but when I try to create the order items, I get this error at line document.AppendChild(elemRoot);
public void AddNodeToXmlDocument(XmlDocument document, XmlElement rootElement, object myObject)
{
XmlElement myObjectElement = document.CreateElement(EntityFormatter.GetObjectName(myObject));
foreach (var objectProperty in EntityFormatter.GetPropertiesAndValues(myObject))
{
if ((objectProperty.Value.GetType().FullName).ToString().Contains("System.Collections.Generic.List"))
{
Regex regex = new Regex(#"Models[.][A-Za-z]+");
Match match = regex.Match(objectProperty.Value.ToString());
var elemRoot = document.CreateElement(match.Value.Substring(7));
document.AppendChild(elemRoot);
foreach (var obj in objectProperty.Value.ToString())
{
AddNodeToXmlDocument(document, elemRoot, obj);
}
}
else
{
var elem = document.CreateElement(objectProperty.Key);
elem.SetAttribute("type", objectProperty.Value.GetType().FullName);
elem.InnerText = objectProperty.Value.ToString();
myObjectElement.AppendChild(elem);
}
}
rootElement.AppendChild(myObjectElement);
}
XML specification only allows single root element in a document. document.AppendChild(elemRoot) line in your AddNodeToXmlDocument() method throws exception because root element has been created before in the SaveToFile() method :
.....
XmlElement elemRoot = xmlDoc.CreateElement("Orders");
xmlDoc.AppendChild(elemRoot);
.....
It isn't clear what you're trying to do with the erroneous line, maybe you want to append elemRoot to the previously created root element instead :
.....
var elemRoot = document.CreateElement(match.Value.Substring(7));
document.DocumentElement.AppendChild(elemRoot);
.....
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.
So, this seems very tricky for some reason. I think I have tried every solution I can think of but none seem to be coming close to the result I require.
Say I have a custom XML. It is unstructured but has a common "schema" i.e:
<holder id="1">
<name datatype="int">First Record</name>
<cost datatype="double">100.24</cost>
<service datatype="string" serviceID="12345">A custom service</service>
</holder>
<holder id="2">
<name datatype="int">secondRecord</name>
<cost datatype="double">3000.24</cost>
<service datatype="string" serviceID="9876">A custom service 2</service>
</holder>
all I want to do is go through all of this and parse it into my custom class files.
I have a custom Xml Node (properties: Name (i.e "Cost", value i.e "100.24")
I have a collection of attributes (key/valuepair) in the custom xml Node class
Any ideas how to parse this xml?
Well, you could go for a simple Tree system to read your Xml, it doesn't have to know anything about the XML, can set attributes with an Item indexer, or by using some methods, and using the XmlReader you can go through the node quite fast yourself
However, from my comments you could also see that to me, your problem wasn't completely clear (what you have tried, what is failing, what should be the end result)
I have set up some classes that create a Tree, can register some properties (ITreeNode), have a parent and have a child (i am guessing, as this part of your post is also missing, that it is something a like your custom XmlNode)
Basically, this is the custom XmlParser
public class TreeNodeParser
{
public T ReadNode<T>(T parent, XmlReader reader)
where T: class, ITreeNode, new()
{
if (reader.NodeType == XmlNodeType.Whitespace)
{
return null;
}
T subNode = new T();
while (reader.NodeType != XmlNodeType.Element && reader.Read())
{
}
subNode.Tag = reader.Name;
subNode.Parent = parent;
if (reader.AttributeCount > 0)
{
reader.MoveToFirstAttribute();
do
{
subNode.SetAttribute(reader.Name, reader.Value);
} while (reader.MoveToNextAttribute());
}
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.EndElement)
{
if (string.Equals(subNode.Tag, reader.Name))
{
break;
}
continue;
}
if (reader.NodeType == XmlNodeType.Element)
{
T child = ReadNode(subNode, reader);
if (child != null)
{
subNode.Children.Add(child);
}
continue;
}
if (reader.NodeType == XmlNodeType.Text)
{
subNode.ValueAsString = reader.Value;
continue;
}
}
return subNode;
}
public T FromStream<T>(Stream stream)
where T : class, ITreeNode, new()
{
T result = default(T);
using (XmlReader reader = new XmlTextReader(stream))
{
while (reader.Read())
{
T child = ReadNode(result, reader);
if (child != null)
{
result = child;
}
}
}
return result;
}
}
You can hand it a stream, which it well read through, reading all elements and all attributes (and ignoring whitespace elements). As an xml is in essence a Tree, you get the rootNode back (if any)
As an example, this is the test main program (console)
static void Main(string[] args)
{
TreeNodeParser nodeParser = new TreeNodeParser();
using (Stream readStream = new FileStream(Path.Combine(Environment.CurrentDirectory, "TestFile.xml"), FileMode.Open, FileAccess.Read))
{
var node = nodeParser.FromStream<Node>(readStream);
Console.WriteLine("{0}", node.ToString());
}
Console.ReadLine();
}
The implement TreeNode had the following ToString() override implemented (so you can see that it's not just reading the value as a string ;)):
public override string ToString()
{
string content = "", attributes = "";
foreach (var child in Children)
{
content += child;
}
if (!string.IsNullOrWhiteSpace(ValueAsString))
{
content += ValueAsString;
}
foreach (var entry in properties)
{
attributes += string.Format(" {0}=\"{1}\"", entry.Key, entry.Value);
}
if (string.IsNullOrWhiteSpace(content))
{
return string.Format("<{0}{2} />", this.Tag, content, attributes);
}
return string.Format("<{0}{2}>{1}</{0}>", this.Tag, content, attributes);
}
}
which has as a result
for this to work, i had to change your basic Xml to the following structure (at least one root element)
<?xml version="1.0" encoding="utf-8"?>
<ValueList>
<holder id="1">
<name datatype="int">First Record</name>
<cost datatype="double">100.24</cost>
<service datatype="string" serviceID="12345">A custom service</service>
</holder>
<holder id="2">
<name datatype="int">secondRecord</name>
<cost datatype="double">3000.24</cost>
<service datatype="string" serviceID="9876">A custom service 2</service>
</holder>
</ValueList>
And a possible interface for the ITreeNode could be something like that (though that shouldn't be necessary with your custom XmlNode, so that part you could refactor in the first piece of code)
public interface IPropertyHolder
{
string this[string attributeName] { get; }
void SetAttribute(string attribute, string value);
string GetAttribute(string attribute);
}
public interface INodeParent<T>
{
IList<T> Children { get; }
}
public interface INodeChild<T>
{
T Parent { get; set; }
}
public interface INode
{
string Tag { get; set; }
string ValueAsString { get; set; }
object Value { get; }
}
public interface ITreeNode : INode, IPropertyHolder, INodeParent<ITreeNode>, INodeChild<ITreeNode>
{
}
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; }
}