Link to home
Start Free TrialLog in
Avatar of AlliedAdmin
AlliedAdmin

asked on

Get property by name, save it's underlying object

I am trying to figure out a very generic way to save some data from an app I'm working on. I'd like to be able to set a property of an object, only using it's name (as a string), and then save the object that it belongs to. Below is some code to show what I'm trying to achieve. In the Save() function of Foo I have some comments that say/show what I'd like to get working.
Basically I want to be able to use a string to get and set the value of a property, and then call Save() on the object that it belongs to:
public class Person
{
    public Address HomeAddress { get; set; }
    public Person() { HomeAddress = new Address(); }
}

public class Address
{
    public string Street { get; set; }
    public string Street2 { get; set; }
    public Address() { }
    public void Save() { /* Do Save stuff */ }
}

public class Foo
{
    public Person person { get; set; }
    public Foo()
    {
        person = new Person();
        Save("123 Test Street", "Street");
    }

    public void Save(string value, string propertyName)
    {
        PropertyInfo[] propInfos = person.GetType().GetProperties();
        PropertyInfo info = GetProperty(propInfos, propertyName);

        // info is returned with the information for the Street property in HomeAddress.
        // Is there a way to set HomeAddress.Street = value, and then call HomeAddress.Save() using Reflection?
    }

    private PropertyInfo GetProperty(PropertyInfo[] pis, string propertyName)
    {
        foreach (PropertyInfo pi in pis)
        {
            if (pi.Name == propertyName)
            {
                return pi;
            }
            else
            {
                if (pi.PropertyType.FullName.StartsWith("MyLibraryName"))
                {
                    PropertyInfo p2 = GetProperty(pi.PropertyType.GetProperties(), propertyName);
                    if (p2 != null)
                        return p2;
                }
            }
        }
        return null;
    }
}

Open in new window

Avatar of Carlos Villegas
Carlos Villegas
Flag of United States of America image

Sure buddy, two lines:
public class Foo
{
    public Person person { get; set; }
    public Foo()
    {
        person = new Person();
        Save("123 Test Street", "Street");
    }

    public void Save(string value, string propertyName)
    {
        // info is returned with the information for the Street property in HomeAddress.
        // Is there a way to set HomeAddress.Street = value, and then call HomeAddress.Save() using Reflection?
            
        // SURE!
        System.Reflection.PropertyInfo prop = person.GetType().GetProperty(propertyName);
        prop.SetValue(person, value, null);
    }
}

Open in new window

Avatar of AlliedAdmin
AlliedAdmin

ASKER

Sorry I should have included that line.  I know how to set the value of the property like that, but now I need to get the actual instantiated object that the property belongs to, and call the save function of that object (the Save function saves that to the database).
ASKER CERTIFIED SOLUTION
Avatar of Carlos Villegas
Carlos Villegas
Flag of United States of America image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Sorry I know that my english is bad hehe
That's really close, but still not exactly what I need.
For example, my recursive GetProperty function returns the property of either the base class, in this case Person, or of any of it's sub property-classes, in this case Address.  So in your example above, if you try to get the property with the name Street, which is not part of Person, that code won't work because my GetProperty will return the PropertyInfo for Street, but it belongs to HomeAddress.  So if you call prop.SetValue(person, value, null), it errors out because Street is a property of Address not Person.

Basically I'll never know exactly which object that property belongs to.  So in any part of the code that uses the object 'person' will break.  Instead I need to get the object that that property belongs to.

Here is a revised version of my original code, plugging in the info you've provided.  I've added comments and changed object names to give further explain.
Specifically note the constructor of Foo, where I call the MyGenericSave() twice.  Ideally I'd like both of those to work and save correctly.

I greatly appreciate the help so far, It's very close to what I need.

public class Person
{
    public Address HomeAddress { get; set; }
    public string Name { get; set; }
    public Person() { HomeAddress = new Address(); }
}

public class Address
{
    public string Street { get; set; }
    public string Street2 { get; set; }
    public Address() { }
    public void Save() { /* Do Save stuff */ }
}

public class Foo
{
    public Person person { get; set; }
    public Foo()
    {
        person = new Person();
        // Both of these need to work, even though the properties belong to 2 different objects
        MyGenericSave("123 Test Street", "Street");
        MyGenericSave("John Doe", "Name");
    }

    public void MyGenericSave(string value, string propertyName)
    {
        Type type = person.GetType();
        PropertyInfo[] propInfos = type.GetProperties();
        PropertyInfo prop = GetProperty(propInfos, propertyName);
        // Won't always work, because the property may or may not belong to person.
        // I need to dynamically figure out which instantiated object the property belongs to here, instead of
        // always passing the person object
        prop.SetValue(person, value, null);

        // Again, we can't always use 'type' here because the property may or may not belong to
        // the Person object.
        FieldInfo fi = type.GetField(propertyName, BindingFlags.Instance | BindingFlags.Public);
        object couldBeAnyInstance = fi.GetValue(/* dynamic object */);

        Type couldBeAnyType = couldBeAnyInstance.GetType();
        MethodInfo methInfo = couldBeAnyType.GetMethod("Save");
        methInfo.Invoke(couldBeAnyInstance, null);
    }

