|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionThis articles describes a problem I came across with The ultimate solution is actually relatively simple but it took me nearly a full Thursday to figure out, so the article is more about how I got to the solution and the dead-ends on the way there. BackgroundI wanted to be able to save a strongly-typed collection class to an XML file, but because of the way I wasn't the first to find this limitation (it has come up a number of times on various forums), and most people seemed to have worked around this by writing custom code to read and write an XML file, but I wanted a simpler solution. The ProblemFirst, I'll show you the three original classes I was working with:-
using System;
using System.IO;
using System.Xml.Serialization;
namespace Dashboard {
[Serializable]
public class ViewInfoCollection: ViewInfoCollectionBase {
#region Constructors
public ViewInfoCollection() {}
public ViewInfoCollection(int capacity): base(capacity) {}
public ViewInfoCollection(ViewInfoCollectionBase c): base(c){}
public ViewInfoCollection(ViewInfo[] a): base(a) {}
#endregion Constructors
#region Static
private static XmlSerializer Serializer {
get {
if (serializer == null) {
serializer = new XmlSerializer(typeof(ViewInfoCollection));
}
return serializer;
}
} static XmlSerializer serializer;
public static ViewInfoCollection FromXmlFile(string filename) {
ViewInfoCollection @new = new ViewInfoCollection();
@new.ReadFromXml(filename);
return @new;
}
#endregion Static
#region Methods
public void WriteToXml(string filename) {
using(StreamWriter writer = new StreamWriter(filename)) {
Serializer.Serialize(writer, this);
}
}
public void ReadFromXml(string filename) { ReadFromXml(filename, false); }
public void ReadFromXml(string filename, bool preserveItems) {
if (preserveItems == false) Clear();
using(StreamReader reader = new StreamReader(filename)) {
AddRange( (ViewInfoCollection) Serializer.Deserialize(reader));
}
}
#endregion Methods
}
}
using System;
using System.Xml.Serialization;
namespace Dashboard {
[Serializable]
public class ViewInfo {
public string Name {
get { return name; }
set { name = value; }
} string name;
public string Category {
get {
return category;
}
set { category = value; }
} string category;
public string ServiceProvider {
get { return serviceProvider; }
set { serviceProvider = value; }
} string serviceProvider;
public bool IsWellKnown {
get { return isWellKnown; }
set { isWellKnown = value; }
} bool isWellKnown;
public string FormType {
get { return formType; }
set { formType = value; }
} string formType;
public string[] AlternativeFormTypes {
get { return alternativeFormTypes; }
set { alternativeFormTypes = value; }
} string[] alternativeFormTypes;
public object UniqueID {
get {
if (uniqueID == null)
return Name;
else {
return uniqueID;
}
}
set { uniqueID = value; }
} object uniqueID;
public DashboardParams Parameters {
get { return parameters; }
set { parameters = value; }
} DashboardParams parameters;
public override string ToString() {
return string.Format("Name={0}, IsWellKnown={1}, " +
"UniqueID={2}, FormType={3}, AlternativeFormTypes={4}",
Name, IsWellKnown, UniqueID, FormType,
AlternativeFormTypes == null ? "(none)" : string.Join("; ",
AlternativeFormTypes));
}
}
}
using System;
using System.Xml.Serialization;
namespace Dashboard {
[Serializable]
public abstract class DashboardParams: IDashboardParams {
#region Properties
[XmlIgnore]
public ClientToken Token {
get { return token; }
} ClientToken token = ClientToken.Instance;
[XmlIgnore]
public DashboardMessageHandler MessageHandler {
get { return messageHandler; }
set { messageHandler = value; }
} DashboardMessageHandler messageHandler;
#endregion Properties
public string DisplayName {
get { return displayName; }
set { displayName = value; }
} string displayName;
public virtual bool IsValid {
get { return messageHandler != null; }
}
public virtual string UniqueID {
get {
return Guid.NewGuid().ToString();
}
}
}
}
I tried saving a test collection which contains two Unhandled Exception: System.InvalidOperationException:
There was an error generating the XML document. --->
System.InvalidOperationException:
The type Dashboard.DataWatchServices.DataWatcherParams was not expected.
Use the XmlInclude or SoapInclude attribute to specify types
that are not known statically."
The SolutionNot knowing much about .NET has two independent serialization paradigms designed for completely different serialization scenarios:-
In my scenario, the generated serializer knew about the Using the The next step to investigate was the options available in the constructor. It is possible to specify a list of types there, but although that eliminates the problem of knowing the derived types at compile-time, I would need to maintain a list and get any new class to 'register' with that list. Definitely over the top. I then looked at the other XML attributes available for controlling XML serialization. A promising option was the Type property on the Then I discovered This allows full control of the XML serialization process and allows a type to put any information it likes into the XML document, an example being Strictly speaking, it is for internal use only in v1.1 but is documented in v2.0. Anyway, if it's good enough for a The public XmlSchema GetSchema();
public void ReadXml(XmlReader reader);
public void WriteXml(XmlWriter writer);
Since we don't need a schema, I guessed (correctly as it turned out) that returning a Then I realized that there is actually a lot of complicated reflection work going inside the generated serializer. Although I could serialize simple properties such as All I really wanted to do was get control of serializing Then I remembered that during my Googling session, I saw a solution to a custom XML Serialization problem that, although not applicable to my problem, gave me another idea. (I can't find the original source now, but thanks to that guy anyway!). His problem was that I came up with this:- using System;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
namespace Dashboard {
public class DashboardParamsSerializer: IXmlSerializable {
#region Constructors
public DashboardParamsSerializer() {}
public DashboardParamsSerializer(DashboardParams parameters) {
this.parameters = parameters;
}
#endregion Constructors
#region Properties
public DashboardParams Parameters {
get { return parameters; }
} DashboardParams parameters;
#endregion Properties
#region IXmlSerializable Implementation
public XmlSchema GetSchema() {
return null;
}
public void ReadXml(XmlReader reader) {
Type type = Type.GetType(reader.GetAttribute("type"));
reader.ReadStartElement();
this.parameters = (DashboardParams) new
XmlSerializer(type).Deserialize(reader);
reader.ReadEndElement();
}
public void WriteXml(XmlWriter writer) {
writer.WriteAttributeString("type", parameters.GetType().ToString());
new XmlSerializer(parameters.GetType()).Serialize(writer, parameters);
}
#endregion IXmlSerializable Implementation
}
}
and I modified my [XmlIgnore]
public DashboardParams Parameters {
get { return parameters; }
set { parameters = value; }
} DashboardParams parameters;
[XmlElement("Parameters")]
public DashboardParamsSerializer XmlParameters {
get {
if (Parameters == null)
return null;
else {
return new DashboardParamsSerializer(Parameters);
}
}
set {
parameters = value.Parameters;
}
}
(The What is happening here is that the Because Deserialize works in reverse. The serializer will call BINGO! It worked a treat! The only fly in the ointment was that I now had an extra Then I had an Epiphany. Remember that I mentioned that the I added this section to #region Static
public static implicit operator DashboardParamsSerializer(DashboardParams p) {
return p == null ? null : new DashboardParamsSerializer(p);
}
public static implicit operator DashboardParams(DashboardParamsSerializer p) {
return p == null ? null : p.Parameters;
}
#endregion Static
changed the attribute on the [XmlElement(Type=typeof(DashboardParamsSerializer))]
and deleted the If So, I now had a single extra class and a single attribute that would allow me to serialize any class derived from So, I tested my solution to its logical conclusion and removed all customization from I changed the private static XmlSerializer Serializer {
get {
if (serializer == null) {
XmlAttributeOverrides attributeOverrides =
new XmlAttributeOverrides();
XmlAttributes attributes = new XmlAttributes();
XmlElementAttribute attribute = new
XmlElementAttribute(typeof(DashboardParamsSerializer));
attributes.XmlElements.Add(attribute);
attributeOverrides.Add(typeof(ViewInfo),
"Parameters", attributes);
serializer = new XmlSerializer(typeof(ViewInfoCollection),
attributeOverrides);
}
return serializer;
}
} static XmlSerializer serializer;
Again, this worked as expected! What we are doing here is telling the serializer that if it should come across a property or method called " SummarySo, now we have a way of being able to XmlSerialize an object that contains a read/write property (or Here is the final using System;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
namespace Dashboard {
public class DashboardParamsSerializer: IXmlSerializable {
#region Static
public static implicit operator
DashboardParamsSerializer(DashboardParams p) {
return p == null ? null : new DashboardParamsSerializer(p);
}
public static implicit operator
DashboardParams(DashboardParamsSerializer p) {
return p == null ? null : p.Parameters;
}
#endregion Static
#region Constructors
public DashboardParamsSerializer() {}
public DashboardParamsSerializer(DashboardParams parameters) {
this.parameters = parameters;
}
#endregion Constructors
#region Properties
public DashboardParams Parameters {
get { return parameters; }
} DashboardParams parameters;
#endregion Properties
#region IXmlSerializable Implementation
public XmlSchema GetSchema() {
return null;
}
public void ReadXml(XmlReader reader) {
Type type = Type.GetType(reader.GetAttribute("type"));
reader.ReadStartElement();
this.parameters = (DashboardParams) new
XmlSerializer(type).Deserialize(reader);
reader.ReadEndElement();
}
public void WriteXml(XmlWriter writer) {
writer.WriteAttributeString("type", parameters.GetType().ToString());
new XmlSerializer(parameters.GetType()).Serialize(writer, parameters);
}
#endregion IXmlSerializable Implementation
}
}
AddendumAnother buglet that I spotted during later testing was that the public bool ShouldSerializeUniqueID() { return uniqueID != null; }
| ||||||||||||||||||||