将Xml反序列化为对象,使用反射确定类型。

3

我正在使用Xml存储应用程序设置,在运行时更改并在应用程序执行期间多次序列化和反序列化。

有一个Xml元素可以容纳任何可序列化类型,并且应该从属性类型为Object的序列化序列化和反序列化。

[Serializable]
public class SetpointPoint
{
    [XmlAttribute]
    public string InstrumentName { get; set; }
    [XmlAttribute]
    public string Property { get; set; }
    [XmlElement]
    public object Value { get; set; }

} // (not comprehensive, only important properties displayed)

XML,

<?xml version="1.0" encoding="utf-8"?>
<StationSetpoints xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xsi:schemaLocation="http://www.w3schools.com StationSetpoints.xsd">
  <Setpoint PartNumber="107983">
    <Point InstrumentName="PD Stage" Property="SetPoint">
      <Value xsi:type="xsd:string">3</Value>
    </Point>
    <Point InstrumentName="TR Camera" Property="MeasurementRectangle" StationSetpointMemberType="Property">
      <Value xsi:type="xsd:string">{X=145,Y=114,Width=160,Height=75}</Value>
    </Point>
  </Setpoint>
</StationSetpoints>

我反序列化Xml并解析属性以通过"InstrumentName"查找仪器对象,该仪器将具有与Xml属性"Property"相同的属性,并且我的意图是将该仪器.property设置为xml中的Value元素。像反射一样轻松地转换对象(在vb.net中)。
Dim ii = InstrumentLoader.Factory.GetNamed(point.InstrumentName)
Dim pi = ii.GetType().GetProperty(point.Property)
Dim tt = pi.PropertyType
Dim vt = Convert.ChangeType(point.Value, tt)
pi.SetValue(ii, vt)

没错,如果 point.Value 是一个对象的话,那么这将起作用,但实际上它并不是。从对象中序列化出来的东西最终变成了一个字符串。在属性是 Double 类型的情况下,我们会得到

<Value xsi:type="xsd:string">3</Value>

当一个 System.Drawing.Rectangle,产生了 "3" 的结果。

<Value xsi:type="xsd:string">{X=145,Y=114,Width=160,Height=75}</Value>

产生的结果为 "{X=145,Y=114,Width=160,Height=75}"

那么有没有一种方法可以直接将值类型或对象的Xml表示转换为.NET等效类型呢?

(或者我必须使用Reflection/System.Activator手动实例化对象并转换(对于基元类型),或解析属性和值(对于非基元类型)?)


1
您可以使用XmlElement属性多次注释Value属性,指定正确的类型,例如:[XmlElement(type=typeof(string)),XmlElement(type=typeof(System.Drawing.Rectangle))],然后使用模式匹配来读取值。 - Eldar
XmlSerializer只支持在派生类型被静态预声明时进行序列化。要做到这一点,请参见使用XmlSerializer序列化派生类 - dbc
@Eldar 我想让它开放式的支持其他类型。配置仅在Xml中,无需重新编译。我真的是指任何可以用Xml表示的东西,比如矩形,当然还有基本类型。 - djv
@dbc,感谢您提供的链接,但我不明白它如何适用于我的问题。 - djv
@dbc 我还没有尝试过 Color,但我可以接受它不可序列化的事实,因为我有一个 Xml 元素,可以容纳任何可序列化类型,如基元和诸如矩形、点等美好的东西,以及其他未来可能起作用的东西。我的代码库有许多不同类型的属性仪器,我希望保持开放,支持任何这些属性,只要它们是 xmlserializable 的,而无需重新编译。我知道一些类型是不可 xmlserializable 的,例如一些(全部?)窗体控件,我可以不用它们... - djv
显示剩余3条评论
2个回答

2

嗯,我觉得我已经成功解决了这个问题,但解决方案并不是很完美。因为它包含了大量使用反射(IL Emit)。

我建立了一个动态类型生成器,扩展了SetpointPoint并重写了Value属性,以便您可以设置我在评论中提到的自定义属性。它看起来像下面这样:

public class DynamicTypeBuilder
{
    private static readonly MethodAttributes getSetAttr =
        MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.SpecialName |
            MethodAttributes.HideBySig;

    private static readonly AssemblyName aName = new AssemblyName("DynamicAssemblyExample");
    private static readonly AssemblyBuilder ab =
         AssemblyBuilder.DefineDynamicAssembly(
            aName,
            AssemblyBuilderAccess.Run);
    private static readonly ModuleBuilder mb =
        ab.DefineDynamicModule(aName.Name + ".dll");

    public Type BuildCustomPoint(Type valueType)
    {
        var tb = mb.DefineType(
            "SetpointPoint_" + valueType.Name,
             TypeAttributes.Public, typeof(SetpointPoint));

        var propertyBuilder = tb.DefineProperty("Value",
                                                       PropertyAttributes.HasDefault,
                                                       typeof(object),
                                                       null);
        var fieldBuilder = tb.DefineField("_value",
                                                   typeof(object),
                                                   FieldAttributes.Private);
        var getBuilder =
      tb.DefineMethod("get_Value",
                                 getSetAttr,
                                 typeof(object),
                                 Type.EmptyTypes);

        var getIL = getBuilder.GetILGenerator();

        getIL.Emit(OpCodes.Ldarg_0);
        getIL.Emit(OpCodes.Ldfld, fieldBuilder);
        getIL.Emit(OpCodes.Ret);

        var setBuilder =
            tb.DefineMethod("set_Value",
                                       getSetAttr,
                                       null,
                                       new Type[] { typeof(object) });

        var setIL = setBuilder.GetILGenerator();

        setIL.Emit(OpCodes.Ldarg_0);
        setIL.Emit(OpCodes.Ldarg_1);
        setIL.Emit(OpCodes.Stfld, fieldBuilder);
        setIL.Emit(OpCodes.Ret);

        // Last, we must map the two methods created above to our PropertyBuilder to
        // their corresponding behaviors, "get" and "set" respectively.
        propertyBuilder.SetGetMethod(getBuilder);
        propertyBuilder.SetSetMethod(setBuilder);

        var xmlElemCtor = typeof(XmlElementAttribute).GetConstructor(new[] { typeof(Type) });
        var attributeBuilder = new CustomAttributeBuilder(xmlElemCtor, new[] { valueType });
        propertyBuilder.SetCustomAttribute(attributeBuilder);

        return tb.CreateType();
    }
}

