Tim

Footprints in the snow of a warped mind

Tag Cloud

AJAX (4) ASP (6) ASP.Net (38) Error Reporting (2) Atlas (2) Business (61) Business Start-up Advice (24) Client (8) Expanding Your Business (15) C# (8) Canoeing (4) Canoe Racing (5) Cheshire Ring Race (5) Racing (2) Training (4) CIMA (1) Cisco (1) 7970G (1) CSS (3) dasBlog (2) Design (9) Icons (1) Development (7) General (37) Christmas (6) Fun and Games (10) Internet (18) Random (42) RX-8 (8) Home Cinema (2) Hosting (1) IIS (8) iPhone (1) JavaScript (2) Marketing (3) Multipack (1) Networking (2) Nintendo (1) OS Commerce (1) Photography (1) PHP (1) PowerShell (1) Press Release (1) Security (1) SEO (5) Server Maintenance (3) Server Management (8) Software (9) Office (4) Visual Studio (6) Windows (4) Vista (1) SQL Server (12) Testing (1) The Site Doctor (86) Turnover Challenge (1) Umbraco (10) Web Development (39) WebDD (32) Wii (1)

Atom 1.0 RSS 2.0 CDF 

Search

<March 2007>
SunMonTueWedThuFriSat
25262728123
45678910
11121314151617
18192021222324
25262728293031
1234567

Recent Comments

Blog Archive

Various Links

Blogs I Read

 Google Blog
Official Google Webmaster Central Blog
 Matt Cutts
Gadgets, Google, and SEO
 Ol' Deano's Blog
My mate Dean's blog on my space, equally as random as mine but not off on as much of a tangent!
 Sam's Blog
Sam is one of my younger brothers studying Product Design and Manufacture at Loughborough, this is his blog :) Enjoy!

Recent Tracks

last.fm - The Social Music Revolution

Disclaimer
The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

newtelligence dasBlog 2.0.7226.0

Send mail to the author(s) Email Me (Tim Gaunt)

© 2008 Tim Gaunt.

Sign In

Get Windows Live Alerts

 Friday, March 09, 2007

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

#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

#region Create the example email body

string emailBody = "<p>This is a test email to {{Name}} that would be sent to the email address: {{Email}}.</p>";

#endregion

#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");

#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

#region Create the example email body

string emailBody = "<p>This is a test email to {{Name}} that would be sent to the email address: {{Email}}.</p>";

#endregion

#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

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Reflection.aspx.cs" Inherits="Reflection" %>

<!DOCTYPE html 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="Make the property search case insensitive" /></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="Merge It!" OnClick="btnSubmit_Click" /></p>
        <asp:Literal ID="litOutput" runat="server" />
    </div>
    </form>
</body>
</html>

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
    {
        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;
        }
    }

    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, falseref obj);
    }

    public bool GetPropertyValueByName(string propertyName, ref object val)
    {
        return this.GetPropertyValueByName(propertyName, falseref 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)
        {
            #region Create the example email body

            txtEmailBody.Text = "Dear {{Name}},\r\n\r\nThis is a 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>";

            #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"falseref obj))
                litOutput.Text += String.Format("<li>{0}</li>", obj);
            else
                litOutput.Text += "<li>Doesn't Exist</li>";

            if (testObject.GetPropertyValueByName("name"trueref obj))
                litOutput.Text += String.Format("<li>{0}</li>", obj);
            else
                litOutput.Text += "<li>Doesn't Exist</li>";

            if (testObject.GetPropertyValueByName("joineddate"trueref obj))
                litOutput.Text += String.Format("<li>{0}</li>", obj);
            else
                litOutput.Text += "<li>Doesn't Exist</li>";

            if (testObject.GetPropertyValueByName("nothere"trueref obj))
                litOutput.Text += String.Format("<li>{0}</li>", obj);
            else
                litOutput.Text += "<li>Doesn't Exist</li>";

            #endregion

            #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

            #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)
                {
                    //Get the property name (depending on your regex but
                    //mine groups the squigly brackets in there incase
                    //a match can't be found
                    string pName = m.Groups[2].ToString();

                    //Check it's not null
                    if (!String.IsNullOrEmpty(pName))
                    {
                        //Create an object that'll be returned from the method
                        object o = null;
                        //Check if that property exists, if it does return it
                        if (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 mistake
                    return m.Value;
                };

                //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!

Accessing properties by a string name
Useful Links:  #  digg it!  del.icio.us  Technorati  email it!  Post CommentsComments [0]  Trackback Link
CategoriesTags: ASP.Net | Web Development

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):

Example standard Atlas/AJAX error - a pretty useless error message as far as the user is concerned!

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"truetruefalse);
}

ErrorHandler.cs

using System;
using System.Data;
using System.Configur