How do you find out when you've been loaded via XML Serialization?

感情迁移 提交于 2019-12-17 03:42:12

问题


I'm trying to load a tree of objects via XML serialisation, and at the moment it will load the objects in, and create the tree quite happily. My issue revolves around the fact that these classes support a level of auditing. What I'd like to be able to do is call some method on each object after it has finished being loaded.

For the sake of argument, assume I have a fairly generic object tree with differing classes at different levels, like:

 <Customer name="Foo Bar Inc.">
   <Office IsHq="True">
     <Street>123 Any Street</Street>
     <Town name="Anytown">
       <State name="Anystate">
         <Country name="My Country" />
       </State>
     </Town>
   </Office>
   <Office IsHq="False">
     <Street>456 High Street</Street>
     <Town name="Anycity">
       <State name="Anystate">
         <Country name="My Country" />
       </State>
     </Town>
   </Office>
 </Customer>

Is there any way using the default serialisers (In the similar way that you can create methods like ShouldSerializeFoo) to determine when loading has finished for each object?

Edit: I should point out that the obvious case of exposing something akin to an OnLoaded() method that I could call after deserialising, strikes me as being a "bad thing to do".

Edit2: For the sake of discussion this is my current hack "approach", which works for the basic level, but the child City node still thinks it needs to be saved with changes (in the real world the object model is a lot more complex, but this will at least compile, without the need for full source)

public class Office
{
    [XmlAttribute("IsHq")]
    public bool IsHeadquarters { get; set; }

    [XmlElement]
    public string Street { get; set; }

    [XmlElement]
    public Town Town { get; set; }

    protected virtual void OnLoaded() {}

    public static OfficeCollection Search()
    {
        OfficeCollection retval = new OfficeCollection();
        string xmlString = @"
                    <Office IsHq='True'>
                        <Street>123 Any Street</Street>
                        <Town name='Anytown'>
                            <State name='Anystate'>
                                <Country name='My Country' />
                            </State>
                        </Town>
                    </Office>";

        XmlSerializer xs = new XmlSerializer(retval.GetType());
        XmlReader xr = new XmlTextReader(xmlString);
        retval = (OfficeCollection)xs.Deserialize(xr);

        foreach (Office thisOffice in retval)
        {
            thisOffice.OnLoaded();
        }
        return retval;
    }
}

回答1:


Hmmm... it's still not pretty but you could refactor your deserialization logic into a dedicated class which could notify the deserialized object that it originated from XML before returning it to the caller.

Update: I think this should be fairly easy to do without straying too far from the patterns laid by the framework... you'd just need to ensure that you use the CustomXmlSerializer. Classes that need this notification just need to implement IXmlDeserializationCallback

using System.Xml.Serialization;

namespace Custom.Xml.Serialization
{
    public interface IXmlDeserializationCallback
    {
        void OnXmlDeserialization(object sender);
    }

    public class CustomXmlSerializer : XmlSerializer
    {
        protected override object Deserialize(XmlSerializationReader reader)
        {
            var result = base.Deserialize(reader);

            var deserializedCallback = result as IXmlDeserializationCallback;
            if (deserializedCallback != null)
            {
                deserializedCallback.OnXmlDeserialization(this);
            }

            return result;
        }
    }
}



回答2:


The accepted solution didn't quite work for me. The overridden Deserialize() method never got called. I believe this is because that method is not public and is therefore called by one (or more) of the public Deserialize() methods, but not all of them.

Here's an implementation that works by method hiding and makes use of the existing IDeserializationCallback interface so any deserialization using non-xml methods can still trigger the OnDeserialization() method of that interface. It also uses reflection to traverse child properties to see if they also implement IDeserializationCallback and calls them accordingly.

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization;
using System.Xml;
using System.Xml.Serialization;

namespace Xml.Serialization
{
    class XmlCallbackSerializer : XmlSerializer
    {
        public XmlCallbackSerializer(Type type) : base(type)
        {
        }

        public XmlCallbackSerializer(XmlTypeMapping xmlTypeMapping) : base(xmlTypeMapping)
        {
        }

        public XmlCallbackSerializer(Type type, string defaultNamespace) : base(type, defaultNamespace)
        {
        }

        public XmlCallbackSerializer(Type type, Type[] extraTypes) : base(type, extraTypes)
        {
        }

        public XmlCallbackSerializer(Type type, XmlAttributeOverrides overrides) : base(type, overrides)
        {
        }

        public XmlCallbackSerializer(Type type, XmlRootAttribute root) : base(type, root)
        {
        }

