我该如何从这种XML创建对象图?

时间:2010-10-13 14:25:52

标签: c# xml .net-3.5

好的,对那里的深层思想家的挑战:

我的soap服务器向我发送了类似这样的XML:

<?xml version='1.0' encoding='utf-8'?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
  <SOAP-ENV:Header xmlns:xxxxxx="...">
    <...bunch of soap header stuff.../>
  </SOAP-ENV:Header>
  <SOAP-ENV:Body>
    <queryResponse xmlns="...">
      <resultSet seqNo="0">
        <Types>
          <name>varchar</name>
          <value>varchar</value>
        </Types>
        <row>
          <name>field1</name>
          <value>0</value>
        </row>
        <row>
          <name>field2</name>
          <value>some string value</value>
        </row>
        <row>
          <name>field3</name>
          <value>false</value>
        </row>
        <... repeats for many more rows... />
      </resultSet>
    </queryResponse>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

如何获取<row>个节点并填充以下类:

public class SessionProperties
{
  public int IntField {get;set;}
  public string StringField {get;set;}
  public bool BooleanField {get;set;}

  // more fields...
}

我想避免手动填充SessionProperties个实例,例如

var myProps = new SessionProperties();
myProps.IntField = XElement("...").Value;  // I don't want to do this!

我想编写一组“通用”代码,可以填充SessionProperties实例,而无需将类属性硬编码到特定的XML节点。

此外,我不想编写IXmlSerializable实现,我认为这只会使代码更复杂。如果我错了,请告诉我。

最后,soap服务器可能会在将来向我发送额外的行节点,而我想做的就是尽可能更新SessionProperties类。是否有一些通用方法(可能使用自定义属性等)可以实现这一目标?

提前致谢!

3 个答案:

答案 0 :(得分:1)

我认为这应该是不言自明的:

    class Program
    {
        // map names in the XML to the names of SessionInfo properties;
        // you'll need to update this when you add a new property to
        // the SessionInfo class:
        private static Dictionary<string, string> PropertyMap = 
            new Dictionary<string, string>
        {
            {"field1", "StringProperty1"},
            {"field2", "IntProperty1"},
            {"field3", "BoolProperty1"},
        };

        // map CLR types to XmlConvert methods; you'll need one entry in
        // this map for every CLR type SessionInfo uses
        private static Dictionary<Type, Func<string, object>> TypeConverterMap = 
            new Dictionary<Type, Func<string, object>>
        {
            { typeof(bool), x => XmlConvert.ToBoolean(x)},
            { typeof(int), x => XmlConvert.ToInt32(x)},
            { typeof(string), x => x},
        };

        static void Main(string[] args)
        {
            // map SessionInfo's property names to their PropertyInfo objects
            Dictionary<string, PropertyInfo> properties = AppDomain.CurrentDomain.GetAssemblies()
                .SelectMany(x => x.GetExportedTypes())
                .Where(x => x.Name == "SessionInfo")
                .SelectMany(x => x.GetMembers())
                .Where(x => x.MemberType == MemberTypes.Property)
                .Cast<PropertyInfo>()
                .ToDictionary(x => x.Name);

            string xml =
                @"<example>
<row>
    <name>field1</name>
    <value>stringProperty</value>
</row>
<row>
    <name>field2</name>
    <value>123</value>
</row>
<row>
    <name>field3</name>
    <value>true</value>
</row>
</example>";
            XmlDocument d = new XmlDocument();
            d.LoadXml(xml);

            SessionInfo s = new SessionInfo();

            // populate the object's properties from the values in the XML
            foreach (XmlElement elm in d.SelectNodes("//row"))
            {
                string name = elm.SelectSingleNode("name").InnerText;
                string value = elm.SelectSingleNode("value").InnerText;
                // look up the property for the name in the XML and get its
                // PropertyInfo object
                PropertyInfo pi = properties[PropertyMap[name]];
                // set the property to the value in the XML, using the the converter for 
                // the property's type
                pi.SetValue(s, TypeConverterMap[pi.PropertyType](value), null);
            }

            // and the results:
            Console.WriteLine(s.StringProperty1);
            Console.WriteLine(s.IntProperty1);
            Console.WriteLine(s.BoolProperty1);
            Console.ReadKey();
        }

答案 1 :(得分:0)

嗯,那种XML有点不好。

如果是我,我会为你的肥皂结果创建一个包装类型,它实现ICustomTypeDescriptor。如果WPF绑定系统检测到您的类型具有自定义类型描述符,则它将在绑定时使用它。

自定义类型描述符执行与普通反射相同的操作,但允许类型创建者处理调用者的反射过程。默认实现TypeDescriptor可以适用于任何类型。但是,您可以为soap消息创建自己的类型描述符包装器,当查询属性时,可以从XML中选择正确的值并将其返回。

这样做的真正好处是您的绑定与普通POCO属性或DependencyProperties 完全相同。

换句话说,您的绑定可能如下所示:

<TextBox Text="{Binding Session.UserName}" /> 

即使您的实际类型没有UserName属性。绑定系统将查找逻辑重定向到您的自定义类型描述符的GetProperty方法,您可以在其中返回您控制的MethodInfo实例。当请求该值时,您可以查看soap XML以获取匹配的元素并将其返回。

您可以优雅地处理soap消息已更改且属性现在映射到其他位置的情况,或者当没有属性时(例如,返回默认值)。在您知道消息流量很大的情况下,您可以将类型描述符设置为重定向到使用依赖注入框架在运行时设置的ICustomTypeDescriptor实例的代理。

我并不是说这很容易,但一旦你掌握了核心概念,它肯定不会那么难。

答案 2 :(得分:0)

此解决方案演示了自定义属性的使用,正如Robert所说。

首先,自定义属性类定义:

// match the properties to the xml dynamically using this attribute...
[AttributeUsage(AttributeTargets.Property)]
public class DBSPropAttribute : Attribute
{
    public string MappingField { get; set; }
    public DBSPropAttribute(string fieldName)
    {
        MappingField = fieldName;
    }
}

然后将自定义属性应用于SessionProperties类,如下所示:

[DBSProp("archiveDays")]
public int ArchiveDays { get; set; }

在SessionProperties类中,我还要定义这个转换器字典,这是Robert的一个非常优雅的想法:

// map CLR types to convert methods; you'll need one entry in
// this map for every CLR type
private static readonly Dictionary<Type, Func<string, object>> TypeConverterMap =
    new Dictionary<Type, Func<string, object>>
{
    { typeof(bool), x => Convert.ToBoolean(x)},
    { typeof(int), x => Convert.ToInt32(x)},
    { typeof(string), x => x},
    { typeof(double), x => Convert.ToDouble(x)}
};

最后,该类将实现以下方法将XML绑定到对象属性:

public void SetPropertyValues(IEnumerable<XElement> elements)
{
    var propList = typeof(SessionProperties).GetProperties();

    foreach (var elm in elements)
    {
        var nm = elm.Element("name").Value;
        var val = elm.Element("value").Value;

        // MUST throw an exception if there are no matches...
        var pi = propList.First(c => c.GetCustomAttributes(true)
                   .OfType<DBSPropAttribute>()
                   .First()
                   .MappingField == nm);

        pi.SetValue(this, TypeConverterMap[pi.PropertyType](val), null);
    }
}

再次感谢罗伯特阐述导致这一答案的关键概念。我认为这是他同样有效的方法的一个很好的替代方案。