使用反射在嵌套对象中设置属性

3
我试图使用反射来设置obj1中的Address1,但我无法找到如何获取正确对象引用的方法。我不确定如何获取Address1实例的引用并将其传递到SetValue()的第一个参数中。
public class StackOverflowReflectionTest
    {
        [Fact]
        public void SetDeepPropertyUsingReflection()
        {
            var breadCrumb = ".Addresses[0].Address1";

            var obj1 = new Person()
            {
                Name = "Eric",
                Addresses = new List<Address>()
                {
                    new Address() {Address1 = "123 First Street"}
                }
            };

            var newAddress1 = "123 Second Street";

            var propNames = breadCrumb.Split(".");
            for (var index = 0; index < propNames.Length; index++)
            {
                var propName = propNames[index];
                if (propName.Contains("["))
                {
                    var propNameToGet = propName.Substring(0, propName.IndexOf("[", StringComparison.Ordinal));
                    var prop = obj1.GetType().GetProperty(propNameToGet);
                    var leftBrace = propName.IndexOf("[", StringComparison.Ordinal);
                    var rightBrace = propName.IndexOf("]", StringComparison.Ordinal);
                    var position = int.Parse(propName.Substring(leftBrace + 1, rightBrace - leftBrace - 1));

                    var propNameToSet = propNames[index + 1];


                    var propToSet = prop.PropertyType.GetGenericArguments()[position].GetProperty(propNameToSet);
                    propToSet.SetValue(obj1, newAddress1);
                }
                else
                {
                    //TODO: deal with different types
                }
            }
        }

        public class Person
        {
            public string Name { get; set; }
            public IList<Address> Addresses { get; set; }
        }

        public class Address
        {
           public string Address1 { get; set; }
        }
    }

第二轮 根据 Ed 的反馈,仍然卡在如何获取这一行的值:var value = property.GetValue(obj, new object[] { indexPart });

  public class StackOverflowReflectionTest
    {
        [Fact]
        public void SetDeepPropertyUsingReflectionRound2()
        {
            var breadCrumb = "Addresses[0].Address1";

            var obj1 = new Person()
            {
                Name = "Eric",
                Addresses = new List<Address>()
                {
                    new Address() {Address1 = "123 First Street"}
                }
            };

            var newAddress1 = "123 Second Street";

            SetPropertyValueByPath(obj1, breadCrumb, newAddress1);
        }

        public bool CrackPropertyName(string name, out string namePart, out object indexPart)
        {
            if (name.Contains("["))
            {
                namePart = name.Substring(0, name.IndexOf("[", StringComparison.Ordinal));

                var leftBrace = name.IndexOf("[", StringComparison.Ordinal);
                var rightBrace = name.IndexOf("]", StringComparison.Ordinal);

                indexPart = name.Substring(leftBrace + 1, rightBrace - leftBrace - 1);
                return true;
            }
            else
            {
                namePart = name;
                indexPart = null;
                return false;
            }
        }

        public object GetPropertyValue(object obj, string name)
        {
            if(CrackPropertyName(name, out var namePart, out var indexPart))
            {
                var property = obj.GetType().GetProperty(namePart);
                var value = property.GetValue(obj, new object[] { indexPart });
                return value;
            }
            else
            {
                return obj.GetType().GetProperty(name);
            }

        }

        public void SetPropertyValue(object obj, string name, object newValue)
        {
            var property = typeof(Address).GetProperty(name);
            property.SetValue(obj, newValue);
        }

        public void SetPropertyValueByPath(object obj, string path, object newValue)
        {
            var pathSegments = path.Split(".");

            if (pathSegments.Length == 1)
            {
                SetPropertyValue(obj, pathSegments[0], newValue);
            }
            else
            {
                ////  If more than one remaining segment, recurse

                var child = GetPropertyValue(obj, pathSegments[0]);

                SetPropertyValueByPath(child, String.Join(".", pathSegments.Skip(1)), newValue);
            }
        }

        public class Person
        {
            public string Name { get; set; }
            public IList<Address> Addresses { get; set; }
        }

        public class Address
        {
           public string Address1 { get; set; }
        }
}

