Get Object nested structure as string path - c#

Consider the following set of classes. There are two things I would like to achieve.
Get the string representation of the path of the current property. For example totalAsset.BuildingAsset.HistoricalBuildingAsset.Path should return "TotalAsset.BuildingAsset.HistoricalBuildingAsset"
Given a path "TotalAsset.BuildingAsset.HistoricalBuildingAsset" and a value "100", I want to use the path to retrieve the property and change its value.
Code Example:
public abstract class Field
{
private string _path = string.Empty;
public double Value {get;set;}
public string Path
{
get
{
//Code probably goes here
throw new NotImplementedException();
}
protected set { _path = value; }
}
}
public sealed class TotalAsset : Field
{
public TotalAsset(BuildingAsset buildingAsset)
{
Path = "TotalAsset";
BuildingAsset = buildingAsset;
}
public BuildingAsset BuildingAsset { get; private set; }
}
public sealed class BuildingAsset : Field
{
public HistoricalBuildingAsset HistoricalBuildingAsset { get; private set; }
public BuildingAsset(HistoricalBuildingAsset historicalBuildingAsset)
{
Path = "BuildingAsset";
this.HistoricalBuildingAsset = historicalBuildingAsset;
}
}
public sealed class HistoricalBuildingAsset : Field
{
public HistoricalBuildingAsset()
{
Path = "HistoricalBuildingAsset";
}
}
[TestClass]
public class TestPath
{
[TestMethod]
public void MethodTestPath()
{
var historicalBuildingAsset = new HistoricalBuildingAsset();
var buildingAsset = new BuildingAsset(historicalBuildingAsset);
var totalAsset = new TotalAsset(buildingAsset);
Assert.AreEqual("TotalAsset.BuildingAsset.HistoricalBuildingAsset", totalAsset.BuildingAsset.HistoricalBuildingAsset.Path);
}
}

Wouldn't this be easily solved using polymorphism?
Based on your question, it seems like your Path property has an inmutable value, thus you should be able to solve your issue like the following code:
public class A
{
public virtual string Path
{
get { return "A"; }
}
}
public class B : A
{
public override string Path
{
get { return base.Path + ".B"; }
}
}
public class C : B
{
public override string Path
{
get { return base.Path + ".C"; }
}
}
A a = new A();
Console.WriteLine(a.Path); // Prints "A"
B b = new B();
Console.WriteLine(b.Path); // Prints "A.B"
C c = new C();
Console.WriteLine(c.Path); // Prints "A.B.C"
Update v1.1: Recursive approach (now includes getting a property value and setting a property value by a given object path)
Because you want to leave your model as is and go with the composition way, this is the piece of "magic" to dynamically get the whole path. Note that I've required a new FullPath property in order to avoid an infinite loop during path calculation (you can also try it in a DotNetFiddle):
using System;
using System.Linq;
using System.Reflection;
public abstract class Field
{
public double Value
{
get;
set;
}
public string Path
{
get;
protected set;
}
public string FullPath
{
get
{
return BuildPath(this);
}
}
/// <summary>
/// Recursively-builds a dot-separated full path of associated fields
/// </summary>
/// <param name="field">Optional, it's a reference to current associated field </param>
/// <param name="path">Optional, provided when this method enters to the first associated </param>
/// <returns>The whole dot-separated full path of associations to Field</returns>
private string BuildPath(Field field, string path = "")
{
// Top-level path won't start with dot
if (path != string.Empty)
{
path += '.';
}
path += field.Path;
// This will look for a property which is of type Field
PropertyInfo fieldProperty = field.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)
.SingleOrDefault(prop => prop.PropertyType.IsSubclassOf(typeof(Field)));
// If current field has a property of type Field...
if (fieldProperty != null)
{
// ...we'll get its value and we'll start a recursion to find the next Field.Path
path = BuildPath((Field)fieldProperty.GetValue(field, null), path);
}
return path;
}
/// <summary>
/// Recursively sets a value to an associated field property
/// </summary>
/// <param name="path">The whole path to the property</param>
/// <param name="value">The value to set</param>
/// <param name="associatedField">Optional, it's a reference to current associated field</param>
public void SetByPath(string path, object value, Field associatedField = null)
{
if (string.IsNullOrEmpty(path.Trim()))
{
throw new ArgumentException("Path cannot be null or empty");
}
string[] pathParts = path.Split('.');
if (associatedField == null)
{
associatedField = this;
}
// This will look for a property which is of type Field
PropertyInfo property = associatedField.GetType().GetProperty(pathParts[0], BindingFlags.Public | BindingFlags.Instance);
if (property == null)
{
throw new ArgumentException("A property in the path wasn't found", "path");
}
object propertyValue = property.GetValue(associatedField, null);
// If property value isn't a Field, then it's the last part in the path
// and it's the property to set
if (!propertyValue.GetType().IsSubclassOf(typeof(Field)))
{
property.SetValue(associatedField, value);
}
else
{
// ... otherwise, we navigate to the next associated field, removing the first
// part in the path, so the next call will look for the next property...
SetByPath(string.Join(".", pathParts.Skip(1)), value, (Field)propertyValue);
}
}
/// <summary>
/// Recursively gets a value from an associated field property
/// </summary>
/// <param name="path">The whole path to the property</param>
/// <param name="associatedField">Optional, it's a reference to current associated field</param>
/// <typeparam name="T">The type of the property from which the value is going to be obtained</typeparam>
public T GetByPath<T>(string path, Field associatedField = null)
{
if (string.IsNullOrEmpty(path.Trim()))
{
throw new ArgumentException("Path cannot be null or empty");
}
string[] pathParts = path.Split('.');
if (associatedField == null)
{
associatedField = this;
}
// This will look for a property which is of type Field
PropertyInfo property = associatedField.GetType().GetProperty(pathParts[0], BindingFlags.Public | BindingFlags.Instance);
if (property == null)
{
throw new ArgumentException("A property in the path wasn't found", "path");
}
object propertyValue = property.GetValue(associatedField, null);
// If property value isn't a Field, then it's the last part in the path
// and it's the property to set
if (!propertyValue.GetType().IsSubclassOf(typeof(Field)))
{
return (T)property.GetValue(associatedField, null);
}
else
{
// ... otherwise, we navigate to the next associated field, removing the first
// part in the path, so the next call will look for the next property...
return GetByPath<T>(string.Join(".", pathParts.Skip(1)), (Field)propertyValue);
}
}
}
public sealed class TotalAsset : Field
{
public TotalAsset(BuildingAsset buildingAsset)
{
Path = "TotalAsset";
BuildingAsset = buildingAsset;
}
public BuildingAsset BuildingAsset
{
get;
private set;
}
}
public sealed class BuildingAsset : Field
{
public HistoricalBuildingAsset HistoricalBuildingAsset
{
get;
private set;
}
public BuildingAsset(HistoricalBuildingAsset historicalBuildingAsset)
{
Path = "BuildingAsset";
this.HistoricalBuildingAsset = historicalBuildingAsset;
}
}
public sealed class HistoricalBuildingAsset : Field
{
public HistoricalBuildingAsset()
{
Path = "HistoricalBuildingAsset";
}
public int Age
{
get;
set;
}
}
public class Program
{
public static void Main()
{
TotalAsset total = new TotalAsset(new BuildingAsset(new HistoricalBuildingAsset()));
// Prints "TotalAsset.BuildingAsset.HistoricalBuildingAsset"
Console.WriteLine(total.FullPath);
total.SetByPath("BuildingAsset.HistoricalBuildingAsset.Age", 300);
// Prints "300" as expected!
Console.WriteLine(total.GetByPath<int>("BuildingAsset.HistoricalBuildingAsset.Age"));
}
}

