Accessing properties by a string name
Friday, March 09, 2007 5:12:02 PM (GMT Standard Time, UTC+00:00)
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.:
//Create a dataset and add some test columns
DataTable dt =
new DataTable();

dt.Columns.Add(
"Name");

dt.Columns.Add(
"Email");
Add some test data
#region Add some test data


DataRow 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);

#endregion
Create the example email body
Do the work
#region Do the work

//Loop through the rows
for (
int i =
0; i < dt.Rows.Count; i++)
{...}

{
//Get the data row for this instance
DataRow row = dt.Rows[i];

//Create a new body as this'll be updated for each user
string body =
String.Empty;

//Update the body
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:
//Create a dataset and add some test columns
DataTable dt =
new DataTable();

dt.Columns.Add(
"Name");

dt.Columns.Add(
"Email");
Add some test data
#region Add some test data


DataRow 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);

#endregion
Create the example email body
Do the work
#region Do the work

//Loop through the rows
for (
int i =
0; i < dt.Rows.Count; i++)
{...}

{
//Get the data row for this instance
DataRow row = dt.Rows[i];


MatchEvaluator replaceField =
delegate(Match m)
{...};

{
return row[m.Groups[
1].ToString()].ToString();

};

//Create a new body as this'll be updated for each user
string body =
String.Empty;
//Find the fields
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.cs
using 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 class TestObject
{...}

{
private int __Id;
private string __Name;
private string __Email;
private DateTime __JoinedDate;

public int Id
{...}
public string Name
{...}
public string Email
{...}
public DateTime JoinedDate
{...}

public TestObject(
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 flag
if (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 access
if (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 exists
return 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

}

}

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 =
new TestObject(
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>";

#endregion
Create 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(
new TestObject(
1,
"Tim",
"tim@email.com", DateTime.Parse(
"01/02/2007")));

testObjects.Add(
new TestObject(
2,
"Jim",
"jim@email.com", DateTime.Parse(
"20/02/2007")));

testObjects.Add(
new TestObject(
3,
"John",
"john@email.com", DateTime.Parse(
"02/03/2007")));

testObjects.Add(
new TestObject(
4,
"Gill",
"gill@email.com", DateTime.Parse(
"01/04/2007")));

testObjects.Add(
new TestObject(
5,
"Bill",
"bill@email.com", DateTime.Parse(
"11/02/2007")));

#endregion
Do the work
#region Do the work

//Format it with <pre> for simplicity
litOutput.Text +=
"<h3>Now for the reflection example:</h3><hr /><pre>";

//Loop through the rows
foreach (
TestObject t
in testObjects)
{...}

{

MatchEvaluator replaceField =
delegate(Match m)
{...};

//Create a new body as this'll be updated for each user
string 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!
Reporting errors from AJAX using the WebException Class
Friday, March 09, 2007 7:57:18 AM (GMT Standard Time, UTC+00:00)
I’ve been using Phil Whinstanley’s error reporting class1 within my applications for some time now and it really does help with diagnosing issues with the site’s during development (or client testing) but also alerting me to errors on live sites. I also like it because it can highlight hacking attempts and also spambot form submissions –allowing you to alter the site as needed. A lot of the time it also means we’re alerted to an issue with the site before the client has a chance to call.
1 Note: I've been told the files Phil put online all those years ago are offline but don't panic, I'm posting another post with the relevant files shortly. If you don't want to use the search function (top right) or you're just keen, check out my comment within my post about ASP.Net WebException and Error Reporting useful code.
I’m glad he developed it because before this was around I was using a very simple email alert system that didn’t contain even a third of what this one does. Historically in ASP we always reported 500-100 errors as I don’t like clients spotting issues before I do. It’s very important to include error reporting in your code otherwise you may miss a sequence of events that causes your client to loose out on a sale.
Recently however we got in on the Atlas/AJAX scene pretty early on because we had a new application that would really benefit from a lack of postback and as it was an internal application only where we had complete control over the user’s environment, accessibility wasn’t so much of a concern (though FWIW you can still use the site in the same way without JavaScript activated).
At present, our development server’s SMTP server isn’t working properly so I didn’t think anything of receiving no email when I threw an exception during the early stages of development but as soon as I threw it onto the live server I quickly noticed that I wasn’t receiving errors from the application (we’ve got a test page to ensure the error reporting is working as expected), on investigation I found that the errors were being caught by the Atlas/AJAX handler (in a similar way to a try/catch block) which meant no emails were being sent out –so what do you do?
Note: Since I first started this article, Atlas has been released by Microsoft and is now AJAX and as part of the current release, Atlas/AJAX allows you to capture errors that are otherwise trapped by the framework and handle them as you like but for completeness I’ll overview things I tried.
Firstly I tried simply bubbling the error up to the global.asax’s Application_Error event handler as I normally would but that won’t work as it will still be trapped by the Atlas/AJAX framework, further more, the error returned to the user isn’t very useful (it’s the text within the exception):

The next thing I tried was taking the exception and passing it to the WebException as you do within the Application_Error event handler, although this worked and for this project would have been an alright solution because the ScriptManager was contained within a single MasterPage, I wanted a solution that I could easily roll out to other projects.
What I decided to do in the end was to wrap the WebException class and adding a single static method that takes an exception, then I replaced the code within the Global.asax and within the ScriptManager’s error event handler and responded to the user with a more informative message. The code below will output a user friendly message -still in a popup though you could redirect if desired. In the live application the user's location and a reference for the incoming error email is also shown to the user.
Note: TSDGlobals is a settings class we use here, it just references the relevant setting and contains a set of useful methods that we use throughout most of our projects.
aspx code
<asp:ScriptManager runat="server" ID="sm" EnablePartialRendering="true" AllowCustomErrorsRedirect="true" OnAsyncPostBackError="atlasScriptManager_PageError"></asp:ScriptManager>
codebehind
protected void atlasScriptManager_PageError(
object sender, AsyncPostBackErrorEventArgs e)
{...}

{
//A page reference for you (optional but useful)
string __PageRef
= "132";
//Update the message the user will see

sm
.AsyncPostBackErrorMessage
= String
.Format(
"I'm sorry, an error has occured, please contact us on 01234 567890. Quoting Page Ref: {0} - {1}", __PageRef, DateTime
.Now
.ToString());
//Pass it through to the new Error Handler

ErrorHandling
.ErrorHandler
.Handle(e
.Exception);

}
global.asax
void Application_Error(
object sender,
EventArgs e)
{...}
{

ErrorHandling.
ErrorHandler.Handle(Server.GetLastError());

}
protected void Application_PreRequestHandlerExecute(
Object sender,
EventArgs e)
{...}

{
if (Context.Handler
is IRequiresSessionState || Context.Handler
is IReadOnlySessionState)

ErrorReporting.
SessionTracker.AddRequest(
"Pre Request Handler Execute",
true,
true,
false);

}
ErrorHandler.cs
using System;
using System.Data;
using System.Configur