        public XmlCallbackSerializer(Type type, XmlAttributeOverrides overrides, Type[] extraTypes,
            XmlRootAttribute root, string defaultNamespace) : base(type, overrides, extraTypes, root, defaultNamespace)
        {
        }

        public XmlCallbackSerializer(Type type, XmlAttributeOverrides overrides, Type[] extraTypes,
            XmlRootAttribute root, string defaultNamespace, string location)
            : base(type, overrides, extraTypes, root, defaultNamespace, location)
        {
        }

        public new object Deserialize(Stream stream)
        {
            var result = base.Deserialize(stream);

            CheckForDeserializationCallbacks(result);

            return result;
        }

        public new object Deserialize(TextReader textReader)
        {
            var result = base.Deserialize(textReader);

            CheckForDeserializationCallbacks(result);

            return result;
        }

        public new object Deserialize(XmlReader xmlReader)
        {
            var result = base.Deserialize(xmlReader);

            CheckForDeserializationCallbacks(result);

            return result;
        }

        public new object Deserialize(XmlSerializationReader reader)
        {
            var result = base.Deserialize(reader);

            CheckForDeserializationCallbacks(result);

            return result;
        }

        public new object Deserialize(XmlReader xmlReader, string encodingStyle)
        {
            var result = base.Deserialize(xmlReader, encodingStyle);

            CheckForDeserializationCallbacks(result);

            return result;
        }

        public new object Deserialize(XmlReader xmlReader, XmlDeserializationEvents events)
        {
            var result = base.Deserialize(xmlReader, events);

            CheckForDeserializationCallbacks(result);

            return result;
        }

        public new object Deserialize(XmlReader xmlReader, string encodingStyle, XmlDeserializationEvents events)
        {
            var result = base.Deserialize(xmlReader, encodingStyle, events);

            CheckForDeserializationCallbacks(result);

            return result;
        }

        private void CheckForDeserializationCallbacks(object deserializedObject)
        {
            var deserializationCallback = deserializedObject as IDeserializationCallback;

            if (deserializationCallback != null)
            {
                deserializationCallback.OnDeserialization(this);
            }

            var properties = deserializedObject.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);

            foreach (var propertyInfo in properties)
            {
                if (propertyInfo.PropertyType.GetInterface(typeof(IEnumerable<>).FullName) != null)
                {
                    var collection = propertyInfo.GetValue(deserializedObject) as IEnumerable;

                    if (collection != null)
                    {
                        foreach (var item in collection)
                        {
                            CheckForDeserializationCallbacks(item);
                        }
                    }
                }
                else
                {
                    CheckForDeserializationCallbacks(propertyInfo.GetValue(deserializedObject));
                }
            }
        }
    }
}



回答3:


I tried the solution provided by abatishchev but as pointed out by the comments below his answer, the Deserialize method in the custom serializer never seems to get called.

I was able to get this working by overloading all the different Deserialize overloads I would need so that it would always call the custom method.

protected object Deserialize(System.IO.StringReader reader)
{
    var result = base.Deserialize(reader);

    CallBack(result);

    return result;
}

protected object Deserialize(System.IO.TextReader reader)
{
    var result = base.Deserialize(reader);

    CallBack(result);

    return result;
}

protected object Deserialize(System.Xml.XmlReader reader)
{
    var result = base.Deserialize(reader);

    CallBack(result);

    return result;
}

protected object Deserialize(System.IO.Stream stream)
{
    var result = base.Deserialize(stream);

    CallBack(result);

    return result;
}

private void CallBack(object result)
{
    var deserializedCallback = result as IXmlDeserializationCallback;
    if (deserializedCallback != null)
    {
        deserializedCallback.OnXmlDeserialization(this);
    }
}

This way I actually see the Deserialize method getting called.




回答4:


A toughie, since XmlSerializer doesn't support serialization callback events. Is there any way you could use DataContractSerializer? That does, but doesn't allow attributes (like @name above).

Otherwise; you could implement IXmlSerializable, but that is lots of work, and very error-prone.

Otherwise - perhaps checking the caller via the stack, but that is very brittle, and smells ripe.




回答5:


After wasting some time with first answer, I adopted code from HotN's post, except for CheckForDeserializationCallbacks:

private static void ProcessOnDeserialize(object _result) {
  var type = _result != null ? _result.GetType() : null;
  var methods = type != null ? type.GetMethods().Where(_ => _.GetCustomAttributes(true).Any(_m => _m is OnDeserializedAttribute)) : null;
  if (methods != null) {
    foreach (var mi in methods) {
      mi.Invoke(_result, null);
    }
  }
  var properties = type != null ? type.GetProperties().Where(_ => _.GetCustomAttributes(true).Any(_m => _m is XmlElementAttribute || _m is XmlAttributeAttribute)) : null;
  if (properties != null) {
    foreach (var prop in properties) {
      var obj = prop.GetValue(_result, null);
      var enumeration = obj as IEnumerable;
      if (obj is IEnumerable) {
        foreach (var item in enumeration) {
          ProcessOnDeserialize(item);
        }
      } else {
        ProcessOnDeserialize(obj);
      }
    }
  }
}

This allows using of standard [OnDeserialized].

UPD. Updated post for recursive walk on object tree.




回答6:


I use a factory method which add more logic after the XML structured object has been deserialized. Such logic includes restoring the internal relationship (child-parent, sibling..) between object members.




回答7:


In my case it was a collection of objects, so used an excepted solution having to modify it a bit

  private static void PostDeserializedProcess<T>(T deserializedObj)
    {
        var deserializedCallback = deserializedObj as IXmlPostDeserializationCallback;
        if (deserializedCallback != null)
        {
            deserializedCallback.OnXmlDeserialized(deserializedObj);
        }
        else
        {
            // it could be a List of objects 
            // and we need to check for every object in the list
            var collection = deserializedObj as System.Collections.IEnumerable;
            if (collection != null)
            {
                foreach (var item in collection)
                {
                    PostDeserializedProcess(item);
                }
            }
        }
    }

And then everything is working perfectly




回答8:


I struggled somewhat as well getting the above solutions to work. I found the simplest solution to have my OnDeserialization() callbacks fire while using XmlSerializer was to chain a call to BinaryFormatter afterwards. My class already had a GetClone() method so it was rather straightforward and negated all my attempts at overriding XmlSerializer

public static Foo Deserialize(string path) {
    Foo foo;
    XmlSerializer xmlSerializer = new XmlSerializer(typeof(Foo));
    using (StreamReader textReader = new StreamReader(path)) {
        foo = (Foo)xmlSerializer.Deserialize(textReader); // this does NOT fire the OnDeserialization callbacks
    }
    return foo.GetClone();
}

public Foo GetClone() {
    using (var ms = new MemoryStream()) {
        var formatter = new BinaryFormatter();
        formatter.Serialize(ms, this);
        ms.Position = 0;
        return (Foo)formatter.Deserialize(ms); // this DOES fire the OnDeserialization callbacks
    }
}



回答9:


The accepted solution didn't work for me either.

In order to make it finally work, I needed to modify HotN's solution a little bit. In particular I added the

propertyInfo.GetIndexParameters().Length

check (according to: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.propertyinfo.getvalue), in order to avoid a missmatching parameters exception.

Also I have some properties that are not supposed to get mapped. I attributed them with [XmlIgnore], but the provided solution still processed them. But adding a check check for whether the passed parameter object is null did the trick.

namespace Custom.Xml.Serialization
{
    public interface IXmlDeserializationCallback
    {
        void OnXmlDeserialization(object sender);
    }

    public class CustomXmlSerializer : XmlSerializer
    {
        public CustomXmlSerializer(Type type) : base(type) { }

        public new object Deserialize(Stream stream)
        {
            var result = base.Deserialize(stream);

            CheckForDeserializationCallbacks(result);

            return result;
        }

        private void CheckForDeserializationCallbacks(object deserializedObject)
        {
            if (deserializedObject == null)
                return;

            var deserializationCallback = deserializedObject as IXmlDeserializationCallback;

            if (deserializationCallback != null)
            {
                deserializationCallback.OnXmlDeserialization(this);
            }

            var properties = deserializedObject.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);

            foreach (var propertyInfo in properties)
            {

                if (propertyInfo.PropertyType.GetInterface(typeof(IEnumerable<>).FullName) != null)
                {
                    var collection = propertyInfo.GetValue(deserializedObject) as IEnumerable;

                    if (collection != null)
                    {
                        foreach (var item in collection)
                        {
                            CheckForDeserializationCallbacks(item);
                        }
                    }
                }
                else
                {
                    if (propertyInfo.GetIndexParameters().Length == 0)
                        CheckForDeserializationCallbacks(propertyInfo.GetValue(deserializedObject));
                }
            }
        }
    }
}


来源:https://stackoverflow.com/questions/1266547/how-do-you-find-out-when-youve-been-loaded-via-xml-serialization

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!