You can re-use the existing .net framework Binding pattern and codebase. Your description of what you want to do sounds mightily like MVVM binding to me. The use of Binding in WPF is explained here http://msdn.microsoft.com/en-us/library/ms752347(v=vs.110).aspx.
Using System.Windows.Data.Binding gives you an extensible framework for getting data into and out of object graphs using relative and absolute string paths to nominate the class members and collection indexes.

Related

Set properties values with violating open-close principle validation

I have a ServiceField class which contains FieldId and Value as two properties .
public class ServiceField
{
public int FieldId { get; set; }
public string Value { get; set; }
}
/// <summary>
/// This method is used to assign value to HajjPackageFields class
/// </summary>
/// <param name="hajPackageObj">HajjPackageFields whose value needs to be updated</param>
private void UpdateDetailFieldsValue(HajjPackageFields hajPackageObj)
{
foreach (ServiceField field in GetPackageDetails(hajPackageObj.PackageId))
{
if (field.FieldId == (int)HajjServiceFieldsEnum.AccomodationView)
{
hajPackageObj.AccomodationView = field.Value == "1";
}
else if (field.FieldId == (int)HajjServiceFieldsEnum.AirTicket)
{
hajPackageObj.AirTicket = field.Value == "1";
}
}
}
Problem is if any new property add in HajjPackageField class than i have to modify my UpdateDetailFieldsValue method which is against the open close principle. Is there any other proper way to achieve this task ?
If I clearly understood you, reflection will help to solve problem:
private void UpdateDetailFieldsValue(HajjPackageFields hajPackageObj)
{
var pairs = new Dictionary<int, string>();
foreach (var enumVal in typeof(HajjServiceFieldsEnum).GetEnumValues())
pairs[(int)(HajjServiceFieldsEnum)enumVal] = typeof(HajjServiceFieldsEnum).GetEnumName(enumVal);
foreach (ServiceField field in GetPackageDetails(hajPackageObj.PackageId))
typeof(HajjPackageFields).GetProperty(pairs[field.FieldId])
?.SetValue(hajPackageObj, field.Value == "1");
}

Mapping a SharePoint list item to a C# class

