I plan to set up configuration keys for FormFields, QueryString parameters etc. In my web.config I have setting as follows:
<WhiteListPaametersGroup>
<WhiteListPaameters>
<FormField1>EVENTVALIDATION</FormField1>
<FormField2>VIEWSTATE</FormField2>
<FormField3>Button1</FormField3>
<QueryString1>firstname</QueryString1>
<QueryString2>lastname</QueryString2>
</WhiteListPaameters>
</WhiteListPaametersGroup>
Then in my code, I read the value as follows:
Dictionary<string, string> parameters = new Dictionary<string,string>();
foreach (XmlNode n in section.ChildNodes)
{
parameters.Add(n.Name, n.InnerText);
}
Is there a better way of storing this. Later on, I want to be able to go through dictionary like object, and be able to get settings for FormFields, Querystrings etc.
Please let me know if I could write it cleaner.
Thanks
You can use XML serialization to store your settings and restore them directly into an object. It's not perfect but very easy to set up and gives you object save/restore.
Have this class (properties must be public):
public class WhiteListParameters
{
public string FormField1 { get; set; }
public string FormField2 { get; set; }
public string FormField3 { get; set; }
public string QueryString1 { get; set; }
public string QueryString2 { get; set; }
}
To save it to XML file run this code:
WhiteListParameters parms = new WhiteListParameters
{
FormField1 = "EVENTVALIDATION",
FormField2 = "VIEWSTATE",
FormField3 = "Button1",
QueryString1 = "firstname",
QueryString2 = "lastname"
};
using(StreamWriter sw = new StreamWriter("C:\\temp\\config.xml"))
{
XmlSerializer xs = new XmlSerializer(typeof (WhiteListParameters));
xs.Serialize(sw, parms);
sw.Close();
}
To read it back into an object:
using(StreamReader sr = new StreamReader("c:\\temp\\config.xml"))
{
XmlSerializer xs = new XmlSerializer(typeof (WhiteListParameters));
WhiteListParameters parms = (WhiteListParameters) xs.Deserialize(sr);
sr.Close();
}
For reading configuration, .NET Framework has many classes in System.Configuration namespace. The most important classes are ConfigurationSection and ConfigurationElement. Derive from these classes, add properties you want and decorate them with ConfigurationProperty attribute. Advantage of this approach is type-safety, possibility to define list of valid and the fact, that .NET Framework framework will do all reading, parsing and checking of values from web.config for you automatically.
One option you might look into is creating a custom configuration section, which you can register in the web.config.
Here's a configuration section that I created for referencing our XSLT templates:
namespace Foo.Web.Applications.CustomConfigurationSections
{
public class XslTemplateConfiguration : ConfigurationSection
{
[ConfigurationProperty("xslTemplates")]
public XslTemplateElementCollection XslTemplates
{
get { return this["xslTemplates"] as XslTemplateElementCollection; }
}
}
public class XslTemplateElement : ConfigurationElement
{
[ConfigurationProperty("name", IsRequired = true, IsKey = true)]
public string Name
{
get { return this["name"] as string; }
set { this["name"] = value; }
}
[ConfigurationProperty("path", IsRequired = true)]
public string Path
{
get { return this["path"] as string; }
set { this["path"] = value; }
}
}
public class XslTemplateElementCollection : ConfigurationElementCollection
{
public XslTemplateElement this[object key]
{
get { return base.BaseGet(key) as XslTemplateElement; }
}
public override ConfigurationElementCollectionType CollectionType
{
get { return ConfigurationElementCollectionType.BasicMap; }
}
protected override string ElementName
{
get { return "xslTemplate"; }
}
protected override bool IsElementName(string elementName)
{
bool isName = false;
if (!String.IsNullOrEmpty(elementName))
isName = elementName.Equals("xslTemplate");
return isName;
}
protected override ConfigurationElement CreateNewElement()
{
return new XslTemplateElement();
}
protected override object GetElementKey(ConfigurationElement element)
{
return ((XslTemplateElement)element).Name;
}
}
}
You can register this section in the web.config as follows:
<configSections>
<section name="xslTemplateConfiguration" type="Foo.Web.Applications.CustomConfigurationSections.XslTemplateConfiguration"/>
</configSections>
And you can access the collection in your code like this:
var config = WebConfigurationManager.OpenWebConfiguration("/");
if (config.HasFile)
{
var templates = config.GetSection("xslTemplateConfiguration") as XslTemplateConfiguration;
if (templates != null)
{
var templatePath = templates.XslTemplates["PO"].Path;
}
}
Related
I'd like to generate some XML like the following with C# code.
<card>
<name>Cool Card</name>
<set rarity="common">S1</set>
</card>
I have something like this.
public class card
{
public string name = "Cool Card";
[XmlAttribute]
public string rarity = "common";
public string set = "S1";
}
public static void WriteXml()
{
var serializer = new XmlSerializer(typeof(card));
var myCard = new();
using var sw = new StringWriter();
serializer.Serialize(sw, myCard);
XDocument doc = XDocument.Parse(sw.ToString());
XmlWriterSettings xws = new();
xws.OmitXmlDeclaration = true;
xws.Indent = true;
using var xw = XmlWriter.Create(path, xws);
doc.Save(xw);
}
The problem is that this doesn't add the "rarity" attribute to the "set" value. Trying to add [XmlAttribute] adds it to the parent element rather than the next sibling element and I can't figure out how to get it on a plain string element, so at present my output looks like.
<card rarity="common">
<name>Cool Card</name>
<set>S1</set>
</card>
The XML example doc shows an example of how to set the attribute on an element, but only one with nested fields and not one that's a plain string. Is it possible to add an attribute to a plain old string element in XML like my first posted example demonstrates?
Try this:
public class card
{
public string name = "Cool Card";
public Set set = new();
}
public class Set
{
[XmlText]
public string value = "S1";
[XmlAttribute]
public string rarity = "common";
}
If you think about it there is no other way the xml attribute can be applied only to the element it is declared in. So you need to move it into another class. When you do this new property for value is required and that one needs to be flattened as value node is not required for that you need XmlText attribute.
The cleanest option in this case is implement IXmlSerializable:
public class card : IXmlSerializable
{
public string name = "Cool Card";
public string rarity = "common";
public string set = "S1";
public System.Xml.Schema.XmlSchema GetSchema()
{
return null;
}
public void ReadXml(System.Xml.XmlReader reader)
{
reader.ReadStartElement(nameof(card));
while (reader.NodeType != System.Xml.XmlNodeType.EndElement)
{
if (reader.Name == nameof(name))
{
this.name = reader.ReadElementContentAsString();
}
else if (reader.Name == nameof(set))
{
this.rarity = reader.GetAttribute(nameof(rarity));
this.set = reader.ReadElementContentAsString();
}
}
reader.ReadEndElement();
}
public void WriteXml(System.Xml.XmlWriter writer)
{
writer.WriteElementString(nameof(name), this.name);
writer.WriteStartElement(nameof(set));
writer.WriteAttributeString(nameof(rarity), this.rarity);
writer.WriteString(this.set);
writer.WriteEndElement();
}
}
If your class is big and you only need do a bit change in the XML, sometimes implement IXmlSerializable is a mess (you must save all the properties and only in one or two, do a bit change). In these cases, you can use attributes and some small helpers classes to get same results. The idea is tell to XML serializer that don't serialize some property and use another fake property to do the serialization.
For example, create a Set class with your desired XML structure:
public class XmlSet
{
private readonly card _card;
public XmlSet()
{
this._card = new card();
}
public XmlSet(card card)
{
this._card = card;
}
[XmlText]
public string set
{
get { return this._card.set; }
set { this._card.set = value; }
}
[XmlAttribute]
public string rarity
{
get { return this._card.rarity; }
set { this._card.rarity = value; }
}
}
It's only a wrapper, with the XML sttributes that you want. You get/set the values from/to your card object.
Then, in your card class, Ignore the set property and serialize the fake property:
public class card
{
public string name = "Cool Card";
[XmlIgnore]
public string rarity = "common";
[XmlIgnore]
public string set = "S1";
// Added to serialization
private XmlSet _xmlSetNode;
[XmlElement("set")]
public XmlSet XmlSet
{
get
{
this._xmlSetNode = this._xmlSetNode ?? new XmlSet(this);
return this._xmlSetNode;
}
set { this._xmlSetNode = value; }
}
}
I have a C# app that uses a custom section for configuration. I have that section of XML defined as a string. The string looks like this:
var xml = #"<departments>
<department id=""1"" name=""Sporting Goods"">
<products>
<product name=""Basketball"" price=""9.99"">
<add key=""Color"" value=""Orange"" />
<add key=""Brand"" value=""[BrandName]"" />
</product>
</products>
</department>
</departments>";
This XML matches the schema defined by the classes I described here. When I pass the above string to the Departments.Deserialize method, I receive an error. The error says: "Unrecognized element 'add'". The debugger jumps to this line in my Departments class.
public void ReadXml(XmlReader reader)
{
this.DeserializeElement(reader, false);
}
I assume that the error is referring to the 'add' elements in the 'product' element. However, the Product ConfigurationElement has a property named KeyValueConfigurationCollection Items. For that reason, it seems that add would work.
Why am I getting this error how do I fix my code so that the XML string shown above can be deserialized?
Update: Long story short - wrong serializer.
Most of the code works, except that particular deserialize method.
I was able to rectify original solution (here's where I found inspiration).
The section that I've corrected and it worked (don't forget IXmlSerializable from original post):
private static string sectionName = "departments";
public static Departments Deserialize(string xml)
{
XmlSerializer serializer = new XmlSerializer(typeof(Departments), new XmlRootAttribute(sectionName));
Departments departments = null;
var xdoc = XDocument.Parse(xml);
departments = (Departments)serializer.Deserialize(xdoc.CreateReader());
//var serializer = new XmlSerializer(typeof(Departments));
//using (var reader = new StringReader(xml))
//{
// departments = (Departments)(serializer.Deserialize(reader));
//}
return departments;
}
Hopefully it will help you moving on. At least works on my machine. Let me know if full code listing needed.
[Keeping original solution, plain reading of suggested problematic xml block]
Test client.
class Program
{
static void Main(string[] args)
{
string str = #"<departments>
<department id = ""1"" name = ""Sporting Goods"">
<products>
<product name=""Basketball"" price=""9.99"">
<add key = ""Color"" value = ""Orange""/>
<add key = ""Brand"" value = ""[BrandName]""/>
</product>
</products>
</department>
</departments>";
XmlDocument xmlDoc = LoadXmlsFromString(str);
string productXpath = "descendant-or-self::product";
var nodes = ExtractNodes(xmlDoc, productXpath);
foreach (XmlNode childrenNode in nodes)
{
var node = childrenNode.SelectSingleNode("descendant-or-self::*")?.OuterXml;
var obj = Product.Deserialize(node);
}
}
private static XmlNodeList ExtractNodes(XmlDocument xmlDoc, string xpath)
{
var nodes = xmlDoc.SelectNodes(xpath);
return nodes;
}
private static XmlDocument LoadXmlsFromString(string str)
{
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(str);
return xmlDoc;
}
}
Reading product is implemented by adding [XmlElement("add")] attribute for the list of add elements (and element-object to convert into).
[Serializable, XmlRoot("add")]
public class KeyValue
{
[XmlAttribute(AttributeName = "key")]
public string Key { get; set; }
[XmlAttribute(AttributeName = "value")]
public string Value { get; set; }
public static KeyValue Deserialize(string xml)
{
KeyValue keyValue = null;
var serializer = new XmlSerializer(typeof(KeyValue));
using (var reader = new StringReader(xml))
{
keyValue = (KeyValue)(serializer.Deserialize(reader));
}
return keyValue;
}
}
[Serializable, XmlRoot("product")]
public class Product
{
[XmlAttribute(AttributeName = "name")]
public string Name { get; set; }
[XmlAttribute(AttributeName = "price")]
public decimal Price { get; set; }
[XmlElement("add")]
public List<KeyValue> KeyValueList { get; set; }
public static Product Deserialize(string xml)
{
Product product = null;
var serializer = new XmlSerializer(typeof(Product));
using (var reader = new StringReader(xml))
{
product = (Product)(serializer.Deserialize(reader));
}
return product;
}
}
In my opinion an XmlSerializer is not needed for the configurations you have to read. All the tools that System.Configuration provides you are more than enough. Here I rewrote your configuration classes (the basic structure):
public class DepartmentsConfiguration : ConfigurationSection
{
[ConfigurationProperty("departments", IsRequired = false, IsDefaultCollection = true)]
public DepartmentItemCollection Departments
{
get
{
var departments = this["departments"] as DepartmentItemCollection;
return departments;
}
set
{
this["departments"] = value;
}
}
}
[ConfigurationCollection(typeof(DepartmentItemCollection), AddItemName = "department")]
public class DepartmentItemCollection : ConfigurationElementCollection
{
protected override ConfigurationElement CreateNewElement()
{
return new Department();
}
protected override object GetElementKey(ConfigurationElement element)
{
return ((Department)element).Name;
}
}
public class Department : ConfigurationElement
{
[ConfigurationProperty("id", IsRequired = false, IsKey = true)]
public int Id
{
get
{
return (int)(this["id"]);
}
set
{
this["id"] = value;
}
}
[ConfigurationProperty("name", IsRequired = true, IsKey = true, DefaultValue = "")]
public string Name
{
get
{
return (string)(this["name"]);
}
set
{
this["name"] = value;
}
}
[ConfigurationProperty("products", IsRequired = false, IsKey = false, IsDefaultCollection = false)]
public ProductCollection Products
{
get
{
return (ProductCollection)this["products"];
}
set
{
this["products"] = value;
}
}
}
[ConfigurationCollection(typeof(ProductCollection), AddItemName = "product")]
public class ProductCollection : ConfigurationElementCollection
{
protected override ConfigurationElement CreateNewElement()
{
return new Product();
}
protected override object GetElementKey(ConfigurationElement element)
{
return ((Product)element).Name;
}
}
public class Product : ConfigurationElement
{
[ConfigurationProperty("name", IsRequired = true, IsKey = true, DefaultValue = "")]
public string Name
{
get
{
return (string)(this["name"]);
}
set
{
this["name"] = value;
}
}
[ConfigurationProperty("price", IsRequired = false)]
public decimal Price
{
get
{
return (decimal)(this["price"]);
}
set
{
this["price"] = value;
}
}
[ConfigurationProperty("", IsRequired = false, IsKey = false, IsDefaultCollection = true)]
[ConfigurationCollection(typeof(KeyValueConfigurationCollection), AddItemName = "add")]
public KeyValueConfigurationCollection Items
{
get
{
var items = base[""] as KeyValueConfigurationCollection;
return items;
}
set
{
base[""] = value;
}
}
}
The <configSections> in App.config:
<configSections>
<section
name="departmentConfiguration"
type="Test.DepartmentsConfiguration, Test"
allowLocation="true"
allowDefinition="Everywhere"
/>
</configSections>
<departmentConfiguration>
<departments>
<department id="1" name="Sporting Goods">
<products>
<product name="Basketball" price="9.99">
<add key="Color" value="Orange" />
<add key="Brand" value="[BrandName]" />
</product>
</products>
</department>
</departments>
</departmentConfiguration>
And how to read it using the ConfigurationManager:
DepartmentsConfiguration config = (DepartmentsConfiguration) ConfigurationManager
.GetSection("departmentConfiguration");
foreach (Department department in config.Departments)
{
Console.WriteLine($"{department.Id}, {department.Name}");
foreach (Product product in department.Products)
{
Console.WriteLine($"{product.Name}, {product.Price}");
foreach (string key in product.Items.AllKeys)
{
Console.WriteLine($"{key} -> {product.Items[key].Value}");
}
}
}
I know it is not consistent with your question, take it as a personal advice.
How about using System.Xml.Linq's XDocument ?
Personal Pros:
Simple
No 3rd Party
Easy to read, for smaller files
Allows Linq Syntax
Personal Cons:
Tends towards spaghetti code in bigger files
Not the fastest solution
Example
static void Main(string[] args)
{
var xml =
#"<departments>
<department id=""1"" name=""Sporting Goods"">
<products>
<product name=""Basketball"" price=""9.99"">
<add key=""Color"" value=""Orange"" />
<add key=""Brand"" value=""[BrandName]"" />
</product>
</products>
</department>
</departments>";
var xDoc = XDocument.Load(new StringReader(xml));
var adds = xDoc.Root.Elements("department")
.Elements("products")
.Elements("product")
.Elements("add")
.Select(s => new KeyValuePair<string, string>(s.Attribute("key").ToString(), s.Attribute("value").ToString()))
.ToList();
foreach (var department in xDoc.Root.Elements("department"))
{
Console.WriteLine("department: {0}", department.Attribute("name"));
foreach (var products in department.Elements("products"))
{
foreach (var product in products.Elements("product"))
{
Console.WriteLine("product: {0}", product.Attribute("name"));
foreach (var config in product.Elements("add"))
{
Console.WriteLine("add: {0} -> {1}", config.Attribute("key"), config.Attribute("value"));
}
}
}
}
I have an xml like so:
<Settings>
<User default="Programmer"></User>
<Level default="2"></Level>
<Settings>
This is deserialized to an object of type UserSettings:
[Serializable]
[XmlRoot("Settings")]
public class UserSettings
{
[XmlElement("User")]
public string User { get; set; }
[XmlElement("Level")]
public string Level { get; set; }
}
The UserSettings object gives whatever the values are there for the tags at runtime.
I want the class to return the default attribute value when either the tag is empty or the tag is absent in the incoming xml.
So if there is an object objUserSettings of type UserSettings then
objUserSettings.User
should give "Programmer", or whatever is in default attribute value in the xml if the tag User is empty.
Regards.
Adding another answer because I had some fun with this question. Take it or leave it, but this is probably how I would attack this feature.
Here's an answer that is more complicated, but it gives you type safety using generics and most of the heavy lifting is done in one base class (no need to copy/paste the same code over and over).
Added a property to UserSettings to show an example of another type...
[Serializable]
[XmlRoot("Settings")]
public class UserSettings
{
public UserSettings()
{
User = new DefaultableStringValue();
Level = new DefaultableIntegerValue();
IsFullscreen = new DefaultableBooleanValue();
}
[XmlElement("User")]
public DefaultableStringValue User { get; set; }
[XmlElement("Level")]
public DefaultableIntegerValue Level { get; set; }
[XmlElement("IsFullscreen")]
public DefaultableBooleanValue IsFullscreen { get; set; }
}
Simple implementations of typed DefaultableValues...
[Serializable]
public class DefaultableStringValue : DefaultableValue<string>
{
public DefaultableStringValue() : base(s => s) { }
}
[Serializable]
public class DefaultableIntegerValue : DefaultableValue<int>
{
public DefaultableIntegerValue() : base(int.Parse) { }
}
[Serializable]
public class DefaultableBooleanValue : DefaultableValue<bool>
{
public DefaultableBooleanValue() : base(bool.Parse) { }
}
Base class that does all of the heavy lifting of parsing and caching parsed values...
[Serializable]
public abstract class DefaultableValue<T>
{
protected Func<string, T> _parsingFunc;
private string _valueText;
private T _cachedValue;
private bool _isValueCached;
private string _defaultText;
private T _cachedDefault;
private bool _isDefaultCached;
protected DefaultableValue(Func<string, T> parsingFunc)
{
_parsingFunc = parsingFunc;
_isValueCached = false;
_isDefaultCached = false;
}
[XmlAttribute("default")]
public string DefaultText
{
get { return _defaultText; }
set
{
_defaultText = value;
_isDefaultCached = false;
}
}
[XmlText]
public string ValueText
{
get { return _valueText; }
set
{
_valueText = value;
_isValueCached = false;
}
}
[XmlIgnore]
public T Default
{
get
{
if (_isDefaultCached)
return _cachedDefault;
if (HasDefault)
return ParseAndCacheValue(DefaultText, out _cachedDefault, out _isDefaultCached);
return default(T);
}
set
{
DefaultText = value.ToString();
_cachedDefault = value;
_isDefaultCached = true;
}
}
[XmlIgnore]
public T Value
{
get
{
if (_isValueCached)
return _cachedValue;
if (HasValue)
return ParseAndCacheValue(ValueText, out _cachedValue, out _isValueCached);
return Default;
}
set
{
ValueText = value.ToString();
_cachedValue = value;
_isValueCached = true;
}
}
[XmlIgnore]
public bool HasDefault { get { return !string.IsNullOrEmpty(_defaultText); } }
[XmlIgnore]
public bool HasValue { get { return !string.IsNullOrEmpty(_valueText); } }
private T ParseAndCacheValue(string text, out T cache, out bool isCached)
{
cache = _parsingFunc(text);
isCached = true;
return cache;
}
}
And a sample program to demonstrate usage...
public class Program
{
private static void Main(string[] args)
{
UserSettings userSettings = new UserSettings();
userSettings.User.Default = "Programmer";
userSettings.Level.Default = 2;
userSettings.Level.Value = 99;
XmlSerializer xmlSerializer = new XmlSerializer(typeof(UserSettings));
string serializedUserSettings;
using (StringWriter stringWriter = new StringWriter())
{
xmlSerializer.Serialize(stringWriter, userSettings);
serializedUserSettings = stringWriter.GetStringBuilder().ToString();
}
UserSettings deserializedUserSettings;
using (StringReader stringReader = new StringReader(serializedUserSettings))
{
deserializedUserSettings = (UserSettings)xmlSerializer.Deserialize(stringReader);
}
Console.Out.WriteLine("User: HasDefault={0}, Default={1}, HasValue={2}, Value={3}",
deserializedUserSettings.User.HasDefault ? "Yes" : "No",
deserializedUserSettings.User.Default,
deserializedUserSettings.User.HasValue ? "Yes" : "No",
deserializedUserSettings.User.Value);
Console.Out.WriteLine("Level: HasDefault={0}, Default={1}, HasValue={2}, Value={3}",
deserializedUserSettings.Level.HasDefault ? "Yes" : "No",
deserializedUserSettings.Level.Default,
deserializedUserSettings.Level.HasValue ? "Yes" : "No",
deserializedUserSettings.Level.Value);
Console.Out.WriteLine("IsFullscreen: HasDefault={0}, Default={1}, HasValue={2}, Value={3}",
deserializedUserSettings.IsFullscreen.HasDefault ? "Yes" : "No",
deserializedUserSettings.IsFullscreen.Default,
deserializedUserSettings.IsFullscreen.HasValue ? "Yes" : "No",
deserializedUserSettings.IsFullscreen.Value);
Console.ReadLine();
}
}
Try this
using System.ComponentModel;
[Serializable]
[XmlRoot("Settings")]
public class UserSettings
{
[DefaultValue("Yogesh")]
[XmlElement("User")]
public string User { get; set; }
[DefaultValue("1st")]
[XmlElement("Level")]
public string Level { get; set; }
}
For more info see this.
You can use Default Value attribute for the property.
In you case it will be,
[Serializable]
[XmlRoot("Settings")]
public class UserSettings
{
[XmlElement("User")]
[DefaultValue("Programmer")]
public string User { get; set; }
[XmlElement("Level")]
[DefaultValue(2)]
public string Level { get; set; }
}
I don't believe there is a way to tell string to use that default xml attribute. You will have to deserialize each of those object into a structure that has the default value as a property which is an xml attribute.
Here's an example...
[Serializable]
[XmlRoot("Settings")]
public class UserSettings
{
[XmlElement("User")]
public DefaultableValue User { get; set; }
[XmlElement("Level")]
public DefaultableValue Level { get; set; }
}
[Serializable]
public class DefaultableValue
{
[XmlAttribute("default")]
public string Default { get; set; }
[XmlText]
public string Value { get; set; }
}
And sample program to demonstrate usage...
public class Program
{
private static void Main(string[] args)
{
UserSettings userSettings = new UserSettings();
userSettings.User = new DefaultableValue()
{
Default = "Programmer",
Value = "Tyler"
};
userSettings.Level = new DefaultableValue()
{
Default = "2",
Value = "99"
};
XmlSerializer xmlSerializer = new XmlSerializer(typeof(UserSettings));
string serializedUserSettings;
using (StringWriter stringWriter = new StringWriter())
{
xmlSerializer.Serialize(stringWriter, userSettings);
serializedUserSettings = stringWriter.GetStringBuilder().ToString();
}
UserSettings deserializedUserSettings;
using (StringReader stringReader = new StringReader(serializedUserSettings))
{
deserializedUserSettings = (UserSettings)xmlSerializer.Deserialize(stringReader);
}
Console.Out.WriteLine("User: Default={0}, Actual={1}",
deserializedUserSettings.User.Default,
deserializedUserSettings.User.Value);
Console.Out.WriteLine("Level: Default={0}, Actual={1}",
deserializedUserSettings.Level.Default,
deserializedUserSettings.Level.Value);
}
}
(Note that I have the default values in code, but they very well could have come from the xml file)
I have a plugin-based host application. Its settings are described as a data contract:
[DataContract(IsReference = true)]
public class HostSetup
{
[DataMember]
public ObservableCollection<Object> PluginSetups
{
get
{
return pluginSetups ?? (pluginSetups = new ObservableCollection<Object>());
}
}
private ObservableCollection<Object> pluginSetups;
}
Any plugin has its own settings type. E. g.:
[DataContract(IsReference = true)]
public class Plugin1Setup
{
[DataMember]
public String Name { get; set; }
}
and
[DataContract(IsReference = true)]
public class Plugin2Setup
{
[DataMember]
public Int32 Percent { get; set; }
[DataMember]
public Decimal Amount { get; set; }
}
At run-time, the user has configured host and plugins such a way:
var obj = new HostSetup();
obj.PluginSetups.Add(new Plugin1Setup { Name = "Foo" });
obj.PluginSetups.Add(new Plugin2Setup { Percent = 3, Amount = 120.50M });
Then, my application has saved its settings via DataContractSerializer. Plugin types were passed as known types to the serializer's constructor.
The question.
User physically removes assembly with "Plugin2" and then starts up my application.
So, when the host receives a list of available plugins, it knows nothing about serialized "Plugin2Setup" instance.
I want to ignore this instance, and let the user to work without "Plugin2" settings.
Is there any elegant way to do this?
I can store plugins' settings as data contracts serialized into strings:
public ObservableCollection<String> PluginSetups
but it's not handy and ugly.
Edit 1
The problem is how to deserialize HostSetup instance and ignore serialized Plugin2Setup instance.
Edit 2
My current solution is:
[DataContract(IsReference = true)]
public class PluginSetupContainer
{
[DataMember]
private String typeName;
[DataMember]
private String rawData;
[OnSerializing]
private void OnSerializing(StreamingContext context)
{
if (SetupParameters != null)
{
using (var writer = new StringWriter())
using (var xmlWriter = new XmlTextWriter(writer))
{
var setupParametersType = SetupParameters.GetType();
var serializer = new DataContractSerializer(setupParametersType);
serializer.WriteObject(xmlWriter, SetupParameters);
xmlWriter.Flush();
typeName = setupParametersType.AssemblyQualifiedName;
rawData = writer.ToString();
}
}
}
[OnSerialized]
private void OnSerialized(StreamingContext context)
{
ClearInternalData();
}
[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
if (!String.IsNullOrEmpty(typeName) && !String.IsNullOrEmpty(rawData))
{
var setupParametersType = Type.GetType(typeName, false);
if (setupParametersType != null)
{
using (var reader = new StringReader(rawData))
using (var xmlReader = new XmlTextReader(reader))
{
var serializer = new DataContractSerializer(setupParametersType);
SetupParameters = serializer.ReadObject(xmlReader);
}
}
ClearInternalData();
}
}
private void ClearInternalData()
{
typeName = null;
rawData = null;
}
public Object SetupParameters { get; set; }
}
[DataContract(IsReference = true)]
public class HostSetup
{
[DataMember]
public ObservableCollection<PluginSetupContainer> PluginSetups
{
get
{
return pluginSetups ?? (pluginSetups = new ObservableCollection<PluginSetupContainer>());
}
}
private ObservableCollection<PluginSetupContainer> pluginSetups;
}
May be it's terrible, but it works. :)
I think ideally you should have something on the lines of
[DataContract(IsReference = true)]
[MyPluginCustomAttribute]
public class Plugin1Setup
{
}
and when you application loads you should initialize obj.PluginSetups using reflection based on MyPluginCustomAttribute so only assemblies that are present have their types registered. So you won't have the problem of missing assemblies. You can also use Managed Extensibility Framework (MEF) instead of your own MyPluginCustomAttribute
I defined two classes. First one...
[Serializable]
public class LocalizationEntry
{
public LocalizationEntry()
{
this.CatalogName = string.Empty;
this.Identifier = string.Empty;
this.Translation = new Dictionary<string, string>();
this.TranslationsList = new List<Translation>();
}
public string CatalogName
{
get;
set;
}
public string Identifier
{
get;
set;
}
[XmlIgnore]
public Dictionary<string, string> Translation
{
get;
set;
}
[XmlArray(ElementName = "Translations")]
public List<Translation> TranslationsList
{
get
{
var list = new List<Translation>();
foreach (var item in this.Translation)
{
list.Add(new Translation(item.Key, item.Value));
}
return list;
}
set
{
foreach (var item in value)
{
this.Translation.Add(item.Language, item.Text);
}
}
}
}
...where public List<Translation> TranslationsList is a wrapper for non-serializable public Dictionary<string, string> Translation.
Pair of key and value is defined as follows:
[Serializable]
public class Translation
{
[XmlAttribute(AttributeName = "lang")]
public string Language
{
get;
set;
}
[XmlText]
public string Text
{
get;
set;
}
public Translation()
{
}
public Translation(string language, string translation)
{
this.Language = language;
this.Text = translation;
}
}
At last code used to serialize:
static void Main(string[] args)
{
LocalizationEntry entry = new LocalizationEntry()
{
CatalogName = "Catalog",
Identifier = "Id",
};
entry.Translation.Add("PL", "jabłko");
entry.Translation.Add("EN", "apple");
entry.Translation.Add("DE", "apfel");
using (FileStream stream = File.Open(#"C:\entry.xml", FileMode.Create))
{
XmlSerializer serializer = new XmlSerializer(typeof(LocalizationEntry));
serializer.Serialize(stream, entry);
}
LocalizationEntry deserializedEntry;
using (FileStream stream = File.Open(#"C:\entry.xml", FileMode.Open))
{
XmlSerializer serializer = new XmlSerializer(typeof(LocalizationEntry));
deserializedEntry = (LocalizationEntry)serializer.Deserialize(stream);
}
}
The problem is that after deserialization deserializedEntry.TranslationsList is empty. I set a breakpoint at setter of LocalizationEntry.TransalionsList and it comes from deserializer empty as well. Product of serialization is of course valid. Is there any gap in my code?
EDIT:
Here is generated XML:
<?xml version="1.0"?>
<LocalizationEntry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<CatalogName>Catalog</CatalogName>
<Identifier>Id</Identifier>
<Translations>
<Translation lang="PL">jabłko</Translation>
<Translation lang="EN">apple</Translation>
<Translation lang="DE">apfel</Translation>
</Translations>
</LocalizationEntry>
The problem is that your TranslationList property is not being set by the Xml Deserializer. The set method will be hit but only by the call to this.TranslationsList = new List(); in the LocalisationEntry constructor. I'm not yet sure why but I suspect it's because it doesn't know how to convert an array of Translation objects back into a List.
I added the following code and it worked fine:
[XmlArray(ElementName = "Translations")]
public Translation[] TranslationArray
{
get
{
return TranslationsList.ToArray();
}
set
{
TranslationsList = new List<Translation>(value);
}
}
[XmlIgnore]
public List<Translation> TranslationsList
....
I am guessing the problem has to do with this:
public List<Translation> TranslationsList
The get/set operators are designed only for something to get or assign a fully-formed list. If you tried to use this in your own code, for example, every time you would do something like
TranslationsList.Add(item)
It would just create a new list from the existing dictionary and not actually deal with your item. I bet the deserializer works much the same way: uses set to create the new object once, then uses get as it adds each item from the XML. Since all that happens in get is it copies from the dictionary (which is empty when you begin your deserialization) you end up with nothing.
Try replacing this with just a field:
public List<Translation> TranslationsList;
and then explicitly call the code to copy the dictionary to this list before you serialize, and copy it from this list to the dictionary after you deserialize. Assuming that works, you can probably figure out a more seamless way to implement what you're trying to do.
I've created a sample, which will allow you to avoid the unnecessary hidden property when using the XmlSerializer:
class Program
{
static void Main(string[] args)
{
LocalizationEntry entry = new LocalizationEntry()
{
CatalogName = "Catalog",
Identifier = "Id",
Translations =
{
{ "PL", "jabłko" },
{ "EN", "apple" },
{ "DE", "apfel" }
}
};
using (MemoryStream stream = new MemoryStream())
{
XmlSerializer serializer = new XmlSerializer(typeof(LocalizationEntry));
serializer.Serialize(stream, entry);
stream.Seek(0, SeekOrigin.Begin);
LocalizationEntry deserializedEntry = (LocalizationEntry)serializer.Deserialize(stream);
serializer.Serialize(Console.Out, deserializedEntry);
}
}
}
public class LocalizationEntry
{
public LocalizationEntry() { this.Translations = new TranslationCollection(); }
public string CatalogName { get; set; }
public string Identifier { get; set; }
[XmlArrayItem]
public TranslationCollection Translations { get; private set; }
}
public class TranslationCollection
: Collection<Translation>
{
public TranslationCollection(params Translation[] items)
{
if (null != items)
{
foreach (Translation item in items)
{
this.Add(item);
}
}
}
public void Add(string language, string text)
{
this.Add(new Translation
{
Language = language,
Text = text
});
}
}
public class Translation
{
[XmlAttribute(AttributeName = "lang")]
public string Language { get; set; }
[XmlText]
public string Text { get; set; }
}
There are some drawbacks when working with the XmlSerializer class itself. The .NET guidelines encourage you the not provide public-setters for collection-properties (like your translation list). But when you look at the code generated by the XmlSerializer, you'll see that it will use the Setter regardless of it is accessible. This results in a compile-error when the interim class is dynamically loaded by the XmlSerializer. The only way to avoid this, is to make the XmlSerializer think, that it can't actually create an instance of the list and thus won't try to call set for it. If the XmlSerializer detects that it can't create an instance it will throw an exception instead of using the Setter and the interim class is compiled successfully. I've used the param-keyword to trick the serializer into thinking that there is no default-constructor.
The only drawback from this solution is that you have to use a non-generic, non-interface type for the property (TranslationCollection) in my example.