解决方案:

public class StackOverflowReflectionTest
    {
[Fact]
        public void SetDeepPropertyUsingReflectionSolution()
        {
            var breadCrumb = "Addresses[0].Address1";

            var obj1 = new Person()
            {
                Name = "Eric",
                Addresses = new List<Address>()
                {
                    new Address() {Address1 = "123 First Street"}
                }
            };

            var newAddress1 = "123 Second Street";

            SetPropertyValueByPath(obj1, breadCrumb, newAddress1);
        }

        public bool CrackPropertyName(string name, out string namePart, out object indexPart)
        {
            if (name.Contains("["))
            {
                namePart = name.Substring(0, name.IndexOf("[", StringComparison.Ordinal));

                var leftBrace = name.IndexOf("[", StringComparison.Ordinal);
                var rightBrace = name.IndexOf("]", StringComparison.Ordinal);

                indexPart = name.Substring(leftBrace + 1, rightBrace - leftBrace - 1);
                return true;
            }
            else
            {
                namePart = name;
                indexPart = null;
                return false;
            }
        }

        public object GetPropertyValue(object obj, string name)
        {
            if(CrackPropertyName(name, out var namePart, out var indexPart))
            {

                var property = obj.GetType().GetProperty(namePart);
                var list = property.GetValue(obj);
                var value = list.GetType().GetProperty("Item").GetValue(list, new object[] { int.Parse(indexPart.ToString()) });
                return value;
            }
            else
            {
                return obj.GetType().GetProperty(namePart);
            }

        }

        public void SetPropertyValue(object obj, string name, object newValue)
        {
            var property = typeof(Address).GetProperty(name);
            property.SetValue(obj, newValue);
        }

        public void SetPropertyValueByPath(object obj, string path, object newValue)
        {
            var pathSegments = path.Split(".");

            if (pathSegments.Length == 1)
            {
                SetPropertyValue(obj, pathSegments[0], newValue);
            }
            else
            {
                ////  If more than one remaining segment, recurse
                var child = GetPropertyValue(obj, pathSegments[0]);

                SetPropertyValueByPath(child, String.Join(".", pathSegments.Skip(1)), newValue);
            }
        }

        public class Person
        {
            public string Name { get; set; }
            public IList<Address> Addresses { get; set; }
        }

        public class Address
        {
           public string Address1 { get; set; }
        }
}
3个回答

2

Type.GetGenericArguments()并不像你认为的那样做任何事情。

你需要使用递归。给定"Foo.Bar[1].Baz",获取Foo。从中获取Bar[1]。从其父级获取BazPropertyInfo,使用它来设置FooBar[1]属性的Baz属性的值。

具体分解如下:

  1. Write a method that "cracks" a property name and uses out parameters to return both the name part and the index value part: "IndexedProperty[1]" goes in; "IndexedProperty" and integer 1 come out. "FooBar" goes in, "FooBar" and null come out. It returns true if there's an indexer, false if not.

    bool CrackPropertyName(string name, out string namePart, out object indexPart)
    
  2. Write a method that takes an object, and a string "PropertyName" or "IndexedPropety[0]" (not a path -- no dot) and returns the value of that property on that object. It uses CrackPropertyName() to simplify its job.

    object GetPropertyValue(object obj, string name)
    
  3. Write a method that sets a property value by name (not by path, just by name). Again, it uses CrackPropertyName() to simplify its job.

    void SetPropertyValue(object obj, string name, object newValue)
    
  4. A recursive method using the above:

    void SetPropertyValueByPath(object obj, string path, object newvalue)
    {
        var pathSegments = /* split path on '.' */;
    
        if (pathSegments.Length == 1)
        {
            SetPropertyValue(obj, pathSegments[0], newValue); 
        }
        else
        {
            //  If more than one remaining segment, recurse
    
            var child = GetNamedPropertyvalue(obj, pathSegments[0]);
    
            return SetPropertyValueByPath(obj, String.Join(".", pathSegments.Skip(1)), newValue);
        }
    }
    