    private PropertyInfo GetProperty(PropertyInfo[] pis, string propertyName)
    {
        foreach (PropertyInfo pi in pis)
        {
            if (pi.Name == propertyName)
            {
                return pi;
            }
            else
            {
                // Only dig deeper if it's an object of a type in my library
                if (pi.PropertyType.FullName.StartsWith("MyClassLibrary"))
                {
                    PropertyInfo p2 = GetProperty(pi.PropertyType.GetProperties(), propertyName);
                    if (p2 != null)
                        return p2;
                }
            }
        }
        return null;
    }
}

Open in new window

Avatar of Mike Tomlinson
...and how are you going to resolve name clashes?  If you can't ever know what object the property belongs to...then how can you also know that there won't be a "Name" property in one more of the objects within the traversed hierarchy?

How does this fit into your actual app?
Oops..."in one more" should read " in one OR more".
Hello!
Ok now I understand what you want, but a have a question by now:

In a person instance work for you format the property name to looking for in this way?
MyGenericSave("123 Test Street", "HomeAddress.Street");
MyGenericSave("Person name", "Name");

Open in new window

Also I think that a custom attribute can be useful to mark the classes that can be processed by your MyGenericSave code
I had thought of that, and am trying to figure out a solution.  The one way I thought about was that when I call MyGenericSave(), I could also pass the old value, then compare that to the value of the property before I set it.  If it's not the same, continue searching.  However I can think of times where even this may not work (such as a home address and billing address, and both are the same).

The app I'm working on is a re-write of a current app we use.  I'm re-writing it as a web app.  I'm testing the edit features in 2 different ways.  This way is very generic, and I'll try to give a brief example of how it's used:

Let's say your viewing the page with all the person's info.  All the data is displayed as Labels.  If you want to edit the first name for example, you double click on the first name.  This hides the label with the first name and displays a textbox with the first name.  After you make the changes, the controls visibility are swapped back to normal, and the MySaveGeneric function is called, with the text of the text box and the property that it is associated with.

The text boxes on the page are all mapped to the property they are associated with via a Dictionary in the ASP Application object.

Hopefully that makes sense, it's kind of hard to explain.
We like the idea of editing this way, just because it allows the users to view and edit on the same page, and it keeps the page pretty clean looking.

Our main concerns for this app are speed and server-side memory usage (hence the reason we're using the Dictionary object in the Application object, so all users have access to it).  Once/If I get this method working we're going to run tests and compare it's speed/memory usage to the more 'traditional' way of editing.
Yes, I had tried passing something like HomeAddress.Street, but was unable to get it to work like I wanted ( I had hoped that Type.GetProperty("HomeAddress.Street") would work, but it doesn't seem to like the full name like that.  This would actually be ideal.

I've never worked with the Reflection namespace so a lot of this is brand new to me.  I appreciate the help so far!
Right..."HomeAddress.Street" can't be directly related to a type, but could be parsed to develop a path to get to the right object.  A "fully qualified path" would be good since you wouldn't be searching for a match, but instead following that path.
Well, some years ago (2008) I did something like that usefully, is a shame that I dont have a copy of my code to remember many thing that I did but I have an idea, I will let you know my progress.
Yes Idle_Mind, its helps a lot.
I believe I have a solution!  I'm passing the fully qualified path for the property, then parsing it.  This allows me to get the property, as well as keep track of it's parent.
So far this code is working (for both saves).  I'll run some tests and see if I can break it.

Thanks so much for all the help!

public Foo()
    {
        person = new Person();
        // Both of these need to work, even though the properties belong to 2 different objects
        MyGenericSave("123 Test Street", "HomeAddress.Street");
        MyGenericSave("John Doe", "Name");
    }

    public void MyGenericSave(string value, string propertyName)
    {
        PropertyInfo pi = null;
        object currentObject = person;
        object lastObject = person;
        string[] splitter = new string[] { "." };
        string[] propNames = propertyName.Split(splitter, StringSplitOptions.None);
        string actualPropertyName = propNames[propNames.Length - 1];
        for (int i = 0; i < propNames.Length; i++)
        {
            pi = currentObject.GetType().GetProperty(propNames[i]);
            currentObject = pi.GetValue(currentObject, null);
            if (currentObject != null)
            {
                lastObject = currentObject;
            }
        }

        pi.SetValue(lastObject, value, null);

        MethodInfo mi = lastObject.GetType().GetMethod("Save");
        mi.Invoke(lastObject, null);
    }

Open in new window

SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial