2

I'm trying to use XmlAttributeOverrides to change the way my class is being serialized to XML. I need to exclude some properties and include others in a specific order.

I have this code here:

// XML Attribute Overrrides
public static XmlAttributeOverrides GetXMLAttributeOverrides(Type theType, List<string> propertiesToInlcudeInOrder, List<string> allColumnNames)
{
    try
    {
        if (propertiesToInlcudeInOrder != null)
        {
            XmlAttributeOverrides theXMLAttributeOverrides = new XmlAttributeOverrides();
            if (propertiesToInlcudeInOrder.Count > 0)
            {
                XmlAttributes mainNewXMLAttributes = new XmlAttributes();
                mainNewXMLAttributes.XmlIgnore = false;

                XmlAttributes ignoreXMLAttributes = new XmlAttributes();
                ignoreXMLAttributes.XmlIgnore = true;

                List<string> propertiesToNotInclude = new List<string>();
                foreach (string theColumnName in allColumnNames)
                {
                    string thePropertyName = theColumnName;
                    bool addProperty = true;
                    foreach (string propertyToInclude in propertiesToInlcudeInOrder)
                    {
                        if (thePropertyName == propertyToInclude)
                        {
                            addProperty = false;
                            break;
                        }
                    }

                    if (addProperty)
                    {
                        propertiesToNotInclude.Add(thePropertyName);
                    }
                }

                // To Ignore
                foreach (string propertyNameToNotInlcude in propertiesToNotInclude)
                {
                    XmlElementAttribute theXMLElementAttributeToAdd = new XmlElementAttribute(propertyNameToNotInlcude);
                    theXMLElementAttributeToAdd.ElementName = propertyNameToNotInlcude;
                    ignoreXMLAttributes.XmlElements.Add(theXMLElementAttributeToAdd);

                    theXMLAttributeOverrides.Add(theType, propertyNameToNotInlcude, ignoreXMLAttributes);
                }

                // To Add In Order
                int counter = 1;
                foreach (string propertyNameToIncludeInOrder in propertiesToInlcudeInOrder)
                {
                    XmlElementAttribute theXMLElementAttributeToAdd = new XmlElementAttribute(propertyNameToIncludeInOrder);
                    theXMLElementAttributeToAdd.ElementName = propertyNameToIncludeInOrder;
                    theXMLElementAttributeToAdd.Order = counter;
                    mainNewXMLAttributes.XmlElements.Add(theXMLElementAttributeToAdd);

                    theXMLAttributeOverrides.Add(theType, propertyNameToIncludeInOrder, mainNewXMLAttributes);

                    counter++;
                }
            }

            return theXMLAttributeOverrides;
        }
        else
        {
            return null;
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show("Error at 'GetXMLAttributeOverrides'" + Environment.NewLine + Environment.NewLine + ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
        return null;
    }
}

In my test I have a class with 13 properties and I want to include 3 in a specific order and exclude all the others.

I have made sure that I don't have any duplicates in my lists. I have double checked that I don't have the same property name in the 'Ignore List' and the 'Include List'. I get an exception on this line of my code: XmlSerializer(dataToSerialize.GetType(), allXMLAttribueOverrides);

allXMLAttribueOverrides Is returned from my method GetXMLAttributeOverrides

The exception is:

There was an error reflecting type 'System.Collections.Generic.List`1[SystemName.UserControls.TestMain]'.

Inner exception is:

There was an error reflecting property 'TextColumn'.

You need to add XmlChoiceIdentifierAttribute to the 'TextColumn' member.

'TextColumn' is the first property in my test class

Here's my test code:

TestMain testItem = new TestMain(null, "TextColumnTEST", 5, Convert.ToDecimal(0.333), Convert.ToDecimal(0.777), DateTime.Now, "12:00:00", DateTime.Now, true, "Password", "#FFFFFF", null, null, null);
List<TestMain> dataToSerialize = new List<TestMain>();
dataToSerialize.Add(testItem);

List<string> propertiesToInlcudeInOrder = new List<string>();
propertiesToInlcudeInOrder.Add("CurrencyColumn");
propertiesToInlcudeInOrder.Add("NumberColumn");
propertiesToInlcudeInOrder.Add("TextColumn");

List<string> allColumnNames = new List<string>();
allColumnNames.Add("ID");
allColumnNames.Add("Select");
allColumnNames.Add("TextColumn");
allColumnNames.Add("NumberColumn");
allColumnNames.Add("CurrencyColumn");
allColumnNames.Add("DecimalColumn");
allColumnNames.Add("DateColumn");
allColumnNames.Add("TimeColumn");
allColumnNames.Add("DateAndTimeColumn");
allColumnNames.Add("YesNoColumn");
allColumnNames.Add("PasswordColumn");
allColumnNames.Add("ColorColumn");
allColumnNames.Add("ImageColumn");
allColumnNames.Add("DocumentColumn");
allColumnNames.Add("OtherColumn");

XmlAttributeOverrides allXMLAttribueOverrides = ReportingManipulation.GetXMLAttributeOverrides(dataToSerialize[0].GetType(), propertiesToInlcudeInOrder, allColumnNames);

using (StringWriter mainStringWriter = new StringWriter())
{
    XmlSerializer mainXMLSerializer = new XmlSerializer(dataToSerialize.GetType(), allXMLAttribueOverrides);
    mainXMLSerializer.Serialize(mainStringWriter, dataToSerialize);
    return mainStringWriter.ToString();
}

and here's my test class:

public class TestMain
{
    #region Properties

    // Properties

    [XmlIgnore]
    public int? ID { get; set; }
    [XmlIgnore]
    public bool Select { get; set; }

    public string TextColumn { get; set; }
    public int NumberColumn { get; set; }
    public decimal CurrencyColumn { get; set; }
    public decimal DecimalColumn { get; set; }
    public DateTime DateColumn { get; set; }
    public string TimeColumn { get; set; }
    public DateTime DateAndTimeColumn { get; set; }
    public bool YesNoColumn { get; set; }
    public string PasswordColumn { get; set; }
    public string ColorColumn { get; set; }
    public byte[] ImageColumn { get; set; }
    public byte[] DocumentColumn { get; set; }
    public byte[] OtherColumn { get; set; }

    #endregion

    #region Constructors

    // Constructors
    public TestMain()
    {
        try
        {

        }
        catch (Exception ex)
        {
            MessageBox.Show("Error at Constructor: 'TestMain'" + Environment.NewLine + Environment.NewLine + ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
        }
    }
    public TestMain(int? theID, string theTextColumn, int theNumberColumn, decimal theCurrencyColumn, decimal theDecimalColumn, DateTime theDateColumn, string theTimeColumn, DateTime theDateAndTimeColumn, bool theYesNoColumn, string thePasswordColumn, string theColorColumn, byte[] theImageColumn, byte[] theDocumentColumn, byte[] theOtherColumn)
    {
        try
        {
            this.ID = theID;

            this.TextColumn = theTextColumn;
            this.NumberColumn = theNumberColumn;
            this.CurrencyColumn = theCurrencyColumn;
            this.DecimalColumn = theDecimalColumn;
            this.DateColumn = theDateColumn;
            this.TimeColumn = theTimeColumn;
            this.DateAndTimeColumn = theDateAndTimeColumn;
            this.YesNoColumn = theYesNoColumn;
            this.PasswordColumn = thePasswordColumn;
            this.ColorColumn = theColorColumn;
            this.ImageColumn = theImageColumn;
            this.DocumentColumn = theDocumentColumn;
            this.OtherColumn = theOtherColumn;
        }
        catch (Exception ex)
        {
            MessageBox.Show("Error at Constructor: 'TestMain'" + Environment.NewLine + Environment.NewLine + ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
        }
    }

    #endregion
}

Where am I going wrong?

Any help / advice would be appreciated.

Shiasu-sama
  • 1,179
  • 2
  • 12
  • 39
  • @dbc :) without the overrides it works fine. I'll edit my question to contain example code. – Shiasu-sama Feb 01 '19 at 09:58
  • I've noticed that if I ignore the `// To Add In Order` loop of my method; then there are no problems. But that way I miss out on the order of the columns that I want serialized – Shiasu-sama Feb 01 '19 at 12:31

1 Answers1

2

Your basic problem is that you are adding multiple override [XmlElement] attributes for each of your properties, because you are using a single instance mainNewXMLAttributes for all of them, which accumulates the XmlElementAttribute objects defined for all of them.

To fix this, you need to allocate a fresh mainNewXMLAttributes for each property inside the foreach (var propertyNameToIncludeInOrder in propertiesToInlcudeInOrder) loop, as shown in the following corrected and simplified version of GetXMLAttributeOverrides():

public static partial class ReportingManipulation
{
    public static XmlAttributeOverrides GetXMLAttributeOverrides(Type theType, IList<string> propertiesToInlcudeInOrder)
    {
        var allProperties = theType.GetProperties(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance).Select(p => p.Name);
        return GetXMLAttributeOverrides(theType, propertiesToInlcudeInOrder, allProperties);
    }

    // XML Attribute Overrrides
    public static XmlAttributeOverrides GetXMLAttributeOverrides(Type theType, IList<string> propertiesToInlcudeInOrder, IEnumerable<string> allProperties)
    {
        if (propertiesToInlcudeInOrder == null || propertiesToInlcudeInOrder.Count == 0)
            return null;

        var theXMLAttributeOverrides = new XmlAttributeOverrides();

        // To Add In Order
        int counter = 1;
        foreach (var propertyNameToIncludeInOrder in propertiesToInlcudeInOrder)
        {
            // Allocate a fresh instance of XmlAttributes for each property, because we are defining a different
            // XmlElementAttribute for each
            var mainNewXMLAttributes = new XmlAttributes { XmlIgnore = false };

            // Specify the element order XmlElementAttribute and attach to the XmlAttributes
            var theXMLElementAttributeToAdd = new XmlElementAttribute { Order = counter };
            mainNewXMLAttributes.XmlElements.Add(theXMLElementAttributeToAdd);

            // Attach the override XmlElementAttribute to the property propertyNameToIncludeInOrder
            theXMLAttributeOverrides.Add(theType, propertyNameToIncludeInOrder, mainNewXMLAttributes);

            counter++;
        }

        // To Ignore
        // Using System.Linq.Enumerable.Except()
        var propertiesToNotInclude = allProperties.Except(propertiesToInlcudeInOrder);
        var ignoreXMLAttributes = new XmlAttributes { XmlIgnore = true };
        foreach (var propertyNameToNotInlcude in propertiesToNotInclude)
        {
            // Attach the override XmlElementAttribute to the property propertyNameToIncludeInOrder
            // No need to allocate a fresh instance of ignoreXMLAttributes for each, because the instances would all be identical
            theXMLAttributeOverrides.Add(theType, propertyNameToNotInlcude, ignoreXMLAttributes);
        }

        return theXMLAttributeOverrides;
    }
}

Why does your code not work? In your initial code, you do:

XmlAttributes mainNewXMLAttributes = new XmlAttributes();
mainNewXMLAttributes.XmlIgnore = false;

int counter = 1;
foreach (string propertyNameToIncludeInOrder in propertiesToInlcudeInOrder)
{
    XmlElementAttribute theXMLElementAttributeToAdd = new XmlElementAttribute(propertyNameToIncludeInOrder);
    theXMLElementAttributeToAdd.ElementName = propertyNameToIncludeInOrder;
    theXMLElementAttributeToAdd.Order = counter;
    mainNewXMLAttributes.XmlElements.Add(theXMLElementAttributeToAdd);

    theXMLAttributeOverrides.Add(theType, propertyNameToIncludeInOrder, mainNewXMLAttributes);

    counter++;
}

Now, the method XmlAttributeOverrides.Add(Type, String, XmlAttributes) is documented to work as follows:

Adds an XmlAttributes object to the collection of XmlAttributes objects. The type parameter specifies an object to be overridden. The member parameter specifies the name of a member that is overridden.

Thus the contents of mainNewXMLAttributes will get applied to the named parameter when the XmlSerializer is eventually constructed. And as you construct only instance of mainNewXMLAttributes for all parameters, its XmlElements array will contain element names corresponding to all of the parameters! I.e. you code attempts to apply multiple [XmlElement] attributes to each named parameter, differing only in override name and order. This accounts for the You need to add XmlChoiceIdentifierAttribute to the 'TextColumn' member. exception -- you can only attach multiple element names to a property if the property value is polymorphic and you want to assign different element names to different value types.

Notes

  • When generating an XmlSerializer with overrides, you must cache it statically and reuse it later to avoid a severe memory leak, as explained in Memory Leak using StreamReader and XmlSerializer.

  • I don't recommend unconditionally swallowing exceptions and presenting them as error messages to the user in low-level utility methods or object constructors.

Demo working fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340