这些方法都相当简单。既然你已经在使用反射,那么你可以写一个非泛型方法,设置任何东西的任何属性。


我添加了第二个回合,但在这一行中遇到了困难:var value = property.GetValue(obj, new object[] { indexPart }); - Eric Walter
给我一点时间,我想我找到了问题所在。 - Eric Walter
做得好。感谢大家,向 Ed 的“全面解决方案”喊话。 - Eric Walter

1
如果您想使用反射从Person的实例中获取Addresses属性的值,可以这样做:
 var myPerson = new Person()
        {
            Name = "Eric",
            Addresses = new List<Address>()
            {
                new Address() {Address1 = "123 First Street"}
            }
        };  
var property = typeof(Person).GetProperty("Addresses");
var addresses = (IList<Address>) property.GetValue(myPerson );

首先,您需要找到属性 - PropertyInfo 实例 - 它属于 Person 类型。然后,您需要检索特定 Person 实例 myPerson 的该属性的值。 addresses 是一个 IList<Address>,所以使用反射获取列表中特定的 Address 没有太大用处。但是如果出于某种原因您想要这样做:
private Address GetAddressAtIndex(IList<Address> addresses, int index)
{
    var property = typeof(IList<Address>).GetProperty("Item");
    var address = (Address) property.GetValue(addresses, new object []{index});
    return address;
}

这基本上与第一个示例相同,只是在此情况下,属性(Item)需要一个索引。因此,我们使用接受一个或多个索引的GetValue重载。
现在你有了一个Address实例。我将每个步骤都分开进行,因为它们都是单独的步骤。没有一个步骤可以执行整个操作。
如果您有一个地址实例,并且想要使用反射来设置Address1属性:
private void SetAddress1OnAddress(Address address, string address1Value)
{
    var property = typeof(Address).GetProperty("Address1");
    property.SetValue(address, address1Value);
}

非常相似。您首先检索Address1属性,然后调用其SetValue方法,在特定实例上设置值,如果Address存在。

我要加一个警告,就是如果你发现自己不得不这样做的时候,95%的情况下是出了问题,你不得不打破紧急玻璃。如果你使用反射,那意味着你不知道你有什么类型的对象。除非你知道你有什么类型的对象,否则你就不知道要查找什么属性。但是像你的类名所示(StackOverflowReflectionTest),你只是在试验。这很有帮助,以防你不得不阅读别人的代码。 - Scott Hannen
为了让您全面了解我的意图,我有一个具备离线功能的Xamarin应用程序,其中包含一个后台服务,每五分钟同步一次数据。当发生同步时,我需要将这些更改传播到UI,不幸的是,ViewModels的形状与SQLite中的表结构大不相同,因此想法是向UI发送无效消息,让它从SQLite重新查询其数据,并进行深度比较以查找差异并相应地更新模型。 - Eric Walter

1
这是我在LINQPad中编写的快速而简单的代码,用于更改您定义的对象的Address1属性。
void Main()
{
    var obj1 = new Person()
    {
        Name = "Eric",
        Addresses = new List<Address>()
                {
                    new Address() {Address1 = "123 First Street"}
                }
    };

    var index = 0;

    var addressList = typeof(Person)
             .GetProperty("Addresses")
             .GetValue(obj1);

    var address = addressList.GetType()
             .GetProperty("Item")
             .GetValue(addressList, new object[]{index});

    address.GetType()
             .GetProperty("Address1")
             .SetValue(address,"321 Fake Street");

    Console.WriteLine(obj1.Addresses[index].Address1); // Outputs 321 Fake Street
}

// Define other methods and classes here
public class Person
{
    public string Name { get; set; }
    public IList<Address> Addresses { get; set; }
}

public class Address
{
    public string Address1 { get; set; }
}

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