XamlReader.Parse throws exception on empty String - c#

In our app, we need to save properties of objects to the same database table regardless of the type of object, in the form of propertyName, propertyValue, propertyType. We decided to use XamlWriter to save all of the given object's properties. We then use XamlReader to load up the XAML that was created, and turn it back into the value for the property. This works fine for the most part, except for empty strings. The XamlWriter will save an empty string as below.
<String xmlns="clr-namespace:System;assembly=mscorlib" xml:space="preserve" />
The XamlReader sees this string and tries to create a string, but can't find an empty constructor in the String object to use, so it throws a ParserException.
The only workaround that I can think of is to not actually save the property if it is an empty string. Then, as I load up the properties, I can check for which ones did not exist, which means they would have been empty strings.
Is there some workaround for this, or is there even a better way of doing this?

We had a similar problem to this when trying to serialize strings. The only way we could resolve it was to create a StringWrapper struct or class that had the appropriate constructors. We then used this type to load and save our string values.

I also got the problem and searched the web for a solution but could not find one.
I solved it by inspecting the saved XML and fixing the empty strings, like this (feed FixSavedXaml with the output from XamlWriter):
static string FixSavedXaml(string xaml)
{
bool isFixed = false;
var xmlDocument = new System.Xml.XmlDocument();
xmlDocument.LoadXml(xaml);
FixSavedXmlElement(xmlDocument.DocumentElement, ref isFixed);
if (isFixed) // Only bothering with generating new xml if something was fixed
{
StringBuilder xmlStringBuilder = new StringBuilder();
var settings = new System.Xml.XmlWriterSettings();
settings.Indent = false;
settings.OmitXmlDeclaration = true;
using (var xmlWriter = System.Xml.XmlWriter.Create(xmlStringBuilder, settings))
{
xmlDocument.Save(xmlWriter);
}
return xmlStringBuilder.ToString();
}
return xaml;
}
static void FixSavedXmlElement(System.Xml.XmlElement xmlElement, ref bool isFixed)
{
// Empty strings are written as self-closed element by XamlWriter,
// and the XamlReader can not handle this because it can not find an empty constructor and throws an exception.
// To fix this we change it to use start and end tag instead (by setting IsEmpty to false on the XmlElement).
if (xmlElement.LocalName == "String" &&
xmlElement.NamespaceURI == "clr-namespace:System;assembly=mscorlib")
{
xmlElement.IsEmpty = false;
isFixed = true;
}
foreach (var childElement in xmlElement.ChildNodes.OfType<System.Xml.XmlElement>())
{
FixSavedXmlElement(childElement, ref isFixed);
}
}

Related

How can I determine the class of an XML serialized object in C# before I deserialize it?

