I have figured out how to populate a custom class from XML data, but I ran into an issue along the way. Things were working perfectly with my existing method of populating data until I was thrown a bit of a curve ball. The new schema I was sent is similar to this:
<ITEM_REPLY>
<TRAN_ID>1320691307345</TRAN_ID>
<REPLY_CODE>0</REPLY_CODE>
<UNIT_PRICE>8.2784</UNIT_PRICE>
<SUP_LOCS>
<SUP_LOC>
<SUP_LOC_ID>001134</SUP_LOC_ID>
<COUNTRY_ID>USA</COUNTRY_ID>
<QTY_AVL>47.000</QTY_AVL>
<ITEM_UOM>EA</ITEM_UOM>
</SUP_LOC>
<SUP_LOC>
<SUP_LOC_ID>006817</SUP_LOC_ID>
<COUNTRY_ID>USA</COUNTRY_ID>
<QTY_AVL>20.000</QTY_AVL>
<ITEM_UOM>EA</ITEM_UOM>
</SUP_LOC>
</SUP_LOCS>
<MESSAGE />
<QTY_BREAKS />
</ITEM_REPLY>
Pretty standard XML schema, problem is I'm not sure how to populate my custom class with it. Here's what I do have:
static void Main(string[] args)
{
var order = ConvertXMLMessage<ItemReply>(request);
}
protected static T ConvertXMLMessage<T>(String xmlData) where T : class, new()
{
var xml = new XmlDocument();
xml.LoadXml(xmlData);
var serializer = new System.Xml.Serialization.XmlSerializer(typeof(T));
using (var xmlReader = new XmlNodeReader(xml.DocumentElement))
{
T work = (T)(serializer.Deserialize(xmlReader));
return work;
}
}
public class ItemReply
{
[XmlElement("ITEM_REPLY")]
public ItemAvlReply ITEM_REPLY { get; set; }
}
public class ItemAvlReply
{
[XmlElement("TRAN_ID")]
public string TRAN_ID { get; set; }
[XmlElement("REPLY_CODE")]
public string REPLY_CODE { get; set; }
[XmlElement("UNIT_PRICE")]
public string UNIT_PRICE { get; set; }
[XmlElement("SUP_LOCS")]
public SupplierLocations SUP_LOCS;
[XmlElement("MESSAGE")]
public string MESSAGE { get; set; }
[XmlElement("QTY_BREAKS")]
public string QTY_BREAKS { get; set; }
}
public class SupplierLocations
{
[XmlElement("SUP_LOC")]
public List<SupplierLocation> SUP_LOC;
}
public class SupplierLocation
{
[XmlElement("SUP_LOC_ID")]
public string SUP_LOC_ID { get; set; }
[XmlElement("COUNTRY_ID")]
public string COUNTRY_ID { get; set; }
[XmlElement("QTY_AVL")]
public string QTY_AVL { get; set; }
[XmlElement("ITEM_UOM")]
public string ITEM_UOM { get; set; }
}
This works perfectly minus the List<Item> part. I'm not overly experienced with LINQ and I'm not sure how to go about declaring a sub array in my class via this statement. I am also open to a different approach from creating the List<Item> part, I'm just not sure where to start otherwise. Is there a better approach for what I'm need to do? Is there an easy solution I am just unaware of in LINQ?
Here's a simple way to do it, assuming the example XML file you provided has typos. I assumed the OrderId has a closing tag, and that the closing tag for Items should be /Items.
Here's the version of the xml I used:
<Order>
<TransactionID>123</TransactionID>
<OrderID>1</OrderID>
<Items Number="2">
<Item>
<ItemName>Test</ItemName>
<Color>Red</Color>
</Item>
<Item>
<ItemName>Test1</ItemName>
<Color>Blue</Color>
</Item>
</Items>
</Order>
Here's the code to read/write the XML: (the xml variable is a String)
var order = ConvertXMLMessage<Order>(xml);
WriteXMLFile<Order>(order, #"test.xml");
Here's the ConvertXMLMessage and WriteXMLFile functions:
protected static T ConvertXMLMessage<T>(String xmlData) where T : class, new()
{
var xml = new XmlDocument();
xml.LoadXml(xmlData);
var serializer = new System.Xml.Serialization.XmlSerializer(typeof(T));
using (var xmlReader = new XmlNodeReader(xml.DocumentElement))
{
T work = (T)(serializer.Deserialize(xmlReader));
return work;
}
}
protected static void WriteXMLFile<T>(T item, String saveLocation) where T : class, new()
{
System.Xml.Serialization.XmlSerializer writer = new System.Xml.Serialization.XmlSerializer(typeof(T));
System.IO.StreamWriter file = new System.IO.StreamWriter(saveLocation);
writer.Serialize(file, item);
file.Close();
}
and here's the class structure:
public class Order
{
[XmlElement("TransactionID")]
public string TransactionId { get; set; }
[XmlElement("OrderID")]
public string OrderId { get; set; }
[XmlElement("Items")]
public ItemsContainer Items;
}
public class ItemsContainer
{
[XmlAttribute("Number")]
public Int32 Number { get; set; }
[XmlElement("Item")]
public List<Item> Items;
}
public class Item
{
[XmlElement("ItemName")]
public string ItemName { get; set; }
[XmlElement("Color")]
public string Color { get; set; }
}
As you'll notice I added some attributes to let the XML parser know how to handle the class when it's converting from/to the XML. I also added another small class called "ItemsContainer" just to hold the details on the Items tag. If you didn't need the "Number" attribute, then you could probably find a way to do away with this. However, this should get you in the right direction.
The example I provided is a simple version of how I usually handle the situation, obviously there's some improvements you can make depending on your needs.
Edit
I changed the Item class to use ItemName instead of TransactionId. It was an oversight on my part.
Edit 2
Here's the corrections you need to make to the newly posted code. The reason the Order class worked in the previous example was it matched the root XML element. You're new XML does align with the base class. So we need to add in a couple more attributes to make this work. You can also remove your ItemReply class. It's not needed.
So here's the new classes:
[XmlRoot("ITEM_REPLY")]
public class ItemAvlReply
{
[XmlElement("TRAN_ID")]
public string TRAN_ID { get; set; }
[XmlElement("REPLY_CODE")]
public string REPLY_CODE { get; set; }
[XmlElement("UNIT_PRICE")]
public string UNIT_PRICE { get; set; }
[XmlElement("SUP_LOCS")]
public SupplierLocations SUP_LOCS;
[XmlElement("MESSAGE")]
public string MESSAGE { get; set; }
[XmlElement("QTY_BREAKS")]
public string QTY_BREAKS { get; set; }
}
public class SupplierLocations
{
[XmlElement("SUP_LOC")]
public List<SupplierLocation> SUP_LOC;
}
public class SupplierLocation
{
[XmlElement("SUP_LOC_ID")]
public string SUP_LOC_ID { get; set; }
[XmlElement("COUNTRY_ID")]
public string COUNTRY_ID { get; set; }
[XmlElement("QTY_AVL")]
public string QTY_AVL { get; set; }
[XmlElement("ITEM_UOM")]
public string ITEM_UOM { get; set; }
}
Everything else should remain the same. The parsing/converting the XML to classes should work without any changes.
Related
I need to parse a xml document into object models that I've created but I can't figure out how to do so, I think it's because of my lack of understanding of the xml structure.
I've tried to get all the elements from the document and to create individual object from each based on their attributes I think they're called.
Here is my C# code :
var manifest = XDocument.Load(theDocument);
var allTheElements = manifest.Descendants();
foreach (var element in allTheElements)
{
//No idea how to parse each object into individual ManifestModel's
}
public class ManifestModel
{
public string Version { get; set; }
public string Resource { get; set; }
public string Size { get; set; }
public string Checksum { get; set; }
}
And here is the XML data :
<?xml version="1.0" encoding="UTF-8"?>
<manifest version="1.0.0" totalbytes="6131797">
<source uri="codeapi.io/Game/patches/">
<file resource="FooGame.sln" size="1125" checksum="530B9F1C2412A6D74EF017919074FD8966E5280D" />
<file resource=".vs\FooGame\v16\.suo" size="69120" checksum="438976A3681FDD503DB4FBFCBB5D420E9E8838DD" />
</source>
</manifest>
Just like we have json2csharp for JSON, we have Xml2Csharp for XML. There are probably lots of other sites that will do this.
Paste your XML and it generates this:
[XmlRoot(ElementName="file")]
public class File {
[XmlAttribute(AttributeName="resource")]
public string Resource { get; set; }
[XmlAttribute(AttributeName="size")]
public string Size { get; set; }
[XmlAttribute(AttributeName="checksum")]
public string Checksum { get; set; }
}
[XmlRoot(ElementName="source")]
public class Source {
[XmlElement(ElementName="file")]
public List<File> File { get; set; }
[XmlAttribute(AttributeName="uri")]
public string Uri { get; set; }
}
[XmlRoot(ElementName="manifest")]
public class Manifest {
[XmlElement(ElementName="source")]
public Source Source { get; set; }
[XmlAttribute(AttributeName="version")]
public string Version { get; set; }
[XmlAttribute(AttributeName="totalbytes")]
public string Totalbytes { get; set; }
}
One could call that lazy or cheating, but I don't see the point in writing code that can be generated for me in a second. You might not always get perfect results, but it's a good starting point. For example, it uses string for all attribute types. If you're expecting all numeric values you could replace those with int or long.
Now you can deserialize like this:
var serializer = new XmlSerializer(typeof(Manifest), new XmlRootAttribute("manifest"));
using (var stream = System.IO.File.OpenRead("test.xml"))
{
var deserialized = (Manifest)serializer.Deserialize(stream);
}
Once you've got the data deserialized into something, the rest is much easier. You can either use the auto-generated models or map them to your own.
Using LINQ...
c#
void Main()
{
string fileName = #"e:\Temp\GamePatches.xml";
XDocument manifest = XDocument.Load(fileName);
string version = manifest.Root.Attribute("version").Value;
List<ManifestModel> manifestModel = new List<ManifestModel>();
foreach (XElement e in manifest.Descendants("file"))
{
manifestModel.Add(new ManifestModel() { Version = version
, Resource = (string)e.Attribute("resource").Value
, Size = (string)e.Attribute("size").Value
, Checksum = (string)e.Attribute("checksum").Value }
);
}
}
// Define other methods and classes here
public class ManifestModel
{
public string Version { get; set; }
public string Resource { get; set; }
public string Size { get; set; }
public string Checksum { get; set; }
}
I spent a lot of time working on a similar app that parsed through XML Schema, and I found the easiest way is to turn the XML Document into an XmlNodeList. From here you can use the SelectNodes and SelectSingleNodes to navigate through it. Take a look at this:https://learn.microsoft.com/en-us/dotnet/api/system.xml.xmlnode.selectnodes?view=netframework-4.8, but basically what you do is create an xpath string which selects the node you need. Here is some documentation on that: https://learn.microsoft.com/en-us/dotnet/standard/data/xml/select-nodes-using-xpath-navigation
the second day I'm trying to solve my problem and I think I need some help to do it. =/
So, I'm trying to do a UI for my project that for each Stage (or screen, however you name it) will take an XML and build Objects from it.
Basically, my code looks like that:
Basic class for stage's - all the other are inheriting from this one:
class BaseStage {
[XmlIgnore]
public Type Type;
[XmlIgnore]
public string XmlPath;
public BaseChildren UIScheme;
public BaseStage()
{
Type = GetType();
XmlPath = "Template/" + Type.ToString().Replace("Some.Project.Namespace.", "") + ".xml";
}
public virtual void LoadContent()
{
if (File.Exists(XmlPath))
{
using (TextReader reader = new StreamReader(XmlPath))
{
XmlSerializer uiDeserializer = new XmlSerializer(typeof(BaseChildren));
UIScheme = (BaseChildren)uiDeserializer.Deserialize(reader);
}
}
}
Some UI Classes:
public class BaseChildren
{
public List<BaseChildren> Children = new List<BaseChildren>();
public virtual void AddChild(BaseChildrenchild)
{
Children.Add(child);
}
}
public class Dialog : BaseChildren {
public string Type;
public Rectangle Position;
}
public class Label : BaseChildren {}
// ... and so on
My XML file:
<?xml version="1.0" encoding="utf-8" ?>
<UIScheme>
<Children>
<Dialog>
<Type>Blue</Type>
<Position>
<X>43</X>
<Y>100</Y>
<Width>350</Width>
<Height>200</Height>
</Position>
</Dialog>
</Children>
</UIScheme>
I've tried numerous variations and have no idea how to force serializer to load those objects =/
I also tried to override XmlSerializer and tried to write my own reading function, but it's hard to even get any tutorials how to properly do it in Google.
for fast check ,you can convert xml to C# class using this one http://xmltocsharp.azurewebsites.net/
[XmlRoot(ElementName="Position")]
public class Position {
[XmlElement(ElementName="X")]
public string X { get; set; }
[XmlElement(ElementName="Y")]
public string Y { get; set; }
[XmlElement(ElementName="Width")]
public string Width { get; set; }
[XmlElement(ElementName="Height")]
public string Height { get; set; }
}
[XmlRoot(ElementName="Dialog")]
public class Dialog {
[XmlElement(ElementName="Type")]
public string Type { get; set; }
[XmlElement(ElementName="Position")]
public Position Position { get; set; }
}
[XmlRoot(ElementName="Children")]
public class Children {
[XmlElement(ElementName="Dialog")]
public Dialog Dialog { get; set; }
}
[XmlRoot(ElementName="UIScheme")]
public class UIScheme {
[XmlElement(ElementName="Children")]
public Children Children { get; set; }
}
I know there are several posts out there with this topic, but I can't seem to figure out what is the problem here. I have serialized and deserialized xml several times, and never had this error.
The exception message is:
There is an error in XML document (1, 2).
With InnerException:
<InvoiceChangeRequest xmlns=''> was not expected.
XML file I want to deserialize:
<ns1:InvoiceChangeRequest xmlns:ns1="http://kmd.dk/fie/external_invoiceDistribution">
<CONTROL_FIELDS>
<STRUCTURID>0000000001</STRUCTURID>
<OPERA>GET</OPERA>
<WIID>000050371220</WIID>
</CONTROL_FIELDS>
<HEADER_IN>
<MANDT>751</MANDT>
<BELNR>1234567890</BELNR>
</HEADER_IN>
<ITEMS>
<ITEM_FIELDS_IN>
<BUZEI>001</BUZEI>
<BUKRS>0020</BUKRS>
</ITEM_FIELDS_IN>
</ITEMS>
</ns1:InvoiceChangeRequest>
Class I'm trying to deserialize to:
[XmlRoot(Namespace = "http://kmd.dk/fie/external_invoiceDistribution", IsNullable = false)]
public class InvoiceChangeRequest
{
[XmlElement("CONTROL_FIELDS")] public ControlFields Styrefelter;
[XmlElement("HEADER_IN")] public HeaderIn HeaderfelterInd;
[XmlElement("ITEMS")] public Items Linjer;
}
public class HeaderIn
{
[XmlElement("MANDT")] public string Kommunenummer;
[XmlElement("BELNR")] public string RegnskabsbilagsNummer;
}
public class Items
{
[XmlElement("ITEM_FIELDS_IN")] public Itemfield[] ItemfelterInd;
}
public class Itemfield
{
[XmlElement("BUZEI")] public string Linjenummer;
[XmlElement("BUKRS")] public string Firmakode;
}
Deserialization code:
XmlSerializer serializer = new XmlSerializer(typeof(InvoiceChangeRequest));
var request = serializer.Deserialize(new StringReader(output)) as InvoiceChangeRequest;
In your XML file your root element is the namespace http://kmd.dk/fie/external_invoiceDistribution with prefix ns1.
The element <CONTROL_FIELDS> isn't because it isn't prefixed. Your serialization class doesn't take this into account though. That means that it expects that <CONTROL_FIELDS> and the other elements are ALSO in the ns1 namespace.
To get the serializer parse the elements correctly add the Namespace to the elements, setting it to an empty string:
[XmlRoot(Namespace = "http://kmd.dk/fie/external_invoiceDistribution", IsNullable = false)]
public class InvoiceChangeRequest
{
[XmlElement("CONTROL_FIELDS", Namespace = "")]
public ControlFields Styrefelter { get; set; }
[XmlElement("HEADER_IN", Namespace = "")]
public HeaderIn HeaderfelterInd { get; set; }
[XmlElement("ITEMS", Namespace = "")]
public Items Linjer { get; set; }
}
This will de-serialize the given XML as intended.
In case of de-serialization issues I often create the classes in memory and then serialize that so I can inspect the resulting XML. That often gives clues on what is missing or being added compared to the input document:
var ms = new MemoryStream();
serializer.Serialize(ms, new InvoiceChangeRequest {
Styrefelter = new ControlFields { Opera="test"}
});
var s = Encoding.UTF8.GetString(ms.ToArray());
And then inspect s for differences.
You can replace 'ns1:' with string.Empty.
Below classes should serialize.
public class Item
{
[XmlElement("BUZEI")]
public string Buzei { get; set; }
[XmlElement("BUKRS")]
public string Bukrs { get; set; }
}
public class Header
{
[XmlElement("MANDT")]
public string Mandt { get; set; }
[XmlElement("BELNR")]
public string Belnr { get; set; }
}
public class ControlFields
{
[XmlElement("STRUCTURID")]
public string StructuredId { get; set; }
[XmlElement("OPERA")]
public string Opera { get; set; }
[XmlElement("WIID")]
public string Wild { get; set; }
}
public class InvoiceChangeRequest
{
[XmlElement("CONTROL_FIELDS")]
public ControlFields ControlFields { get; set; }
[XmlElement("HEADER_IN")]
public Header Header { get; set; }
[XmlArray("ITEMS")]
[XmlArrayItem("ITEM_FIELDS_IN")]
public List<Item> Items { get; set; }
}
I have created a little logg program and with this i can save custom classes to xml, and convert them back from xml to a class. this works fine, but the problem is if i want to add one class to the list in the xml, i have to read all of them, add one class and rewrite all of them if i use this method, now i know i can manualy add a class by searching for elements and so on, but i wondered if i could do this in the way that i write all of them.
this is an example of the code that i ame using:
public static void Test()
{
List<LoggInformation> infos = new List<LoggInformation>();
infos.Add(new LoggInformation() { Level = BpuInterface.BpuInterface.BPUController.LoggLevel.Debug, Message = "error1" });
infos.Add(new LoggInformation() { Level = BpuInterface.BpuInterface.BPUController.LoggLevel.Error, Message = "error2" });
DataContractSerializer dd = new DataContractSerializer(typeof(List<LoggInformation>));
using (var writer = new StreamWriter(#"C://testLoggfile.xml"))
{
dd.WriteObject(writer.BaseStream, infos);
}
}
public static void AddOneItem()
{
//??????????
}
[DataContract]
public class LoggInformation
{
[DataMemberAttribute]
public BpuInterface.BpuInterface.BPUController.LoggLevel Level { get; set; }
[DataMemberAttribute]
public Source Source { get; set; }
[DataMemberAttribute]
public string ExceptionMessage { get; set; }
[DataMemberAttribute]
public string ExceptionStack { get; set; }
[DataMemberAttribute]
public string ThreadName { get; set; }
[DataMemberAttribute]
public System.Threading.ApartmentState ThreadApartmentState { get; set; }
[DataMemberAttribute]
public string Message { get; set; }
[DataMemberAttribute]
public DateTime DateTime { get; set; }
}
You can use this instead new StreamWriter(#"C://testLoggfile.xml"), true) will append line at the end of file.
public static void Test()
{
...
using (var writer = new StreamWriter(#"C://testLoggfile.xml"), true) // this will append line at the end of file.
{
dd.WriteObject(writer.BaseStream, infos);
}
}
...
I've managed to deserialize my XML for the most part, but I'm stuck on one particular thing. How do I get the value of an element, if it's in an array, and each item in that array has it's own attributes. Let me show you what I have
<BusinessObject Name="BusinessName" RecID="12345">
<FieldList>
<Field Name="Field1">FieldValue1</Field>
<Field Name="Field2">FieldValue2</Field>
</FieldList>
</BusinessObject>
So this is a cut-down version, but shows the basis of the XML. I'm currently having trouble trying to capture "FieldValue1" and "FieldValue2" in their respective Field elements.
[XmlRoot("BusinessObject")]
public sealed class BusinessObject
{
[XmlAttribute("Name")]
public string Name { get; set; }
[XmlAttribute("RecID")]
public string RecID { get; set; }
[XmlElement("FieldList", Type = typeof(FieldList))]
public FieldList FieldList { get; set; }
public BusinessObject()
{
FieldList = null;
}
public static BusinessObject FromXmlString(string xmlString)
{
var reader = new StringReader(xmlString);
var serializer = new XmlSerializer(typeof(BusinessObject));
var instance = (BusinessObject)serializer.Deserialize(reader);
return instance;
}
}
[Serializable]
public class FieldList
{
[XmlElement("Field", Type = typeof(Field))]
public Field[] Fields { get; set; }
public FieldList()
{
Fields = null;
}
}
[Serializable]
public class Field
{
[XmlAttribute("Name")]
public string Name { get; set; }
public Field()
{
}
}
I'm sure it's just something simple that I'm missing, but I was having trouble trying to put my problem into words to perform a relevant search.
Many thanks,
Mark
Thanks for taking a look. Clearly I should play around more before posting here, as I've just managed to get it working. I had to add the following to the Field class:
[Serializable]
public class Field
{
[XmlAttribute("Name")]
public string Name { get; set; }
[XmlText]
public string Value { get; set; }
public Field()
{
}
}
Unfortunately I'm just running on a copy of express here, so I couldn't work out how to use xsd without the command prompt.
Cheers all,
Mark