Accessing properties by a string name

This morning Julian Voelcker came to me with an interesting issue that I’ve looked into before but I’ve never really looked into a re-useable solution. Seeing as it’s fun Friday I thought why not ;)

The scenario: I would like to offer my users a custom mail merge facility where by they can insert values stored in the database such as their name. The selection of columns is unlikely to be changed and if it does then I’ll be the one to do it. There are about 20 fields to choose from.

Easy enough, in the past I’ve kept it to a minimum and then just done a simple find and replace on the body i.e.:

//CreateadatasetandaddsometestcolumnsDataTable dt = new DataTable();dt.Columns.Add("Name");dt.Columns.Add("Email");Add some test data#region Add some test dataDataRow dr = dt.NewRow();dr["Name"] = "Julian";dr["Email"] = "julian@email.com";dt.Rows.Add(dr);dr = dt.NewRow();dr["Name"] = "Tim";dr["Email"] = "tim@email.com";dt.Rows.Add(dr);#endregionCreate the example email body#region Create the example email bodystring emailBody = "<p>Thisisatestemailto{{Name}}thatwouldbesenttotheemailaddress:{{Email}}.</p>";#endregionDo the work#region Do the work//Loopthroughtherowsfor (int i = 0; i < dt.Rows.Count; i++){...}{//Getthedatarowforthisinstance    DataRow row = dt.Rows[i];//Createanewbodyasthis'llbeupdatedforeachuserstring body = String.Empty;//Updatethebody    body = emailBody.Replace("##Name##", row["Name"]);    body = body.Replace("##Email##", row["Email"]);    litOutput.Text += String.Format("{0}<hr/>", body);}#endregion

The issue I see with this however is (among others) having 20 fields is a lot to be doing with a find/replace statement as it wouldn’t be very elegant and a nightmare to manage. Sticking with this method of using a dataset I suggested we use a regular expression to match the field delimiters and do a replace that way:

//CreateadatasetandaddsometestcolumnsDataTable dt = new DataTable();dt.Columns.Add("Name");dt.Columns.Add("Email");Add some test data#region Add some test dataDataRow dr = dt.NewRow();dr["Name"] = "Julian";dr["Email"] = "julian@email.com";dt.Rows.Add(dr);dr = dt.NewRow();dr["Name"] = "Tim";dr["Email"] = "tim@email.com";dt.Rows.Add(dr);#endregionCreate the example email body#region Create the example email bodystring emailBody = "<p>Thisisatestemailto{{Name}}thatwouldbesenttotheemailaddress:{{Email}}.</p>";#endregionDo the work#region Do the work//Loopthroughtherowsfor (int i = 0; i < dt.Rows.Count; i++){...}{//Getthedatarowforthisinstance    DataRow row = dt.Rows[i];    MatchEvaluator replaceField = delegate(Match m){...};    {return row[m.Groups[1].ToString()].ToString();    };//Createanewbodyasthis'llbeupdatedforeachuserstring body = String.Empty;//Findthefields    Regex r = new Regex(@"{{(\w{0,15}?)}}");    body = r.Replace(emailBody, replaceField);    litOutput.Text += String.Format("{0}<hr/>", body);}#endregion

This is alright and in many ways very scaleable. I’m not a fan of DataSets but in this instance it works nicely and does mean expanding the available fields at a later date would just be a matter of adding columns to the query.

How does this relate to accessing a property of an object using a string value instead? Well there was a catch, Julian wasn’t using a DataSet and didn’t want to, he had a collection of custom objects all ready and waiting. As he uses a code generator to generate his Data Access Layer and Business Logic Layer there was a method already exposed allowing you to search for a property by string but it's not a standard .Net method so I decided to work out how it was done.

The solution it turned out was a really rather elegant solution IMHO. Using reflection you can use the same concept as above but with custom objects and Robert is your father’s wife’s sister:

Reflection.aspx<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Reflection.aspx.cs" Inherits="Reflection" %><!DOCTYPEhtml PUBLIC"-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head runat="server"><title>Untitled Page</title></head><body><form id="form1" runat="server"><div><h1>Reflection Demo</h1><p>Choose from the following fields to build up your email message, the valid fields are (you can choose whether to use non-valid fields as a test if you like):</p><ul><li>Id</li><li>Email</li><li>Name</li><li>JoinedDate</li></ul><p><asp:CheckBox ID="chkCaseSensitive" runat="server" Text="Makethepropertysearchcaseinsensitive"/></p><p><label for="txtEmailBody">Example email body:</label><br/><asp:TextBox runat="server" ID="txtEmailBody" TextMode="MultiLine" style="width:500px;height:200px;"/></p><p><small>HTML submissions are not allowed and they're encoded anyways so no point in spamming -not that you were going to of course!</small></p><p><asp:Button runat="server" ID="btnSubmit" Text="MergeIt!" OnClick="btnSubmit_Click"/></p><asp:Literal ID="litOutput" runat="server"/></div></form></body></html>

Reflection.aspx.csusing System;using System.Data;using System.Configuration;using System.Collections;using System.Web;using System.Web.Security;using System.Web.UI;using System.Web.UI.WebControls;using System.Web.UI.WebControls.WebParts;using System.Web.UI.HtmlControls;using System.Text.RegularExpressions;using System.Collections.Generic;using System.Reflection;public classTestObject{...}{private int __Id;private string __Name;private string __Email;private DateTime __JoinedDate;public int Id{...}    {get{...}        {return __Id;        }set{...}        {            __Id = value;        }    }public string Name{...}    {get{...}        {return __Name;        }set{...}        {            __Name = value;        }    }public string Email{...}    {get{...}        {return __Email;        }set{...}        {            __Email = value;        }    }public DateTime JoinedDate{...}    {get{...}        {return __JoinedDate;        }set{...}        {            __JoinedDate = value;        }    }publicTestObject(int id, string name, string email, DateTime joinedDate){...}    {        __Id = id;        __Name = name;        __Email = email;        __JoinedDate = joinedDate;    }public bool GetPropertyValueByName(string propertyName){...}    {object obj = null;return this.GetPropertyValueByName(propertyName, false, ref obj);    }public bool GetPropertyValueByName(string propertyName, ref object val){...}    {return this.GetPropertyValueByName(propertyName, false, ref val);    }public bool GetPropertyValueByName(string propertyName, bool caseInsensitive, ref object val){...}    {PropertyInfo p = null;BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;//If it's a case-insensitive search then add the flagif (caseInsensitive)            flags = flags | BindingFlags.IgnoreCase;        p = this.GetType().GetProperty(               propertyName,               flags,null,null,               Type.EmptyTypes,null);//Check the property exists and that it has read accessif (p != null && p.CanRead){...}        {//There is a property that matches the name, we can read it so get it            val = this.GetType().InvokeMember(                propertyName,BindingFlags.GetProperty | flags,null,this,null);//We return true as the user may just want to check that it existsreturn true;        }return false;    }}public partial class Reflection : System.Web.UI.Page{...}{protected void Page_Load(object sender, EventArgs e){...}    {if (!Page.IsPostBack){...}        {Create the example email body#region Create the example email body            txtEmailBody.Text = "Dear {{Name}},\r\n\r\nThis isa test email that would be sent to the email address: {{Email}}.\r\n\r\n{{Name}} joined on: {{JoinedDate}}. This field should not be found {{Don't Find Me}}\r\n\r\nRegards,\r\n\r\nThe webmaster.";#endregion        }    }protected void btnSubmit_Click(object sender, EventArgs e){...}    {if (Page.IsValid && !String.IsNullOrEmpty(txtEmailBody.Text)){...}        {            litOutput.Text = "<h2>Output</h2>";Perform some basic tests#region Perform some basic tests            litOutput.Text += "<h3>Perform some basic tests:</h3>";TestObject testObject = newTestObject(1, "Tim", "tim@email.com", DateTime.Today);object obj = null;if (testObject.GetPropertyValueByName("id", false, ref obj))                litOutput.Text += String.Format("<li>{0}</li>", obj);else                litOutput.Text += "<li>Doesn't Exist</li>";if (testObject.GetPropertyValueByName("name", true, ref obj))                litOutput.Text += String.Format("<li>{0}</li>", obj);else                litOutput.Text += "<li>Doesn't Exist</li>";if (testObject.GetPropertyValueByName("joineddate", true, ref obj))                litOutput.Text += String.Format("<li>{0}</li>", obj);else                litOutput.Text += "<li>Doesn't Exist</li>";if (testObject.GetPropertyValueByName("nothere", true, ref obj))                litOutput.Text += String.Format("<li>{0}</li>", obj);else                litOutput.Text += "<li>Doesn't Exist</li>";#endregionCreate a collection and add a couple of items#region Create a collection and add a couple of items            List<TestObject> testObjects = new List<TestObject>();            testObjects.Add(newTestObject(1, "Tim", "tim@email.com", DateTime.Parse("01/02/2007")));            testObjects.Add(newTestObject(2, "Jim", "jim@email.com", DateTime.Parse("20/02/2007")));            testObjects.Add(newTestObject(3, "John", "john@email.com", DateTime.Parse("02/03/2007")));            testObjects.Add(newTestObject(4, "Gill", "gill@email.com", DateTime.Parse("01/04/2007")));            testObjects.Add(newTestObject(5, "Bill", "bill@email.com", DateTime.Parse("11/02/2007")));#endregionDo the work#region Do the work//Format it with <pre> for simplicity            litOutput.Text += "<h3>Now for the reflectionexample:</h3><hr /><pre>";//Loop through the rowsforeach (TestObject t in testObjects){...}            {                MatchEvaluator replaceField = delegate(Match m){...};                {//Get the property name (depending on your regex but//mine groups the squigly brackets in there incase//a match can't be foundstring pName = m.Groups[2].ToString();//Check it's not nullif (!String.IsNullOrEmpty(pName)){...}                    {//Create an object that'll be returned from the methodobject o = null;//Check if that property exists, if it does return itif (t.GetPropertyValueByName(pName, chkCaseSensitive.Checked, ref o))return o.ToString();                    }//We've not found a match for the property in the object//so return the match instead as it's probably a mistakereturn m.Value;                };//Create a new body as this'll be updated for each userstring body = String.Empty;//Find the fields within the main body -this can be any of the properties of the object                Regex r = new Regex(@"({{)(\w{0,15}?)(}})");                body = r.Replace(txtEmailBody.Text, replaceField);//Output the example content (HtmlEncoded so not to hurt us!!)                litOutput.Text += String.Format("{0}<hr />", Server.HtmlEncode(body));            }            litOutput.Text += "</pre>";#endregion        }    }}

I’ve thrown up a quick demo if you want to test it out. I think in the longer run I’m going to look into having it generate some form of reporting system as that’d be seriously nice, but the suns out and I need to go for a paddle so that’ll have to wait for another day! So that's my first delve into reflection and so far I love it!

Subscribe to TSD

Don’t miss out on the latest posts. Sign up now to get access to the library of members-only posts.
jamie@example.com
Subscribe