I have a server that accepts requests as XML serialized objects which could be any of 10 or so different Classes. Of course in order for the server to process the request, it must first de-serialize the XML string back into an object. To do that, it needs to know what class the object came from to choose the correct de-serializer and re-construct the object. So it would be good to be able to just quickly inspect the XML string before attempting to de-serialize it to get the object type and then select the appropriate de-serializer.
I have been using the following code, but, like the song goes, "I know there's got to be a better way..." Any suggestions or insight would be appreciated.
private void button1_Click(object sender, EventArgs e)
{
//any class - does not matter - create an object
SomeClass tc = new SomeClass();
//populate it
tc.i = 5;
tc.s = "Hello World";
tc.d = 123.456;
//Serialize it to XML
StringWriter xml = new StringWriter();
System.Xml.Serialization.XmlSerializer x = new System.Xml.Serialization.XmlSerializer(typeof(SomeClass));
x.Serialize(xml, tc);
//Extract the Class name and show the XML to the user without de-serializing it
textBox1.Text = GetClassNameFromXMLSerializedString(xml.ToString());
}
private string GetClassNameFromXMLSerializedString(string xml)
{
//The class name is somewhere in the xml
string classname = xml;
//get the start of class name
classname = xml.Substring(classname.IndexOf('>') + 4);
//get the class name which is terminated by a space
classname = classname.Substring(0, classname.IndexOf(' '));
//return it
return classname;
}
The XML Deserializer does not need to know what type it is before deserializing. The MSDN Article about the Deserialize method has some useful information about it and of course it has a code snippet, which I've put below.
I think you might have confused yourself with the fact that the server will deserialize it to an object, but then won't know what to do with it. You can always do a switch case for the result of the ReturnedObject.GetType() method and work out what you need to do with it.
You can just serialize it to an object like this:
var ReturnedObject = XMLSerializer.Deserialize(reader);
Then you can go ahead and do
switch (ReturnedObject.getType())
{
case MyClass:
// Insert code here
case AnotehrClass:
//Do something else here for another class
}
If you really want to you can read the 3rd element like this:
using (XmlReader xr = XmlReader.Create(GenerateStreamFromString(xml.ToString())))
{
xr.Read();
xr.Read();
xr.Read();
textBox1.Text = xr.Name;
}
Using this helper function:
public static MemoryStream GenerateStreamFromString(string value)
{
return new MemoryStream(Encoding.Unicode.GetBytes(value ?? ""));
}
All checks omitted..
If you want to you can test if the 1st element is xml and the 2nd one is empty.
I'm not really sure if this is a good idea.
XDocument xd = XDocument.Parse(xml.ToString()); switch (xd.Root.Name.ToString()) { case "Class1": //do something break; case "Class2": //do something break; }

Getting user settings collection types' default values in Visual Studio .NET

I want the ability to reset a single user setting easily to its default value.
I've written an extension like so:
public static void sReset(this Properties.Settings set, string key = "")
{
if (key == "")
{
set.Reset();
return;
}
Console.WriteLine("Reset '" + key + "' to: " + set.Properties[key].DefaultValue);
set.PropertyValues[key].PropertyValue = set.PropertyValues[key].Property.DefaultValue;
}
And this works fine for primitive types.
But now I want to apply that to a stringCollection, and it fails:
An unhandled exception of type 'System.InvalidCastException' occurred
in myApp.exe
Additional information: Unable to cast object of type 'System.String'
to type 'System.Collections.Specialized.StringCollection'.
This is because these collection type default values (Stored serialized as XML) are returned as strings:
set.Properties[key].DefaultValue.GetType() returns System.String
I can see in the settings designer that usually, it just casts the value to StringCollection:
public global::System.Collections.Specialized.StringCollection settingsName {
get {
return ((global::System.Collections.Specialized.StringCollection)(this["settingsName"]));
}
set {
this["settingsName"] = value;
}
}
But that fails after assigning the DefaultValue, with the above error message.
I tried casting the XML String before assigning, but of course that failed there, too.
How is an XML String like this converted before being assigned to the settings property?
What do I need to do to make this work?
Nobody?
That sucks, I would've thought there was a more elegant solution...
So I'm using this now:
Check if the Type of the setting is StringCollection
Read out XML Elements at "ArrayOfString/string"
... <-- wouldn't let me format code as code without an additional paragraph here. What the hell?
public static void sReset(this Properties.Settings set, string key = "")
{
if (key == "")
{
set.Reset();
return;
}
if (set.PropertyValues[key].Property.PropertyType == typeof(StringCollection))
{
string tvs = (string)set.PropertyValues[key].Property.DefaultValue;
set.PropertyValues[key].PropertyValue = tvs.XmlToStringCollection();
return;
}
set.PropertyValues[key].PropertyValue = set.PropertyValues[key].Property.DefaultValue;
}
public static StringCollection XmlToStringCollection(this string str)
{
StringCollection ret = new StringCollection();
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(str);
XmlNodeList terms = xmlDoc.SelectNodes("ArrayOfString/string");
foreach (XmlElement s in terms)
{
if (s.InnerXml != "")
{
Console.WriteLine("Found: " + s.InnerXml);
ret.Add(s.InnerXml);
}
}
return ret;
}
since the XML looks like this:
<?xml version="1.0" encoding="utf-16"?>
<ArrayOfString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<string>first string</string>
<string>second string</string>
<string>...</string>
</ArrayOfString>
...Which is a shame, it's damn ugly and limited:
You'd have to do the same for any kind of XML serialized type
Really, what I'd like is a way to get the default value the same way VS does.
I can't imagine it could be as hacky as this, could it?
I realize this is an old question, but if anyone else lands here like I did, the OP's solution works. However, to answer OP's final question, I suspect Microsoft is using the XmlSerializer class, something like this extension method I came up with, though I also couldn't find a simple method call to do it for me:
public static StringCollection ToStringCollection(this string str)
{
// Create an instance of the XmlSerializer.
XmlSerializer serializer =
new XmlSerializer(typeof(StringCollection));
// Declare an object variable of the type to be deserialized.
StringCollection sc;
using (Stream reader = new MemoryStream(Encoding.Unicode.GetBytes(str)))
{
// Call the Deserialize method to restore the object's state.
sc = (StringCollection)serializer.Deserialize(reader);
}
return sc;
}
The above is based upon example code from https://learn.microsoft.com/en-us/dotnet/api/system.xml.serialization.xmlserializer.deserialize?view=net-6.0

Remove metadata from Excel file using C#?

I'm currently using C# to set the custom attributes of multiple excel files. I'm using an imported library from Microsoft known as DSOFile to write to the CustomProperties property. One issue that I'm running into is whenever the code attempts to write to an excel file that already has custom properties written to it, such as the Company and Year, a COMException exception is thrown to indicate the custom properties of the file already has a field with that name. Exact Message: "An item by that name already exists in the collection". I would like to be able to delete that item in the collection so that I can rewrite to the file. For example, if I accidentally added the wrong year to the year attribute in the file, I would like the ability to clear that field and write a new value to it. I was unable to find a method in the DSOFile class that removes metadata. Is there anyway to "programmatically" clear metadata from a file without doing it through the file properties window?
Sample Code:
DSOFILE.OleDocumentProperties dso = new DSOFile.OleDocumentProperties();
dso.Open(#"c\temp\test.xls", false, DSOFile.dsoFileOpenOptions.dsoOptionDefault);
//add metadata
dso.CustomProperties.Add("Company", "Sony");
dso.Save();
dso.Close(false);
If you want to change the default properties used by Office like Company or Author, you can just update them via the SummaryProperties object:
OleDocumentProperties dso = new DSOFile.OleDocumentProperties();
dso.Open(#"c:\temp\test.xls", false, DSOFile.dsoFileOpenOptions.dsoOptionDefault);
//Update Company
dso.SummaryProperties.Company = "Hello World!";
dso.Save();
dso.Close(false);
Note, that you cannot change the default properties of documents that you can access via the SummaryProperties object via the CustomProperties object in dso. The CustomProperties are meant for additional properties used by the user, not the ones already introduced by Microsoft Office.
In order to change the custom properties, you have to be aware that CustomProperties is a collection that you can iterate over via foreach. So you can use the following two methods:
private static void DeleteProperty(CustomProperties properties, string propertyName)
{
foreach(CustomProperty property in properties)
{
if (string.Equals(property.Name, propertyName, StringComparison.InvariantCultureIgnoreCase))
{
property.Remove();
break;
}
}
}
private static void UpdateProperty(CustomProperties properties, string propertyName, string newValue)
{
bool propertyFound = false;
foreach (CustomProperty property in properties)
{
if (string.Equals(property.Name, propertyName, StringComparison.InvariantCultureIgnoreCase))
{
// Property found, change it
property.set_Value(newValue);
propertyFound = true;
break;
}
}
if(!propertyFound)
{
// The property with the given name was not found, so we have to add it
properties.Add(propertyName, newValue);
}
}
Here is an example on how to use UpdateProperty:
static void Main(string[] args)
{
OleDocumentProperties dso = new DSOFile.OleDocumentProperties();
dso.Open(#"c:\temp\test.xls", false, DSOFile.dsoFileOpenOptions.dsoOptionDefault);
UpdateProperty(dso.CustomProperties, "Year", "2017");
dso.Save();
dso.Close(false);
}

SettingsPropertyValue.SerializedValue is null, unless I view the variable through locals

I'm storing some common settings used in a C# program inside the ApplicationSettings. Most of the settings are strings, but one of them is a StringCollection.
A null exception is occurring in some code that loops through the default settings at Properties.Settings.Default and adds them to a dictionary (in preparation to send as parameters), as shown below.
// Generate parameters
Dictionary<string, string> signalparams = new Dictionary<string, string>();
// Add parameters
foreach (SettingsProperty property in Properties.Settings.Default.Properties)
{
SettingsPropertyValue value = new SettingsPropertyValue(property);
if (value.Property.SerializeAs == SettingsSerializeAs.Xml)
{
// Here's where the error occurs
signalparams.Add(value.Name, value.SerializedValue.ToString());
}
else if (value.Property.SerializeAs == SettingsSerializeAs.String)
{
signalparams.Add(value.Name, value.PropertyValue.ToString());
}
}
The strings settings are added fine, but when it reaches the StringCollection, it (correctly) evaluates that the property has SerializeAs == SettingsSerializeAs.Xml. However, the SerializedValue is null, and thus ToString() throws an exception.
The strange thing is that when running the debugger, the SerializedValue is null until I try viewing the value variable's properties in the locals windows. At that point, SerializedValue contains the correct XML serialized format for the StringCollection, and the program continues fine.
Why is this happening?
The issue stems probably from the fact that SerializedValue is a property that is implemented something like that:
public object SerializedValue
{
get
{
if (this._ChangedSinceLastSerialized)
return CalculateSerializedData();
else
return _cachedData;
}
}
What happens in your case, in my opinion, is that you're getting _cachedData when you first access the SettingPropertyValue, then, by viewing value via the debugger, you cause _ChangedSinceLastSerialized to be true which causes the next call to SerializedValue property to return the actual serialized value.
The next question is to find out why _ChangedSinceLastSerialized in your case is set to false. The logic of SettingsPropertyValue says (you can see it in Reflector in PropertyValue property of the class) that _ChangedSinceLastSerialized is set to true when the user either accesses the settings (in case of most object types), so for example merely accessing your setting like that: MyAppSettings.Default.MySettingObject would change _ChangedSinceLastSerialized to true.
What could happen in your case that you have code similar to this:
object storingObject = MyAppSettings.Default.MySettingObject;
// Now I will use storingObject to access the setting.
Once you do something like that and keep using storingObject instead of directly accessing MyAppSettings you can create a situation in which the object changed while _ChangedSinceLastSerialized remains false.
I hope that was helpful.
I actually dropped trying to use SettingsPropertyValue and decided to serialize the object to XML manually. I simply used the SettingsProperty.Name as a key to retrieve the values of the settings stored in Properties.Settings.Default, since it is a dictionary-like object. It fixed the issue, but of course the root cause still remains unknown. The revised code looks something like this:
// Generate parameters
Dictionary<string, string> signalparams = new Dictionary<string, string>();
// Add parameters
foreach (SettingsProperty property in Properties.Settings.Default.Properties)
{
if (property.SerializeAs == SettingsSerializeAs.String)
{
signalparams.Add(property.Name, Properties.Settings.Default[property.Name].ToString());
}
else if (property.SerializeAs == SettingsSerializeAs.Xml)
{
// Serialize collection into XML manually
XmlSerializer serializer = new XmlSerializer(Properties.Settings.Default[property.Name].GetType());
StringBuilder sb = new StringBuilder();
XmlWriter writer = XmlWriter.Create(sb);
serializer.Serialize(writer, Properties.Settings.Default[property.Name]);
writer.Close();
signalparams.Add(property.Name, sb.ToString());
}
}

C# Cut/Copy & Paste objects

This is a 2 parter.
First I am having a heck of a time getting the paste part of a copy and paste operation to work.
I have a method that copies information to the Clipboard, works perfectly.
private void CopyData(string format, object data, string text)
{
bool addedData = false;
DataObject copyData = new DataObject();
if (!string.IsNullOrEmpty(text))
{
copyData.SetData(DataFormats.Text, text);
addedData = true;
}
if (!string.IsNullOrEmpty(format) && data != null)
{
copyData.SetData(format, false, data);
addedData = true;
//this is only for testing
object obj = null;
if (copyData.GetDataPresent(format))
obj = (object)copyData.GetData(format);
}
if (addedData)
Clipboard.SetDataObject(copyData, true);
}
When I check that the data was added the object (obj) is not null.
However when I then go to paste the data from a different method using the same format key I get null, every time.
private void PasteFromClipboard()
{
object obj = null;
IDataObject paste = null;
if (Clipboard.GetDataObject().GetDataPresent("mydatatype"))
obj = (object)Clipboard.GetDataObject().GetData("mydatatype");
else
return;
if (obj == null)
throw new NullReferenceException("Could not gather information from the
}
I have tried everything that I can think of and it just doesn't make sense. I created an array of strings to capture all of the format keys the DataObject was holding and "mydatatype" was the first one. I have tried casting, not casting, using (Clipboard.GetDataObject().GetData("mydatatype") as object) and I just cannot figure it out. I know that there is data there because I can go to NotePad and paste the text that I copied along with the object.
Any thoughts as to why I would be able to get the data in one method, but not another?
Secondly I was wondering how I would go about making a Cut & Paste operation work between two of my windows. I am thinking about something like Excel, where if only the text is pasted the data will remain, however if the objects are pasted then the source will be deleted.
Thanks
Patrick.
Try pulling the data out as Text (instead of "mydatatype") - at least to confirm you can read from the clipboard. This is most likely what Notepad is reading. Also, does it matter that you're copying with "format" but pasting with "mydatatype"?
Could it be that text parameter always has a value and gets set. Then possibly the second if the one that would set the object does not get executed. Or if it does since the data was set in the first if statement the second set fails to set it correctly.
My recommendation would be to walk the code in the debugger during the copy operation.
Before the paste use GetDataObject().GetFormats() to enumerate the list of formatting codes.
Perhaps your using the wrong one.. just an idea
Try using reflection like this:
private static T TryGetClipboardData<T>(IDataObject clipboardData, string dataFormat)
{
System.Reflection.FieldInfo fieldInfo = clipboardData.GetType().GetField("innerData", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var outerData = fieldInfo.GetValue(clipboardData);
if (outerData == null)
{
return default(T);
}
fieldInfo = outerData.GetType().GetField("innerData", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var innerData = fieldInfo.GetValue(outerData);
if (innerData is System.Runtime.InteropServices.ComTypes.IDataObject)
{
// It is (probably) necessary to wrap COM IDataObject to Windows.Forms.IDataObject
System.Windows.Forms.DataObject wrappedDataObject = new System.Windows.Forms.DataObject(innerData);
var data = wrappedDataObject.GetData(dataFormat);
if (data is T)
{
return (T)data;
}
}
return default(T);
}
I suspect the COM object of your data in the Clipboard had a difficult time converting itself to the format you specified. I'm also playing safe with the input format string so that it is registered as a proper clipboard format.
HTH

Categories