views:

116

answers:

3

I would like to be able to access the value of an object property to any depth having only the string-key of the property. Also, if possible, using collection indexing on List properties. So, If I have the string "Person.Surname" then I could get the value "Smith" from and instanciated CaseConductor object. So given some setup code like this ...

//- Load a caseConductor
var caseConductor = new CaseConductor();
caseConductor.CaseID = "A00001"; 
// person 
caseConductor.Person = new Person();
caseConductor.Person.Surname = "Smith" ;
caseConductor.Person.DOB = DateTime.Now ; 
// case note list
caseConductor.CaseNoteList = new List<Note>();
caseConductor.CaseNoteList.Add(new Note { NoteText = "A-1" , NoteDt  = DateTime.Now });
caseConductor.CaseNoteList.Add(new Note { NoteText = "B-2", NoteDt = DateTime.Now });
// I could do this ...
object val = caseConductor.SomeCleverFunction("Person.Surname");
// or this ...
object val = caseConductor.SomeCleverFunction("CaseNoteList[0].NoteText");

Has anyone done this before ? Here are some setup classes ...

class Note
    {
        public Guid NoteID { get; set; }
        public string NoteText { get; set; }
        public DateTime? NoteDt { get; set; }
    }
    public class Person
    {
        public Guid PersonID { get; set; }
        public string Surname { get; set; }
        public string Forename { get; set; }
        public DateTime? DOB { get; set; }
    }
    class CaseConductor
    {
        public String CaseID{get;set;}
        public Person Person { get; set; }
        public List<Note> CaseNoteList { get; set; }
    }

Our use case is to iterate over a series of appropriately named content controls in a word dcoument template using open xml sdk 2, and poke values into a newly created word documents, something like this ...

List<SdtElement> ccList = wordprocessingDocument.MainDocumentPart.Document.Descendants<SdtElement>().ToList();
foreach (var cc in ccList)
{
  string alias = cc.SdtProperties.GetFirstChild<SdtAlias>().Val.Value;
  switch (cc.GetType().Name)
  {
    case "SdtRun":
      SdtRun thisRun = (SdtRun)cc;
      //thisRun.Descendants<Text>().First().Text  = theValueToBePoked ; 
      break;
   }
}
+1  A: 

Use good old reflection. I have tested and this actually works:

    public static object GetValue(object o, string propertyName)
    {
        Type type = o.GetType();
        PropertyInfo propertyInfo = type.GetProperties(BindingFlags.Public | BindingFlags.Instance ).Where(x => x.Name == propertyName).FirstOrDefault();
        if(propertyInfo!=null)
        {
            return propertyInfo.GetValue(o, BindingFlags.Instance, null, null, null);
        }
        else
        {
            return null; // or throw exception 
        }
    }
Aliostad
+1 for suggesting reflection
ChrisBD
This code does work for single depth objects but not for composed classes. Perhaps, I did not explain my scenario quite well or you have a different use. I changed your function into a static method and tried it in the following test code.
The code is the same. You just have to parse the expression and then move down the object hierarchy which is not that difficult - definitely doable. I can give you code for "CaseNoteList[0].NoteText" or "Person.Name" if you want.
Aliostad
yep your right, I came up with something more where I move down the object hierarchy which I will post later as an answer to my own question, please comment, ... I am sure my code is not perfect ...!
+1  A: 

I'm assuming that caseConductor.SomeCleverFunction is not a static method, and has access to the Person object, and that the Person object itself if a public property.

I'm also assuming that you want to pass a string like "prop.address.street" where each sub property is an class that containts a puplic property with that name

  1. Split the string input on the period, find the left most string
  2. Use reflection to get a list of properties ( typeof(caseconductor).GetProperties() )
  3. Find the matching property, call GetValue on it, passing the last known solid object (starting with 'this') and storing a refernce to it.
  4. if there is more sub properties in the string left, repeat to step 1, removing the left most part of the string.
  5. otherwise, call GetValue() on the property, using the last GetValue() return object from step 3, and return it.

Something like:

"prop.address.street" -> find property "prop" from 'this' and GetValue,

there is still more "."'s so repeat, storing return value

"address.street" -> find property "address" from the last returned GetValue, and get it's value.

there is still more "."'s so repeat, storing return value

"street" -> find property "street" from the last returned GetValue, and return it's value.

End of string, return last value

Edit -

This is pretty rough, but toss it into LinqPAD and take a look.

http://www.linqpad.net/

Edit #2 - you should be able to index into arrays using the ^ syntax below.

Again this is reaaaaaaaaally rough, just enough to get a working example.

Edit #3 - Cleaned up the example slightly and changed it from my example classes to yours.

void Main()
{
 //- Load a caseConductor 
 var caseConductor = new CaseConductor(); 
 caseConductor.CaseID = "A00001";  
 // person  
 caseConductor.Person = new Person(); 
 caseConductor.Person.Surname = "Smith" ; 
 caseConductor.Person.DOB = DateTime.Now ;  
 // case note list 
 caseConductor.CaseNoteList = new List<Note>(); 
 caseConductor.CaseNoteList.Add(new Note { NoteText = "A-1" , NoteDt  = DateTime.Now }); 
 caseConductor.CaseNoteList.Add(new Note { NoteText = "B-2", NoteDt = DateTime.Now }); 
 // I could do this ... 
 string val1 = caseConductor.GetPropertyValue<string>("Person.Surname"); 
 // or this ... 
 Note val2 =  caseConductor.GetPropertyValue<Note>("CaseNoteList^1"); 
 val1.Dump("val1"); //this is a string
 val2.Dump("val2"); //this is a Note
}

public static class extensions
{
 public static T GetPropertyValue<T>(this object o,string Properties) where T:class
 {

  var properties = Properties.Split('.');
  var indexsplit = properties[0].Split('^');

  var current = indexsplit[0];


  var prop = (from p  in o.GetType().GetProperties() where p.Name == current select p).Take(1).Single();
  var val = prop.GetValue(o,null);

  if(indexsplit.Length>1)
  {
   var index = int.Parse(indexsplit[1]);
   IList ival = (IList)val;
   val = ival[index];
  }

  if(properties[0] == Properties)
   return (T)val;
  else
   return val.GetPropertyValue<T>(Properties.Replace(properties[0]+".",""));
 }
}


class Note 
 { 
  public Guid NoteID { get; set; } 
  public string NoteText { get; set; } 
  public DateTime? NoteDt { get; set; } 
 } 
 public class Person 
 { 
  public Guid PersonID { get; set; } 
  public string Surname { get; set; } 
  public string Forename { get; set; } 
  public DateTime? DOB { get; set; } 
 } 
 class CaseConductor 
 { 
  public String CaseID{get;set;} 
  public Person Person { get; set; } 
  public List<Note> CaseNoteList { get; set; } 
 } 
asawyer
I suppose I should mention, if your classes have a lot of properties and / or your searching very deep into a class you're likely to get fairly poor performance because of the reflection.
asawyer
OK, thanks, I will have a go at this. Wrt to poor performance and reflection, ( and the object boxing ...) ... I only need fast enough. I guess making GetPropertyValue<T>( this T object) generic will help , ... onwards and upwards ...
Edited answer to make the recursive call generic.
asawyer
fyi if you call the generic with a type that doesn't match the return type bad things will happen. Ie var a = cls.getval<int>("aboolprop"); // runtime error
asawyer
Yes, the IList ival = (IList) ; val = ival[index]; indexer thing now works in my code ; thank you.
I changed your fn sig to public static object GetPropertyValue<T>(this T o, string Properties) and it worked. The ^index selector works as well. Your fn looks good, and seems sightly less 'laboured' than mine at the moment. I am going to swap the ^ for [] code and write some nunit tests to expose edge behaviour ...
I'm glad you got it working! I enjoyed writing the proof of concept and I think I'll hang onto it in case I need something like this in the future.
asawyer
A: 

OK, I came up with something which continues Aliosted and asowyer start suggestions, here it is. You can see I still having trouble with the index access of composed objects. Thnaks for your help.

    #region object data ...
    var model = new HcmlDocumentProductionModel();
    model.CaseID = "A001";
    model.CaseConductor = new CaseConductor();
    model.CaseConductor.AField = "AField";
    model.CaseConductor.Person = new Person();
    model.CaseConductor.Person.Surname = "{Smith}";
    model.CaseConductor.Person.DOB = DateTime.Now;
    model.CaseConductor.CaseNoteList = new List<Note>();
    model.CaseConductor.CaseNoteList.Add(new Note { NoteText = "A-1", NoteDt = DateTime.Now, NoteTypeEnum = NoteTypeEnum.CaseNote });
    model.CaseConductor.CaseNoteList.Add(new Note { NoteText = "B-2", NoteDt = DateTime.Now, NoteTypeEnum = NoteTypeEnum.ReferralNote });
    model.CaseConductor.CaseNoteList.Add(new Note { NoteText = "C-3", NoteDt = DateTime.Now, NoteTypeEnum = NoteTypeEnum.StatusNote });
    model.CaseConductor.CaseNoteList.Add(new Note { NoteText = "d-3", NoteDt = DateTime.Now, NoteTypeEnum = NoteTypeEnum.CaseNote });
    model.CaseConductor.CaseNoteList.Add(new Note { NoteText = "e-3", NoteDt = DateTime.Now, NoteTypeEnum = NoteTypeEnum.StatusNote });
    model.CaseConductor.CaseNoteList.Add(new Note { NoteText = "f-3", NoteDt = DateTime.Now, NoteTypeEnum = NoteTypeEnum.CaseNote });
    #endregion

    string head = "";
    string tail = "";

    // tail
    tail = "".Tail();
    tail = "Surname".Tail();
    tail = "Person.Surname".Tail();
    tail = "CaseConductor.Person.Surname".Tail();

    // head 
    head = "".Head();
    head = "Surname".Head();
    head = "Person.Surname".Head();
    head = "CaseConductor.Person.Surname".Head();

    // ObjectDictionary
    //var person = new Person { Surname = "Smith" };
    //var d = person.ObjectDictionary();
    //object ovalue = d["Surname"]; 

    // get value special
    object o2 = model.CaseConductor.Person.ValueByKey("Surname");
    object o3 = model.CaseConductor.Person.ValueByKey("DOB");
    object o4 = model.CaseConductor.ValueByKey("Person.Surname");
    object o5 = model.ValueByKey("CaseConductor.Person.Surname");

    // get the list of ...
    object o6 = model.ValueByKey("CaseConductor.CaseNoteList");

    // get item - index thing does not work - get anull here
    string noteText = model.CaseConductor.CaseNoteList[1].NoteText;
    object o7 = model.ValueByKey("CaseConductor.CaseNoteList[1].NoteText");

namespace Zed
{
    public static class Zed
    {
        public static object ValueByKey(this object o, string key)
        {
            if (!String.IsNullOrEmpty(key))
            {
                if (!key.Contains("."))
                {
                    return (o.ObjectDictionary())[key];
                }
                else
                {
                    // key contains a dot ; therefore get object by the name of the head 
                    // and pass on that object and get propety by the tail
                    var d = o.ObjectDictionary();
                    var head = key.Head();
                    if (head.Contains("["))
                    {
                        string headMinusIndexer = head.Substring(0, head.IndexOf("["));
                        string indexString = head.Between("[", "]");
                        int index = Convert.ToInt32(indexString);
                        object oArray = d[headMinusIndexer];
                        //List<object> oList= d[headMinusIndexer]; 
                        // now get the object with the index, ... and continue
                        //object el = ((object[])oArray)[index];
                        return null;
                    }
                    else
                    {
                        var onext = d[head];
                        return onext.ValueByKey(key.Tail());
                    }

                }
            }
            return null;
        }

        public static Dictionary<string,object> ObjectDictionary(this object o)
        {
            return o.GetType().GetProperties().ToDictionary(p => p.Name, p => p.GetValue(o, null));
        }
        public static string Head(this  string key)
        {
            var head = String.Empty;
            var splittBy = '.';
            if (!String.IsNullOrEmpty(key))
            {
                var keyArray = key.Split(splittBy);
                head = keyArray[0];
            }
            //-Return
            return head;
        }
        public static string Tail(this string key)
        {
            var tail = "";
            var splittBy = '.';
            if (!String.IsNullOrEmpty(key))
            {
                var keyArray = key.Split(splittBy);
                for (int i = 1; i < keyArray.Length; i++)
                {
                    tail += (i > 1) ? "." + keyArray[i] : keyArray[i];
                }
            }
            //-Return
            return tail;
        }
        public static string Between(this string head, string start, string end)
        {
            string between = String.Empty ;
            between = head.Substring(head.IndexOf(start) + 1, head.IndexOf(end) - head.IndexOf(start) - 1);
            return between;
        }

        public static object ZGetValue( this object o, string propertyName)
        {
            Type type = o.GetType();
            PropertyInfo propertyInfo = type.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(x => x.Name == propertyName).FirstOrDefault();
            if (propertyInfo != null)
            {
                return propertyInfo.GetValue(o, BindingFlags.Instance, null, null, null);
            }
            else
            {
                return null;
            }
        }
    }

}
In my example I just assumed that if the current requested property in the recusive loop contained a ^ sign, that everything that followed would be the index number, so the item had to be an array type, and thus castable to IList, so convert the indexer into an interger and return the indexed value. True I didn't keep the "p[0]" syntax, but it's easy enough to make that work. I just choose a simpler method for a proof of concept.
asawyer
This bit: ... object oArray = d[headMinusIndexer]; .... Looks pretty close. See if tweaking it to my example will make it work.
asawyer