Say I have:
public class SPListItem
{
public override object this[string fieldName]
{
get
{
return this.GetValue(fieldName);
}
set
{
this.SetValue(fieldName, value, !this.HasExternalDataSource);
}
}
}
public class Bar
{
public int Prop1 { get; set; }
public int Prop2 { get; set; }
public int Prop3 { get; set; }
}
is there any way I can do:
var fooInst = new SPListItem();
Bar barInst = (Bar)fooInst // or maybe Bar.FromFoo(Foo f) if handling the cast is not possible
and then have:
barInst.Prop1 give me the equivalent of:
fooInst["Prop"];
Without implementing the getters and setters for every property in Bar?
Aaaaaand, here we go. This class generates entities from your lists.
From: https://justsharepointthings.wordpress.com/2015/09/10/sharepoint-generate-c-poco-classes-from-an-existing-definition/
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.SharePoint;
namespace Code_Generation {
/// <summary>
/// Generates List object entities from a site connection.
/// </summary>
public class SPListPocoGenerator {
string parentDir = "GeneratedListPOCO/";
string hiddenDir = "GeneratedListPOCO/HiddenLists/";
private class PropertyString {
private string _propStr;
public PropertyString(string propStr) {
_propStr = propStr;
_properties = new Dictionary < string, string > ();
}
private Dictionary < string, string > _properties;
public string this[string key] {
get {
return _properties.ContainsKey(key) ? _properties[key] : string.Empty;
}
set {
if (_properties.ContainsKey(key)) {
_properties[key] = value;
} else {
_properties.Add(key, value);
}
}
}
/// <summary>
/// Replaces properties in the format {{propertyName}} in the source string with values from KeyValuePairPropertiesDictionarysupplied dictionary.nce you've set a property it's replaced in the string and you
/// </summary>
/// <param name="originalStr"></param>
/// <param name="keyValuePairPropertiesDictionary"></param>
/// <returns></returns>
public override string ToString() {
string modifiedStr = _propStr;
foreach(var keyvaluePair in _properties) {
modifiedStr = modifiedStr.Replace("{{" + keyvaluePair.Key + "}}", keyvaluePair.Value);
}
return modifiedStr;
}
}
public string _classDefinitionStr = #
"
using System;
using Microsoft.SharePoint;
public class {{EntityName}}
{
private SPListItem listItem;
public {{EntityName}}_InternalProperties InternalProperties
{
get; private set;
}
public {{EntityName}}(SPListItem li)
{
this.listItem = li;
this.InternalProperties = new {{EntityName}}_InternalProperties(this.listItem);
}
{{PropertySections}}
public class {{EntityName}}_InternalProperties
{
private SPListItem listItem;
public {{EntityName}}_InternalProperties(SPListItem li)
{
this.listItem = li;
}
{{HiddenPropertySections}}
{{InternalPropertySections}}
}
}";
private const string _propertySectionStr = "\n\n\t" + #
"public {{PropertyType}} {{PropertyName}}
{ get { return listItem[Guid.Parse("
"{{PropertyId}}"
")] as {{PropertyType}}; }
set { listItem[Guid.Parse("
"{{PropertyId}}"
")] = value; }}";
/// <summary>
/// Gets string identifying the field type
/// </summary>
/// <param name="field"></param>
/// <returns></returns>
private string GetSafeTypeName(SPField field) {
if (field.FieldValueType == null) {
return "object"; //Not going to try to parse it further, this is enough.
}
var type = field.FieldValueType;
if (type.IsValueType) {
return type.FullName + "?";
}
return type.FullName;
}
public void GenerateForWeb(SPWeb web) {
var blackList = new[] {
"Documents", "Form Templates", "Site Assets", "Site Pages", "Style Library"
};
Directory.CreateDirectory(parentDir);
Directory.CreateDirectory(hiddenDir);
foreach(SPList list in web.Lists) {
PropertyString _classDefinition = new PropertyString(_classDefinitionStr);
string entityName = "SPL_" + list.Title.Replace(" ", "");
_classDefinition["EntityName"] = entityName;
foreach(SPField field in list.Fields) {
PropertyString propertySection = new PropertyString(_propertySectionStr);
propertySection["PropertyType"] = GetSafeTypeName(field); //field.FieldValueType.FullName; -> Returning Null often. Thanks, SharePoint!
propertySection["PropertyName"] = field.EntityPropertyName.Replace("_x0020_", "_");
propertySection["PropertyId"] = field.Id.ToString();
if (SPBuiltInFieldId.Contains(field.Id)) _classDefinition["InternalPropertySections"] += propertySection;
else if (field.Hidden) _classDefinition["HiddenPropertySections"] += propertySection;
else _classDefinition["PropertySections"] += propertySection;
}
if (list.Hidden || blackList.Contains(list.Title)) {
File.WriteAllText(hiddenDir + entityName + ".cs", _classDefinition.ToString());
} else {
File.WriteAllText(parentDir + entityName + ".cs", _classDefinition.ToString());
}
}
}
}
}
For my former employer I implemented a DAO pattern for SharePoint. Unfortunately I was not allowed to take the code with me or publish it... I used annotations together with reflection to solve the issues with different names, optional fields, type casting etc. I also wrote a generator for the DTO-objects. But to be honest, it was a quite big effort for something that LINQ might solve in your case. Or writing your classes by hand, or writing a code generator for the getters and setters - it all depends on the size of your project(s).
Before implementing my own DAO I had a quite bad experience with LINQ to SQL, especially when columns were renamed, added or removed I didn't like the behaviour, I also had performance issues using it. That's why I prefered my own DAO pattern, but for easy tasks LINQ might be enough. My experience with LINQ also might be outdated, it was about 7 years ago ;)
You could use an ExpandoObject, which is in the System.Dynamic namespace. Try something like this (untested):
public class SPListItemPropertyMapper
{
private dynamic _expandoObject;
public SPListItemPropertyMapper(SPListItem listItem)
{
_expandoObject = new ExpandoObject();
foreach (SPField field in listItem.Fields)
{
_expandoObject.Add(field.InternalName, listItem.GetFormattedValue(field.InternalName));
}
}
public dynamic FieldValues
{
get { return _expandoObject; }
}
}
Usage:
SPListItem listItem; //your list item here
var propertyMapper = new SPListItemPropertyMapper(listItem);
var title = propertyMapper.FieldValues.Title;
var editor = propertyMapper.FieldValues.Editor;
var created = propertyMapper.FieldValues.Created;
etc. You should consider extending the foreach loop by more logic, to return values based on the field type instead of just using GetFormattedValue.

Problem dynamically adding XML items to listbox, C#?

I'm pulling feeds for my RSS project and I am running into a problem of not knowing how to allow the user to load more items into the collection. At the current moment, everything loads at once. While this is in some cases all right, I would like the user to be able to choose how things get loaded in case they have a slow mobile connection.
This is borrowed code and thus it only adds to my confusion.
Where could i be able to inject code into this sample to allow a dynamic loading of items, say, 30 at a time?
Rss Class:
namespace MyRSSService
{
public class RssService
{
/// Gets the RSS items.
/// <param name="rssFeed">The RSS feed.</param>
/// <param name="onGetRssItemsCompleted">The on get RSS items completed.</param>
/// <param name="onError">The on error.</param>
public static void GetRssItems(string rssFeed, Action<IList<RssItem>> onGetRssItemsCompleted = null, Action<Exception> onError = null, Action onFinally = null)
{
WebClient webClient = new WebClient();
// register on download complete event
webClient.OpenReadCompleted += delegate(object sender, OpenReadCompletedEventArgs e)
{
try
{
// report error
if (e.Error != null)
{
if (onError != null)
{
onError(e.Error);
}
return;
}
// convert rss result to model
IList<RssItem> rssItems = new List<RssItem>();
Stream stream = e.Result;
XmlReader response = XmlReader.Create(stream);
{
SyndicationFeed feeds = SyndicationFeed.Load(response);
foreach (SyndicationItem f in feeds.Items)
{
RssItem rssItem = new RssItem(f.Title.Text, f.Summary.Text, f.PublishDate.ToString(), f.Links[0].Uri.AbsoluteUri);
rssItems.Add(rssItem);
}
}
// notify completed callback
if (onGetRssItemsCompleted != null)
{
onGetRssItemsCompleted(rssItems);
}
}
finally
{
// notify finally callback
if (onFinally != null)
{
onFinally();
}
}
};
webClient.OpenReadAsync(new Uri(rssFeed));
}
}
}
items setting class:
namespace MyRSSService
{
public class RssItem
{
/// <summary>
/// Initializes a new instance of the <see cref="RssItem"/> class.
/// </summary>
/// <param name="title">The title.</param>
/// <param name="summary">The summary.</param>
/// <param name="publishedDate">The published date.</param>
/// <param name="url">The URL.</param>
public RssItem(string title, string summary, string publishedDate, string url)
{
Title = title;
Summary = summary;
PublishedDate = publishedDate;
Url = url;
// Get plain text from html
PlainSummary = HttpUtility.HtmlDecode(Regex.Replace(summary, "<[^>]+?>", ""));
}
public string Title { get; set; }
public string Summary { get; set; }
public string PublishedDate { get; set; }
public string Url { get; set; }
public string PlainSummary { get; set; }
}
}
the binding C# to the page to display the feeds
public partial class FeedPage : PhoneApplicationPage
{
private const string WindowsPhoneBlogPosts = "http://feeds.bbci.co.uk/news/rss.xml";
public FeedPage()
{
InitializeComponent();
RssService.GetRssItems(WindowsPhoneBlogPosts, (items) => { listbox.ItemsSource = items; }, (exception) => { MessageBox.Show(exception.Message); }, null);
}
}
Unless the server where the feed is hosted provides an API to limit the number of returned items (for example, this practice is used for the Xbox Marketplace), you will be downloading the entire feed, even if you decide to only show a part of it.

A type of generic list deserialization class?

OK, so here's the story so far.
I could already deserialize individual objects using XmlSerializer, but deserializing lists was proving to be a real headache. I started out by trying to serialize List<Foo> and the serializer serialized multiple <Foo> XML structures inside a root <ArrayOfFoo> element. That proved to be problematic to deserialize, so it looks like I needed to have defined the 'ArrayOfFoo' element myself. So, I've got a class working that is a 'wrapper' for the list, as shown in this program:
using System;
using System.IO;
using System.Collections.Generic;
using System.Xml.Serialization;
namespace XmlTester2
{
public class Program
{
static void Main(string[] args)
{
Console.WriteLine("XML tester...");
string xml =
"<ItemList xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\">" +
"<Person i:type=\"PersonI2\">" + "<Field1>field1Val</Field1>" +
"<Field2>field2Val</Field2>" + "<Field3>field3Val</Field3>" +
"<Field4>field4Val</Field4>" + "</Person>" +
"<Account i:type=\"AccountI2\">" + "<Field1>field1Val</Field1>" +
"<Field2>field2Val</Field2>" + "<Field3>field3Val</Field3>" +
"<Field4>field4Val</Field4>" + "</Account>" +
"<Person i:type=\"PersonI2\">" + "<Field1>field1Val</Field1>" +
"<Field2>field2Val</Field2>" + "<Field3>field3Val</Field3>" +
"<Field4>field4Val</Field4>" + "</Person>" + "</ItemList>";
XmlSerializer ser = new XmlSerializer(typeof(ItemList));
using (var reader = new StringReader(xml))
{
ItemList result = (ItemList)ser.Deserialize(reader);
}
Console.WriteLine("Break here and check 'result' in Quickwatch...");
Console.ReadKey();
}
}
[XmlRootAttribute(IsNullable = false)]
public class ItemList
{
[XmlElementAttribute("Person")]
public List<Person> Persons { get; set; }
[XmlElementAttribute("Account")]
public List<Account> Accounts { get; set; }
}
[XmlTypeAttribute(AnonymousType = false, TypeName = "Person", Namespace = "")]
[XmlInclude(typeof(PersonI2))]
public class Person
{
public string Field1 { get; set; }
public string Field2 { get; set; }
public string Field3 { get; set; }
}
[XmlTypeAttribute(AnonymousType = false, TypeName = "PersonI2", Namespace = "")]
public class PersonI2 : Person
{
public string Field4 { get; set; }
}
[XmlTypeAttribute(AnonymousType = false, TypeName = "Account", Namespace = "")]
[XmlInclude(typeof(AccountI2))]
public class Account
{
public string Field1 { get; set; }
public string Field2 { get; set; }
public string Field3 { get; set; }
}
[XmlTypeAttribute(AnonymousType = false, TypeName = "AccountI2", Namespace = "")]
public class AccountI2 : Account
{
public string Field4 { get; set; }
}
}
However, this 'wrapper', ItemList, still has to have manually defined in it all the elements that might be contained (in the example, Person and Account). What would be really ideal would be to have a generic list wrapper class. I know this is a bit hopeful, but would there be a way to do this? I'm thinking of something along these lines (this does not work, but is just to give you the general idea):
using System;
using System.IO;
using System.Collections.Generic;
using System.Xml.Serialization;
namespace XmlTester3
{
public class Program
{
static void Main(string[] args)
{
Console.WriteLine("XML tester...");
string xml =
"<ItemList xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\">" +
"<Person i:type=\"PersonI2\">" +
"<Field1>field1Val</Field1>" +
"<Field2>field2Val</Field2>" +
"<Field3>field3Val</Field3>" +
"<Field4>field4Val</Field4>" +
"</Person>" +
"<Person i:type=\"PersonI2\">" +
"<Field1>field1Val</Field1>" +
"<Field2>field2Val</Field2>" +
"<Field3>field3Val</Field3>" +
"<Field4>field4Val</Field4>" +
"</Person>" +
"</ItemList>";
XmlSerializer ser = new XmlSerializer(typeof(ItemList<Person>));
using (var reader = new StringReader(xml))
{
ItemList<Person> result = (ItemList<Person>)ser.Deserialize(reader);
}
Console.WriteLine("Break here and check 'result' in Quickwatch...");
Console.ReadKey();
}
}
[XmlRootAttribute(IsNullable = false)]
[XmlInclude(typeof(Person))]
[XmlInclude(typeof(PersonI2))]
[XmlInclude(typeof(Account))]
[XmlInclude(typeof(AccountI2))]
public class ItemList<T>
{
[XmlElementAttribute]
public List<T> Items { get; set; }
}
[XmlTypeAttribute(AnonymousType = false, TypeName = "Person", Namespace = "")]
[XmlInclude(typeof(PersonI2))]
public class Person
{
public string Field1 { get; set; }
public string Field2 { get; set; }
public string Field3 { get; set; }
}
[XmlTypeAttribute(AnonymousType = false, TypeName = "PersonI2", Namespace = "")]
public class PersonI2 : Person
{
public string Field4 { get; set; }
}
[XmlTypeAttribute(AnonymousType = false, TypeName = "Account", Namespace = "")]
[XmlInclude(typeof(AccountI2))]
public class Account
{
public string Field1 { get; set; }
public string Field2 { get; set; }
public string Field3 { get; set; }
}
[XmlTypeAttribute(AnonymousType = false, TypeName = "AccountI2", Namespace = "")]
public class AccountI2 : Account
{
public string Field4 { get; set; }
}
}
So, the XML structures passed inside the ItemList would only be able to be of one type, say Person in this example, and I could define an ItemList<Person> that would allow me to deserialize a list containing multiple Person objects? Any ideas? If necessary, I wouldn't mind having to tag the ItemList class with an [XmlInclude...] for every type that ItemList might contain a collection of.
I'm guessing this is possible, I just haven't figured out quite how? :-) Or is the default XmlSerializer too fussy?
You can do this easily enough, just implement the System.Xml.Serialization.IXmlSerializable interface. If I were doing this, I might even reflect the possible derived types of T in the assembly that defines T and completely omit the [XmlInclude] declarations. The real down side with this approach is the creation of the XmlSerializers. You might consider caching them. Anyway just use this in your second example and it should work.
BTW, that is an interesting thing your doing with the "i:type=\"PersonI2\""; props for figuring that one out ;)
[XmlRootAttribute("ItemList", IsNullable = false)]
[XmlInclude(typeof(Person))]
[XmlInclude(typeof(PersonI2))]
[XmlInclude(typeof(Account))]
[XmlInclude(typeof(AccountI2))]
public class ItemList<T> : System.Xml.Serialization.IXmlSerializable
{
class Map : Dictionary<String, XmlSerializer>
{ public Map() : base(StringComparer.Ordinal) { } }
public List<T> Items { get; set; }
public System.Xml.Schema.XmlSchema GetSchema()
{
return null;
}
private string TypeName(Type t)
{
String typeName = t.Name;
foreach (XmlTypeAttribute a in t.GetCustomAttributes(typeof(XmlTypeAttribute), true))
if (!String.IsNullOrEmpty(a.TypeName))
typeName = a.TypeName;
return typeName;
}
private Map LoadSchema()
{
Map map = new Map();
foreach (XmlIncludeAttribute inc in typeof(ItemList<T>).GetCustomAttributes(typeof(XmlIncludeAttribute), true))
{
Type t = inc.Type;
if (typeof(T).IsAssignableFrom(t))
map.Add(TypeName(t), new XmlSerializer(t));
}
return map;
}
public void ReadXml(System.Xml.XmlReader reader)
{
Map map = LoadSchema();
int depth = reader.Depth;
List<T> items = new List<T>();
if (!reader.IsEmptyElement && reader.Read())
{
while (reader.Depth > depth)
{
items.Add((T)map[reader.LocalName].Deserialize(reader));
}
}
this.Items = items;
}
public void WriteXml(System.Xml.XmlWriter writer)
{
Map map = LoadSchema();
foreach (T item in this.Items)
{
map[TypeName(item.GetType())].Serialize(writer, item);
}
}
}
I'm not sure I understand your question but do you know there's a XmlArrayItemAttribute.
[XmlArray("foos"), XmlArrayItem(typeof(Foo), ElementName = "foo")]
Under .NET 3.5 SP1 (Specificly SP1) you can use the Serializers from WCF to deserialize objects without specificly marking the class up with DataContract or Serializable attributes.
Almost any class should be able to be deserialized this way - as long as the Property names match the element names.
If you're getting deserializer errors - then it's possibly because of some misnamed property or an incorrect type. To check the input that the Serializer is looking for, you can populate an object once, and then serialize it down to XML to compare.
I wrote myself a helper class for using this a while back.
The way to use the helper is:
string serialized = "some xml";
MyType foo = Helpers.Deserialize<MyType>(serialized, SerializerType.Xml);
The actual helper class:
using System.IO;
using System.Runtime.Serialization; // System.Runtime.Serialization.dll (.NET 3.0)
using System.Runtime.Serialization.Json; // System.ServiceModel.Web.dll (.NET 3.5)
using System.Text;
namespace Serialization
{
public static class Helpers
{
/// <summary>
/// Declare the Serializer Type you want to use.
/// </summary>
public enum SerializerType
{
Xml, // Use DataContractSerializer
Json // Use DataContractJsonSerializer
}
public static T Deserialize<T>(string SerializedString, SerializerType UseSerializer)
{
// Get a Stream representation of the string.
using (Stream s = new MemoryStream(UTF8Encoding.UTF8.GetBytes(SerializedString)))
{
T item;
switch (UseSerializer)
{
case SerializerType.Json:
// Declare Serializer with the Type we're dealing with.
var serJson = new DataContractJsonSerializer(typeof(T));
// Read(Deserialize) with Serializer and cast
item = (T)serJson.ReadObject(s);
break;
case SerializerType.Xml:
default:
var serXml = new DataContractSerializer(typeof(T));
item = (T)serXml.ReadObject(s);
break;
}
return item;
}
}
public static string Serialize<T>(T ObjectToSerialize, SerializerType UseSerializer)
{
using (MemoryStream serialiserStream = new MemoryStream())
{
string serialisedString = null;
switch (UseSerializer)
{
case SerializerType.Json:
// init the Serializer with the Type to Serialize
DataContractJsonSerializer serJson = new DataContractJsonSerializer(typeof(T));
// The serializer fills the Stream with the Object's Serialized Representation.
serJson.WriteObject(serialiserStream, ObjectToSerialize);
break;
case SerializerType.Xml:
default:
DataContractSerializer serXml = new DataContractSerializer(typeof(T));
serXml.WriteObject(serialiserStream, ObjectToSerialize);
break;
}
// Rewind the stream to the start so we can now read it.
serialiserStream.Position = 0;
using (StreamReader sr = new StreamReader(serialiserStream))
{
// Use the StreamReader to get the serialized text out
serialisedString = sr.ReadToEnd();
sr.Close();
}
return serialisedString;
}
}
}
}
There are 2 main techniques for (de)serializing objects:
Implement an interface together with its Serialize() and Deserialize() methods for each class you want to (de)serialize - fast but requires a lot of maintenance.
Use a reflection based serizlier/deserializer that analizes the public fields and properties in your classes - slower but does not require maintaining (de)serialize() methods in each class.
Personally, in many cases, I prefer the 2nd technique.
.NET's built in XmlSerializer supports the 2nd technique, but has many limitations:
1 . Multi-deminsional arrays.
2 . Deserializing objects of unexpected types:
public MyClass
{
public IMyInterface MyProperty1
{
get;
set;
}
public MyBaseType MyProperty2
{
get;
set;
}
}
The types of the actual objects in MyProperty1, MyProperty2 is unknown during deserialization.
3 . (De)serializing complex collections.
4 . No good way to handle case where fields/properties were added/remove to/from class between serialization and deserialization.
5 . No support for serializing graphs with cycles.
The solution I came up with was to write a custom reflection based serializer/deserializer,
at the time I could not find any existing serializer, so I wrote a new one from scratch.
I can not publish it, since it is proprietary, however I noticed that afterwards simular serializers were published:
http://www.codeproject.com/KB/XML/GR_CustomXmlSerializer.aspx
XML Serialization and Inherited Types
http://www.codeproject.com/KB/XML/deepserializer.aspx
!THIS IS THE BEST SOLUTION I'VE FOUND!
OK, sorry for the answer-spam here, people, but I've found an even more elegant way of doing this that avoids the need for ItemList to have its items accessed using an 'Items' property; make the ItemList a List itself! This way, you just directly access ItemList as a list. Here's the amended example program:
using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
namespace XmlTester
{
public class Program {
static void Main(string[] args) {
Console.WriteLine("XML tester...");
// Valid XML for an ItemList of Person's
XmlSerializer ser = new XmlSerializer(typeof(ItemList<Person>));
string xmlIn =
#"<ItemList xmlns:i=""http://www.w3.org/2001/XMLSchema-instance"">
<PersonBilingual>
<FullName>John Smith</FullName>
<Age>21</Age>
<Language>French</Language>
<SecondLanguage>German</SecondLanguage>
</PersonBilingual>
<Person>
<FullName>Joe Bloggs</FullName>
<Age>26</Age>
<Language>English</Language>
</Person>
<Person i:type=""PersonBilingual"">
<FullName>Jane Doe</FullName>
<Age>78</Age>
<Language>Italian</Language>
<SecondLanguage>English</SecondLanguage>
</Person>
</ItemList>";
//// Valid XML for an ItemList of Account's
//XmlSerializer ser = new XmlSerializer(typeof(ItemList<Account>));
//string xmlIn =
//#"<ItemList xmlns:i=""http://www.w3.org/2001/XMLSchema-instance"">
// <AccountBank>
// <AcctName>Deposit account</AcctName>
// <WithCompany>Bank of Switzerland</WithCompany>
// <BalanceInEuros>300</BalanceInEuros>
// </AccountBank>
// <Account>
// <AcctName>Book buying account</AcctName>
// <WithCompany>Amazon</WithCompany>
// </Account>
// <Account i:type=""AccountBank"">
// <AcctName>Savings account</AcctName>
// <WithCompany>Bank of America</WithCompany>
// <BalanceInEuros>2500</BalanceInEuros>
// </Account>
//</ItemList>";
//// Invalid XML, as we have mixed incompatible types
//XmlSerializer ser = new XmlSerializer(typeof(ItemList<Person>));
//string xmlIn =
//#"<ItemList xmlns:i=""http://www.w3.org/2001/XMLSchema-instance"">
// <PersonBilingual>
// <FullName>John Smith</FullName>
// <Age>21</Age>
// <Language>French</Language>
// <SecondLanguage>German</SecondLanguage>
// </PersonBilingual>
// <Account>
// <AcctName>Book buying account</AcctName>
// <WithCompany>Amazon</WithCompany>
// </Account>
// <Person i:type=""PersonBilingual"">
// <FullName>Jane Doe</FullName>
// <Age>78</Age>
// <Language>Italian</Language>
// <SecondLanguage>English</SecondLanguage>
// </Person>
//</ItemList>";
// Deserialize...
ItemList<Person> result;
using (var reader = new StringReader(xmlIn)) {
result = (ItemList<Person>)ser.Deserialize(reader);
}
Console.WriteLine("Break here and check 'result' in Quickwatch...");
Console.ReadKey();
// Serialize...
StringBuilder xmlOut = new StringBuilder();
ser.Serialize(new StringWriter(xmlOut), result);
Console.WriteLine("Break here and check 'xmlOut' in Quickwatch...");
Console.ReadKey();
}
}
[XmlRoot(ElementName = "ItemList", IsNullable = false)]
[XmlInclude(typeof(Person))]
[XmlInclude(typeof(PersonBilingual))]
[XmlInclude(typeof(Account))]
[XmlInclude(typeof(AccountBank))]
public class ItemList<T> : List<T>, IXmlSerializable {
#region Private vars
/// <summary>
/// The class that will store our serializers for the various classes that may be (de)serialized, given
/// the type of this ItemList (ie. the type itself, as well as any type that extends the type)
/// </summary>
private class Map : Dictionary<string, XmlSerializer> { public Map() : base(StringComparer.Ordinal) { } }
#endregion
#region Private methods
/// <summary>
/// Creates a 'schema' for this ItemList, using its type, and the XmlIncludeAttribute types that are
/// associated with it. For each XmlIncludeAttribute, if it can be assigned to this ItemList's type (so
/// it's either the same type as this ItemList's type or a type that extends this ItemList's type), adds
/// the XmlSerializer for that XmlIncludeAttribute's type to our 'schema' collection, allowing a node
/// corresponding to that type to be (de)serialized by this ItemList.
/// </summary>
/// <returns>The 'schema' containing the XmlSerializer's available for this ItemList to use during (de)serialization.</returns>
private Map loadSchema() {
Map map = new Map();
foreach (XmlIncludeAttribute inc in typeof(ItemList<T>).GetCustomAttributes(typeof(XmlIncludeAttribute), true)) {
Type t = inc.Type;
if (typeof(T).IsAssignableFrom(t)) { map.Add(xmlTypeName(t), new XmlSerializer(t)); }
}
return map;
}
/// <summary>
/// As the XML type name can be different to our internal class name for that XML type, we need to be able
/// to expect an XML element name that is different to our internal class name for that XML type. Hence,
/// our 'schema' map will contain XmlSerializer's whose keys are based on the XML type name, NOT our
/// internal class name for that XML type. This method returns the XML type name given our internal
/// class we're using to (de)serialize that XML type. If no XML TypeName is specified in our internal
/// class's XmlTypeAttribute, we assume an XML type name identical to the internal class name.
/// </summary>
/// <param name="t">Our internal class used to (de)serialize an XML type.</param>
/// <returns>The XML type name corresponding to the given internal class.</returns>
private string xmlTypeName(Type t) {
string typeName = t.Name;
foreach (XmlTypeAttribute ta in t.GetCustomAttributes(typeof(XmlTypeAttribute), true)) {
if (!string.IsNullOrEmpty(ta.TypeName)) { typeName = ta.TypeName; }
}
return typeName;
}
#endregion
#region IXmlSerializable Members
/// <summary>
/// Reserved and should not be used.
/// </summary>
/// <returns>Must return null.</returns>
public XmlSchema GetSchema() {
return null;
}
/// <summary>
/// Generates a list of type T objects from their XML representation; stores them in this ItemList.
/// </summary>
/// <param name="reader">The System.Xml.XmlReader stream from which the objects are deserialized.</param>
public void ReadXml(XmlReader reader) {
Map map = loadSchema();
int depth = reader.Depth;
List<T> items = new List<T>();
if (!reader.IsEmptyElement && reader.Read()) {
// While the reader is at a greater depth that the initial depth, ie. at one of the elements
// inside the list wrapper, the initial depth being that of the list wrapper <ItemList>...
while (reader.Depth > depth) {
try { items.Add((T)map[reader.LocalName].Deserialize(reader)); }
catch (InvalidOperationException iopEx) {
if (
iopEx.InnerException != null &&
iopEx.InnerException.Message.StartsWith("The specified type was not recognized")
) { throw new InvalidOperationException("Couldn't deserialize node '" + reader.LocalName + "' because although its element node is a valid type, its attribute-specified type was not recognized. Perhaps it needs adding to the ItemList using XmlIncludeAttribute?", iopEx); }
}
catch (KeyNotFoundException knfEx) {
throw new InvalidOperationException("Couldn't deserialize node '" + reader.LocalName + "' because its element node was not recognized as a valid type. Perhaps it needs adding to the ItemList using XmlIncludeAttribute?", knfEx);
}
catch (Exception ex) {
throw ex;
}
}
}
this.AddRange(items);
}
/// <summary>
/// Converts a list of type T objects into their XML representation; writes them to the specified writer.
/// </summary>
/// <param name="writer">The System.Xml.XmlWriter stream to which the objects are serialized.</param>
public void WriteXml(XmlWriter writer) {
Map map = loadSchema();
foreach (T item in this) {
map[xmlTypeName(item.GetType())].Serialize(writer, item);
}
}
#endregion
}
/// <summary>
/// A regular person.
/// </summary>
[XmlType(AnonymousType = false, TypeName = "Person", Namespace = "")]
[XmlInclude(typeof(PersonBilingual))]
public class Person {
public string FullName { get; set; }
public int Age { get; set; }
public string Language { get; set; }
}
/// <summary>
/// A person who can speak a second language.
/// </summary>
[XmlType(AnonymousType = false, TypeName = "PersonBilingual", Namespace = "")]
public class PersonBilingual : Person {
public string SecondLanguage { get; set; }
}
/// <summary>
/// Some kind of account.
/// </summary>
[XmlType(AnonymousType = false, TypeName = "Account", Namespace = "")]
[XmlInclude(typeof(AccountBank))]
public class Account {
public string AcctName { get; set; }
public string WithCompany { get; set; }
}
/// <summary>
/// A bank account.
/// </summary>
[XmlType(AnonymousType = false, TypeName = "AccountBank", Namespace = "")]
public class AccountBank : Account {
public int BalanceInEuros { get; set; }
}
}
UPDATE: Please see the answer beginning !THIS IS THE BEST SOLUTION I'VE FOUND! - it's a better solution than this one.
...
Heavily inspired by csharptest.net's comment, I've created a class that pretty much does the job I wanted. :-) You access the deserialized items by checking ItemList.Items, and serialize stuff by inserting the items into ItemList.Items and then serializing it using an appropriate XmlSerializer. The only slight annoyance is that you must ensure that the ItemList class is tagged with an XmlIncludeAttribute for every class type that may need to be (de)serialized, or the XmlSerializer won't be able to deal with it.
Here's the example program, containing the generic ItemList class:
using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
namespace XmlTester
{
public class Program {
static void Main(string[] args) {
Console.WriteLine("XML tester...");
// Valid XML for an ItemList of Person's
XmlSerializer ser = new XmlSerializer(typeof(ItemList<Person>));
string xmlIn =
#"<ItemList xmlns:i=""http://www.w3.org/2001/XMLSchema-instance"">
<PersonBilingual>
<FullName>John Smith</FullName>
<Age>21</Age>
<Language>French</Language>
<SecondLanguage>German</SecondLanguage>
</PersonBilingual>
<Person>
<FullName>Joe Bloggs</FullName>
<Age>26</Age>
<Language>English</Language>
</Person>
<Person i:type=""PersonBilingual"">
<FullName>Jane Doe</FullName>
<Age>78</Age>
<Language>Italian</Language>
<SecondLanguage>English</SecondLanguage>
</Person>
</ItemList>";
//// Valid XML for an ItemList of Account's
//XmlSerializer ser = new XmlSerializer(typeof(ItemList<Account>));
//string xmlIn =
//#"<ItemList xmlns:i=""http://www.w3.org/2001/XMLSchema-instance"">
// <AccountBank>
// <AcctName>Deposit account</AcctName>
// <WithCompany>Bank of Switzerland</WithCompany>
// <BalanceInEuros>300</BalanceInEuros>
// </AccountBank>
// <Account>
// <AcctName>Book buying account</AcctName>
// <WithCompany>Amazon</WithCompany>
// </Account>
// <Account i:type=""AccountBank"">
// <AcctName>Savings account</AcctName>
// <WithCompany>Bank of America</WithCompany>
// <BalanceInEuros>2500</BalanceInEuros>
// </Account>
//</ItemList>";
//// Invalid XML, as we have mixed incompatible types
//XmlSerializer ser = new XmlSerializer(typeof(ItemList<Person>));
//string xmlIn =
//#"<ItemList xmlns:i=""http://www.w3.org/2001/XMLSchema-instance"">
// <PersonBilingual>
// <FullName>John Smith</FullName>
// <Age>21</Age>
// <Language>French</Language>
// <SecondLanguage>German</SecondLanguage>
// </PersonBilingual>
// <Account>
// <AcctName>Book buying account</AcctName>
// <WithCompany>Amazon</WithCompany>
// </Account>
// <Person i:type=""PersonBilingual"">
// <FullName>Jane Doe</FullName>
// <Age>78</Age>
// <Language>Italian</Language>
// <SecondLanguage>English</SecondLanguage>
// </Person>
//</ItemList>";
// Deserialize...
ItemList<Person> result;
using (var reader = new StringReader(xmlIn)) {
result = (ItemList<Person>)ser.Deserialize(reader);
}
Console.WriteLine("Break here and check 'result' in Quickwatch...");
Console.ReadKey();
// Serialize...
StringBuilder xmlOut = new StringBuilder();
ser.Serialize(new StringWriter(xmlOut), result);
Console.WriteLine("Break here and check 'xmlOut' in Quickwatch...");
Console.ReadKey();
}
}
[XmlRoot(ElementName = "ItemList", IsNullable = false)]
[XmlInclude(typeof(Person))]
[XmlInclude(typeof(PersonBilingual))]
[XmlInclude(typeof(Account))]
[XmlInclude(typeof(AccountBank))]
public class ItemList<T> : IXmlSerializable {
#region Private vars
/// <summary>
/// The class that will store our serializers for the various classes that may be (de)serialized, given
/// the type of this ItemList (ie. the type itself, as well as any type that extends the type)
/// </summary>
private class Map : Dictionary<string, XmlSerializer> { public Map() : base(StringComparer.Ordinal) { } }
#endregion
#region Private methods
/// <summary>
/// Creates a 'schema' for this ItemList, using its type, and the XmlIncludeAttribute types that are
/// associated with it. For each XmlIncludeAttribute, if it can be assigned to this ItemList's type (so
/// it's either the same type as this ItemList's type or a type that extends this ItemList's type), adds
/// the XmlSerializer for that XmlIncludeAttribute's type to our 'schema' collection, allowing a node
/// corresponding to that type to be (de)serialized by this ItemList.
/// </summary>
/// <returns>The 'schema' containing the XmlSerializer's available for this ItemList to use during (de)serialization.</returns>
private Map loadSchema() {
Map map = new Map();
foreach (XmlIncludeAttribute inc in typeof(ItemList<T>).GetCustomAttributes(typeof(XmlIncludeAttribute), true)) {
Type t = inc.Type;
if (typeof(T).IsAssignableFrom(t)) { map.Add(xmlTypeName(t), new XmlSerializer(t)); }
}
return map;
}
/// <summary>
/// As the XML type name can be different to our internal class name for that XML type, we need to be able
/// to expect an XML element name that is different to our internal class name for that XML type. Hence,
/// our 'schema' map will contain XmlSerializer's whose keys are based on the XML type name, NOT our
/// internal class name for that XML type. This method returns the XML type name given our internal
/// class we're using to (de)serialize that XML type. If no XML TypeName is specified in our internal
/// class's XmlTypeAttribute, we assume an XML type name identical to the internal class name.
/// </summary>
/// <param name="t">Our internal class used to (de)serialize an XML type.</param>
/// <returns>The XML type name corresponding to the given internal class.</returns>
private string xmlTypeName(Type t) {
string typeName = t.Name;
foreach (XmlTypeAttribute ta in t.GetCustomAttributes(typeof(XmlTypeAttribute), true)) {
if (!string.IsNullOrEmpty(ta.TypeName)) { typeName = ta.TypeName; }
}
return typeName;
}
#endregion
#region IXmlSerializable Members
/// <summary>
/// Reserved and should not be used.
/// </summary>
/// <returns>Must return null.</returns>
public XmlSchema GetSchema() {
return null;
}
/// <summary>
/// Generates a list of type T objects from their XML representation; stores them in this.Items.
/// </summary>
/// <param name="reader">The System.Xml.XmlReader stream from which the objects are deserialized.</param>
public void ReadXml(XmlReader reader) {
Map map = loadSchema();
int depth = reader.Depth;
List<T> items = new List<T>();
if (!reader.IsEmptyElement && reader.Read()) {
// While the reader is at a greater depth that the initial depth, ie. at one of the elements
// inside the list wrapper, the initial depth being that of the list wrapper <ItemList>...
while (reader.Depth > depth) {
try { items.Add((T)map[reader.LocalName].Deserialize(reader)); }
catch (InvalidOperationException iopEx) {
if (
iopEx.InnerException != null &&
iopEx.InnerException.Message.StartsWith("The specified type was not recognized")
) { throw new InvalidOperationException("Couldn't deserialize node '" + reader.LocalName + "' because although its element node is a valid type, its attribute-specified type was not recognized. Perhaps it needs adding to the ItemList using XmlIncludeAttribute?", iopEx); }
}
catch (KeyNotFoundException knfEx) {
throw new InvalidOperationException("Couldn't deserialize node '" + reader.LocalName + "' because its element node was not recognized as a valid type. Perhaps it needs adding to the ItemList using XmlIncludeAttribute?", knfEx);
}
catch (Exception ex) {
throw ex;
}
}
}
this.Items = items;
}
/// <summary>
/// Converts a list of type T objects into their XML representation; writes them to the specified writer.
/// </summary>
/// <param name="writer">The System.Xml.XmlWriter stream to which the objects are serialized.</param>
public void WriteXml(XmlWriter writer) {
Map map = loadSchema();
foreach (T item in this.Items) {
map[xmlTypeName(item.GetType())].Serialize(writer, item);
}
}
#endregion
#region Public properties
public List<T> Items { get; set; }
#endregion
}
/// <summary>
/// A regular person.
/// </summary>
[XmlType(AnonymousType = false, TypeName = "Person", Namespace = "")]
[XmlInclude(typeof(PersonBilingual))]
public class Person {
public string FullName { get; set; }
public int Age { get; set; }
public string Language { get; set; }
}
/// <summary>
/// A person who can speak a second language.
/// </summary>
[XmlType(AnonymousType = false, TypeName = "PersonBilingual", Namespace = "")]
public class PersonBilingual : Person {
public string SecondLanguage { get; set; }
}
/// <summary>
/// Some kind of account.
/// </summary>
[XmlType(AnonymousType = false, TypeName = "Account", Namespace = "")]
[XmlInclude(typeof(AccountBank))]
public class Account {
public string AcctName { get; set; }
public string WithCompany { get; set; }
}
/// <summary>
/// A bank account.
/// </summary>
[XmlType(AnonymousType = false, TypeName = "AccountBank", Namespace = "")]
public class AccountBank : Account {
public int BalanceInEuros { get; set; }
}
}
Thanks everyone for your help!

Serializing a Nullable<DateTime> in to XML

I am trying to serialize a class several of the data-members are Nullable objects, here is a example
[XmlAttribute("AccountExpirationDate")]
public Nullable<DateTime> AccountExpirationDate
{
get { return userPrincipal.AccountExpirationDate; }
set { userPrincipal.AccountExpirationDate = value; }
}
However at runtime I get the error
Cannot serialize member 'AccountExpirationDate' of type System.Nullable`1[System.DateTime]. XmlAttribute/XmlText cannot be used to encode complex types.
However I checked and Nullable is a SerializableAttribute. What am I doing wrong?
If you just want it to work, then perhaps:
using System;
using System.ComponentModel;
using System.Xml.Serialization;
public class Account
{
// your main property; TODO: your version
[XmlIgnore]
public Nullable<DateTime> AccountExpirationDate {get;set;}
// this is a shim property that we use to provide the serialization
[XmlAttribute("AccountExpirationDate")]
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public DateTime AccountExpirationDateSerialized
{
get {return AccountExpirationDate.Value;}
set {AccountExpirationDate = value;}
}
// and here we turn serialization of the value on/off per the value
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public bool ShouldSerializeAccountExpirationDateSerialized()
{
return AccountExpirationDate.HasValue;
}
// test it...
static void Main()
{
var ser = new XmlSerializer(typeof(Account));
var obj1 = new Account { AccountExpirationDate = DateTime.Today };
ser.Serialize(Console.Out, obj1);
Console.WriteLine();
var obj2 = new Account { AccountExpirationDate = null};
ser.Serialize(Console.Out, obj2);
}
}
This will only include the attribute when there is a non-null value.
You can only serialize it as an XmlElement, not as an XmlAttribute, as the representation is too complex for an attribute. That's what the exception is telling you.
I've used something like this many times.
[XmlIgnore]
public Nullable<DateTime> AccountExpirationDate
{
get { return userPrincipal.AccountExpirationDate; }
set { userPrincipal.AccountExpirationDate = value; }
}
///
/// <summary>Used for Xml Serialization</summary>
///
[XmlAttribute("AccountExpirationDate")]
public string AccountExpirationDateString
{
get
{
return AccountExpirationDate.HasValue
? AccountExpirationDate.Value.ToString("yyyy/MM/dd HH:mm:ss.fff")
: string.Empty;
}
set
{
AccountExpirationDate =
!string.IsNullOrEmpty(value)
? DateTime.ParseExact(value, "yyyy/MM/dd HH:mm:ss.fff")
: null;
}
}
I was stuck into the similar problem.
I had a datetime property (as XmlAttribute) in a class which was exposed in the WCF service.
Below is what I faced and the solution that worked for me :
1) XmlSerializer class was not serialising XmlAttribute of nullable type
[XmlAttribute]
public DateTime? lastUpdatedDate { get; set; }
Exception thrown : Cannot serialize member 'XXX' of type System.Nullable`1.
2) Some posts suggest to replace [XmlAttribute] with [XmlElement(IsNullable =true)]. But this will serialize the Attribute as an Element which is totally useless. However it works fine for XmlElements
3) Some suggest to implement IXmlSerializable interface into your class, but that doesn't allow WCF service to be called from WCF consuming application.
So this too does not work in this case.
Solution :
Don't mark property as nullable, instead use a ShouldSerializeXXX() method to put your constraint.
[XmlAttribute]
public DateTime lastUpdatedDate { get; set; }
public bool ShouldSerializelastUpdatedDate ()
{
return this.lastUpdatedDate != DateTime.MinValue;
// This prevents serializing the field when it has value 1/1/0001 12:00:00 AM
}
Define a Serializable that encapsulates your funcionality.
Here's are and example.
[XmlAttribute("AccountExpirationDate")]
public SerDateTime AccountExpirationDate
{
get { return _SerDateTime ; }
set { _SerDateTime = value; }
}
/// <summary>
/// Serialize DateTime Class (<i>yyyy-mm-dd</i>)
/// </summary>
public class SerDateTime : IXmlSerializable {
/// <summary>
/// Default Constructor when time is not avalaible
/// </summary>
public SerDateTime() { }
/// <summary>
/// Default Constructor when time is avalaible
/// </summary>
/// <param name="pDateTime"></param>
public SerDateTime(DateTime pDateTime) {
DateTimeValue = pDateTime;
}
private DateTime? _DateTimeValue;
/// <summary>
/// Value
/// </summary>
public DateTime? DateTimeValue {
get { return _DateTimeValue; }
set { _DateTimeValue = value; }
}
// Xml Serialization Infrastructure
void IXmlSerializable.WriteXml(XmlWriter writer) {
if (DateTimeValue == null) {
writer.WriteString(String.Empty);
} else {
writer.WriteString(DateTimeValue.Value.ToString("yyyy-MM-dd"));
//writer.WriteString(SerializeObject.SerializeInternal(DateTimeValue.Value));
}
}
void IXmlSerializable.ReadXml(XmlReader reader) {
reader.ReadStartElement();
String ltValue = reader.ReadString();
reader.ReadEndElement();
if (ltValue.Length == 0) {
DateTimeValue = null;
} else {
//Solo se admite yyyyMMdd
//DateTimeValue = (DateTime)SerializeObject.Deserialize(typeof(DateTime), ltValue);
DateTimeValue = new DateTime(Int32.Parse(ltValue.Substring(0, 4)),
Int32.Parse(ltValue.Substring(5, 2)),
Int32.Parse(ltValue.Substring(8, 2)));
}
}
XmlSchema IXmlSerializable.GetSchema() {
return (null);
}
}
#endregion
Simple workaround, which exposes one more property and is not very clean, but for simple cases works
public bool? Nullable { get; set; }
[XmlAttribute("nullable")]
public string NullableXml
{
get => Nullable == null ? null : (bool)Nullable ? "true" : "false";
set => Nullable = value == null ? (bool?)null : value == "true" ? true : value == "false" ? false : throw new Exception($"value {value} is not allowed");
}

Categories

Resources