对您的类进行轻微修改,使得Value属性是虚拟的,在动态类型中我们可以覆盖它。

[XmlElement]
public virtual object Value { get; set; }
DynamicTypeBuilder 的作用是动态生成一个类,就像这样:
public class SetpointPoint_Double : SetpointPoint
{
    [XmlElement(typeof(double))]
    public override object Value { get; set; }
}


我们还需要一个根类,其中包含我们的Point类:
[Serializable]
public class Root
{
    [XmlElement("Point")]
    public SetpointPoint Point { get; set; }
}

这是我们如何测试代码的方法:

var builder = new DynamicTypeBuilder();
var doublePoint = builder.BuildCustomPoint(typeof(double));
var pointPoint = builder.BuildCustomPoint(typeof(Point));
var rootType = typeof(Root);
var root = new Root();
var root2 = new Root();
var instance1 = (SetpointPoint)Activator.CreateInstance(doublePoint);
var instance2 = (SetpointPoint)Activator.CreateInstance(pointPoint);

instance1.Value = 1.2;
instance2.Value = new Point(3, 5);

root.Point = instance1;
root2.Point = instance2;

// specifying used types here as the second parameter is crucial
// DynamicTypeBuilder can also expose a property for derived types.
var serialzer = new XmlSerializer(rootType, new[] { doublePoint, pointPoint });
TextWriter textWriter = new StringWriter();
serialzer.Serialize(textWriter, root);
var r = textWriter.ToString();
/*
 output :
<?xml version="1.0" encoding="UTF-8"?>
<Root xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
   <Point xsi:type="SetpointPoint_Double">
      <Value xsi:type="xsd:double">1.2</Value>
   </Point>
</Root>
 */
textWriter.Dispose();
textWriter = new StringWriter();
serialzer.Serialize(textWriter, root2);

var x = textWriter.ToString();
/*
 output 
<?xml version="1.0" encoding="UTF-8"?>
<Root xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
   <Point xsi:type="SetpointPoint_Point">
      <Value xsi:type="Point">
         <X>3</X>
         <Y>5</Y>
      </Value>
   </Point>
</Root>
 */

var d = (Root)serialzer.Deserialize(new StringReader(x));
var d2 = (Root)serialzer.Deserialize(new StringReader(r));

PrintTheValue(d);
PrintTheValue(d2);

void PrintTheValue(Root r)
{
    // you can use reflection here
    if (r.Point.Value is Point p)
    {
        Console.WriteLine(p.X);
    }
    else if (r.Point.Value is double db)
    {
        Console.WriteLine(db);
    }
}

我非常感谢这份工作,但是我想使用非常宽松的类型,并且当需要反序列化不同类型(例如bool)时,必须重新编译此解决方案。好吧,那么请为所有基元类型提供写入支持,以及像“矩形”或“线条”等类型。因此,这不能满足我所需的未来可扩展性程度。 - djv

1
我决定允许序列化器将类(在矩形的情况下是结构)序列化为带有属性名称和值对的字符串,如下所示:{X=145,Y=114,Width=160,Height=75},并将基元序列化为值,如3
然后解析此Xml表示形式为可以迭代的一对,并相应地设置属性和字段。必须对装箱结构进行一些操作,因为它们的基础类型似乎在装箱时无法识别,因此vb中的解决方案是使用Dim boxed As ValueType(感谢这个评论)。
Dim ii = InstrumentLoader.Factory.GetNamed(point.InstrumentName)
Dim pi = ii.GetType().GetProperty(point.Property)
Dim tt = pi.PropertyType
If valueString.StartsWith("{") Then
    Dim instance = CTypeDynamic(Activator.CreateInstance(tt), tt)
    Dim instanceType = instance.GetType()
    Dim boxed As ValueType = CType(instance, ValueType)
    Dim propertiesAndValues =
        valueString.Replace("{", "").Replace("}", "").Split(","c).
        ToDictionary(Function(s) s.Split("="c)(0), Function(s) s.Split("="c)(1))
    For Each p In instanceType.GetProperties()
        If propertiesAndValues.ContainsKey(p.Name) Then
            Dim t = p.PropertyType
            Dim v = Convert.ChangeType(propertiesAndValues(p.Name), t)
            p.SetValue(boxed, v, Nothing)
        End If
    Next
    For Each f In instanceType.GetFields()
        If propertiesAndValues.ContainsKey(f.Name) Then
            Dim t = f.FieldType
            Dim v = Convert.ChangeType(propertiesAndValues(f.Name), t)
            f.SetValue(boxed, v)
        End If
    Next
    pi.SetValue(ii, boxed)
Else
    Dim vt1 = Convert.ChangeType(valueString, tt)
    pi.SetValue(ii, vt1)
End If

我还没有尝试过在XmlSerializable类(而不是结构体)上进行此操作,但正在进行中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接