# Friday, March 09, 2007

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!

Friday, March 09, 2007 5:12:02 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 

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.Configuration;
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;

namespace ErrorHandling
{
    public class ErrorHandler
    {
        //Declare for the scope of the class
        private static HttpRequest context = HttpContext.Current.Request;

        public static void Handle(Exception currentError)
        {
            Handle(currentError, true);
        }

        public static void Handle(Exception currentError, bool redirectUser)
        {
            if (TSDGlobals.SendSiteErrors)
            {
                #region Deal with 404's

                //Redirect the user to a friendly page
                if (CheckForErrorType(currentError, "FileNotFound") && redirectUser)
                    RedirectToFriendlyUrl(TSDGlobals.ErrorPage_PageNotFound);

                #endregion
                #region Deal with Spambots

                //Check the error type
                if (CheckForErrorType(currentError, "System.FormatException"))
                {
                    if (context.Form.Count > 0)
                    {
                        foreach (string key in context.Form)
                        {
                            if (key.IndexOf("_VIEWSTATE") > 0 && context.Form[key].ToString().IndexOf("Content-Type") > 0)
                                return;
                        }
                    }
                }

                #endregion

                //Enable the trace for the duration of the error handling
                TraceContext t = HttpContext.Current.Trace;
                bool bCurrentState = t.IsEnabled;
                t.IsEnabled = true;

                #region Handle the Exception

                WebException WE = new WebException();
                WE.CurrentException = currentError;
                WE.Site = context.Url.Host.ToString();
                //Pull the information from the web.config here if desired
                WE.FloodCount = 50;
                WE.FloodMins = 5;

                #endregion
                #region Choose what you're interested in

                WE.ReturnCache = true;
                WE.DrillDownInCache = true;
                WE.IncludeApplication = true;
                WE.IncludeBrowser = true;
                WE.IncludeEnvironmentVariables = true;
                WE.IncludeForm = true;
                WE.IncludeProcess = true;
                WE.IncludeQueryString = true;
                WE.IncludeRequestCookies = true;
                WE.IncludeRequestHeader = true;
                WE.IncludeResponseCookies = true;
                WE.IncludeServerVariables = true;
                WE.IncludeSession = true;
                WE.IncludeTrace = true;
                WE.IncludeVersions = true;
                WE.IncludeAuthentication = true;

                #endregion

                WE.Handle();

                //Return the trace to its original state
                t.IsEnabled = bCurrentState;

                //Redirect the user to a friendly page
                if (redirectUser)
                    RedirectToFriendlyUrl(TSDGlobals.ErrorPage_CodeIssue);
            }
        }

        private static bool CheckForErrorType(Exception ex, string errorText)
        {
            if (ex != null)
            {
                //Check the exception
                if (ex.GetType().ToString().IndexOf(errorText) > 0)
                    return true;
                else
                    return CheckForErrorType(ex.InnerException, errorText);
            }
            else
            {
                return false;
            }
        }

        private static void RedirectToFriendlyUrl(string Url)
        {
            //Only redirect the user if the URL is not empty and we're not on a dev machine
            //TODO: Check the referrer to ensure we don't redirect the user to the page causing the error!
            //TODO: Pull the list of development server addresses from an XML file
            if (!String.IsNullOrEmpty(Url) && (context.Url.Host.IndexOf("localhost") < 0))
                HttpContext.Current.Response.Redirect(Url);
        }
    }
}

I’m not sure if this is a recommended way of doing it but it works pretty well and in my case, the majority of settings from the code are the same regardless of the project but you can still alter those if required –as they’re not likely to change project-project I’ve kept the settings within the web.config. I decided to wrap Phil’s code in my own because that way if he ever releases an update (not sure what that’d do tbh) I could just drop the new WebException code into my project and be ready to go straight away.

What do you think Phil? Use or Abuse of your code ;)

Friday, March 09, 2007 7:57:18 AM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 
# Wednesday, March 07, 2007
The BlackBerry Pearl -the next generation in BlackBerrys

I’ve always been adverse to getting a BlackBerry for a few of reasons:

  1. I don’t get away from work much as it is so I felt having emails on my BlackBerry on the go all the time would be one step too far.
  2. The size of the BlackBerry –they’re ridiculous, I like my phone to be as small as possible so it interferes with my life as little as possible.
  3. I’ve heard horror stories about the increase in bandwidths to manage the email push.

Then while we were looking at new contracts over Christmas Stacey picked up the BlackBerry Pearl which was small and sleek and suggested I gave it a go. As she didn’t want to change from her V3 we agreed I’d give it a go for a month and if I didn’t like it I’d have her free upgrade (a Sony Ericsson w810i).

I’ve had the BlackBerry Pearl for well over a month now so I thought I’d share my findings in case anyone else is thinking of getting one.

How does the BlackBerry “work”?

There are plenty of tutorials for the BlackBerry and far more advanced information on how the BlackBerry actually works but I thought it may be worth over viewing how it’s configured and the basic concepts.

BlackBerrys use something called “Push” Technology to retrieve emails, your email client (Outlook, Eudora, Thunderbird etc) classically collects email from a server by contacting the server and getting a list of emails and then downloading the emails. BlackBerrys on the other hand have the email sent to it from the server.

When you first get your BlackBerry you have to configure your email accounts with your BlackBerry’s ISP (in my case O2), you supply them with server details for your mail account (POP3 settings). Your ISP will then periodically check your email account and collect the new email messages which are then encrypted and sent (or pushed) to your BlackBerry (in a similar way to an SMS message). The BlackBerry then decrypts the email and allows you to read it. Simple eh :)

My thoughts on the BlackBerry Pearl

Things I like about the BlackBerry Pearl:

  • It’s size –it’s no larger than a lot of the other phones out there at the moment, in fact it’s smaller than my old Motorola V3 while it was closed (though it’s about 5mm longer)
  • It’s synchronisation with Outlook. I used to have a Palm LifeDrive to manage my calander, address book and tasks etc and a separate address book on my V3 which didn’t sync correctly with Outlook so the three were nearly always different –that and I hated having to carry around the LifeDrive as it was just extra bulk... The BlackBerry Pearl however seamlessly manages it all which has meant that I’ve started managing my to-do list a lot more efficiently as well as my address book.
  • The size and clarity of the screen.
  • Today Plus theme –an extra download but well worth it as it summarises all the info you need to know at a glance on the home screen.
  • Battery life –quoted at around 8½ hours talk time I’m certainly getting at least this. A lot of the time I plug it in via USB while I’m working which charges it but I’ve had a couple of weekends away recently which has meant it’s not got charged and it happily coped with the (for me) heavy use without an issue.
  • The complete call log of all incoming and out going calls on a user basis –this is great for me if I need to know how long I spent speaking to a client etc (I tend to use my mobile for most outgoing calls as they’re all free ;)).
  • The degree of customisation (though this is also a downside as it is somewhat complicated).
  • The fact you can customise what the side buttons do.
  • The standby button –why it has a keylock I don’t know as it gets in the way of the standby button.
  • The voice dialler –that’s awesome.
  • VoiceRecorder+ (a voice memo recorder for the BlackBerry Pearl from ShapeServiceswww.shapeservices.com)
  • The fact you can add delays into the number dialling to quickly and automatically navigate the IVR options for systems you regularly use.

Things I dislike about the BlackBerry Pearl:

  • It’s pretty complicated. Unlike most phones these days each application has it’s own settings and finding where they are is sometimes a real PITA.
  • You can’t set ringing profiles to activate at certain times of the day.
  • You can’t send SMS messages from it through your computer.
  • It doesn’t ring and vibrate at the same time.
  • The ringer is a little quiet at times (though people suggest drilling holes in the back of the casing sorts this).
  • The key lock which sometimes gets in the way of taking the phone out of standby –I expect there’s a way of turning it off but I’ve not found it yet.

All in all I think the BlackBerry Pearl is a superb phone and I’ll certainly be keeping it. I’ve setup a filter to ensure I don’t get spam coming through on it but I still get between 10 and 50 emails a day on it, the data transfer is still low (it’s still under 100KB) but I don’t surf the web etc.

O2 offer a deal at the moment where you can have unlimited data for £10pm on top of your bill, my thinking is if I start to use more than £10 of data a month I’ll upgrade but atm it’s all good. In regards the additional load on our mail server I’ve not noticed anything significant but I’ll analyse this in a couple of months as the calls should be clear. To avoid spam emails I’ve setup a separate mail account that the O2 server collects from, then from my main email accounts I forward any that are sent directly to me and without my spam filter’s headers added to my GMail account which then filters pretty much everything else missed by my server’s filter before forwarding it onto my BlackBerry account. I realise this is a slightly long winded method of managing it but it has meant that 99.9% of all spam has been ignored. There have been a couple of emails missed (i.e. where I’ve been CC’d) but I can live without having those on the go ;)

I had an issue when I first got the BlackBerry Pearl that all numbers would be dialled with the prefix of +44 and the leading 0. As it happens, the fix is pretty simple:

  1. Open the call log
  2. Press the menu key (the BlackBerry icon)
  3. Choose “General Options”
  4. Choose “Smart Dialing”
  5. Change the country code to +44
Wednesday, March 07, 2007 11:30:48 AM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 
# Monday, March 05, 2007

** WARNING ** this post is most definitely a rant
A couple of weeks ago Stacey and I were hit with some really nasty flu like bug (not man flu :P), I don’t think I’ve ever had flu before so I’m not sure if it was flu but this was nasty, I was (among other things) hallucinating the duvet wanted me to join a cult but that’s a whole other story!

I like talking to people and running my own business I tend to have my phone on me wherever I go, very infrequently do I not answer within a couple of rings. A couple of days before I was hit with this bug, I said I would get back to one of the guys that passes us a little work every now and again about a potential client he had.

Being ill however (and I mean bed-ridden) I didn’t manage it so he gave my mobile a call –great, only it was in the other room and I really couldn’t be bothered to get up and get it so I left it. A couple of minutes later I get an SMS through (which turned out to be from my answer phone) just before the house phone started ringing. As it was within arms length I answered to a very grumpy git bitching on about how I was ignoring him and why didn’t I answer my mobile –was it because I didn’t have caller id on that line etc.

As it turns out, we do have caller id on all phones and I did know it was him and it wasn’t that I was avoiding him, it was merely that I was too ill to answer. FWIW I never ignore people when they call, if it’s you and you’re after something and I don’t want to talk to you I’ll just tell you so, I don’t have the time to arse around playing games so grow up!

Monday, March 05, 2007 10:02:29 AM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 
# Wednesday, February 28, 2007

Ok, as sad as it sounds I've been looking forward to the latest SQL Server service pack since I heard about some of the issues it fixed as there was two "glitches" in particular that seriously bugged me, namely:

  • Loading a SQL file into the query editor on a live connection would ask you to log in again -this bugged the hell out of me because I have a number of routines saved on my disk as SQL files that manage client's servers and so I don't always have the password to easily hand which would just delay development (ok all be it by 30seconds or so but that's not the point)
  • It would never remember my password in the initial splash screen, again see why above.

Anyway, it's great to see that these two points were fixed as well as a load of other issues but I couldn't help but chuckle when I saw the new context menus, I don't know about you but when editing this table I didn't know which one I needed to choose:

Confusing SQL Server Context Menu

FWIW you need "Design" to open the design view and "Edit" to generate a SQL CREATE Script. Genius!

Wednesday, February 28, 2007 6:35:40 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 
# Tuesday, February 27, 2007

Stacey recently wrote an excellent brief on the pro's and con's of employing someone however this morning when I came to post it online I realised that it was deleted when I formatted the laptop for a system demonstration last week. Luckily however I have a hard copy so I'll have it re-typed and put online ASAP.

In the meantime however I shall reflect with this classic (which I’m looking to implement in The Site Doctor ASAP)…

Attire

It is advised that you come to work dressed according to your salary. If we see you wearing Prada sneakers and carrying a Gucci bag, we assume you are doing well financially and therefore you do not need a raise.

If you dress poorly, you need to learn to manage your money better, and therefore you do not need a raise. If you dress in-between, you are right where you need to be and therefore you do not need a raise.

Personal Days

Each employee will receive 104 personal days a year. They are called Saturday and Sunday.

Lunch Break

Skinny people get 30 minutes for lunch as they need to eat more so that they can look healthy.

Normal size people get 15 minutes for lunch to g et a balanced meal to maintain their average figure.

Sick Days

We will no longer accept a doctor statement as proof of sickness. If you are able to go to the doctor, you are able to come to work.

Restroom Use

Entirely too much time is being spent in the restroom. There is now a strict 3-minute time limit in the stalls. At the end of three minutes, an alarm will sound, the toilet paper roll will retract, the stall door will open and a picture will be taken. After your second offence, your picture will be posted on the company bulletin board under the "Chronic Offenders" category.

Surgery

As long as you are an employee here, you need all your organs. You should not consider removing anything. We hired you intact. To have something removed constitutes a breach of employment.

Thank you for your loyalty to our company. We are here to provide a positive employment experience. Therefore, all questions, comments, concerns, complaints, frustrations, irritations, aggravations, insinuations, allegations, accusations, contemplation, consternation and input should be directed to the State Unemployment Offices.

Tuesday, February 27, 2007 9:22:48 AM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 
# Saturday, February 24, 2007

As part of my blog’s re-design I wanted to integrate my statistics from Last.FM which monitors what music you’re listening to and generates a stack of statistics about your listening habit (see About Last FM for more information).

Anyways, I started writing my own RSS macro when I came across one already developed by John Forsythe (http://www.jforsythe.com/) which did pretty much exactly what I was planning on developing, the only difference though was that his was hard-coded to preset node names whereas I was planning on using an XSL file to format mine to offer maximum flexibility in the long run so I updated his with the use of reflector (thanks to John Forsythe though!!).

There are a couple of difference to note with this code and John Forsythe's:

  • The RSS retrieval is no longer handled by an external library -in this instance I wanted to keep this as simple and stand-alone as possible.
  • There is no max item count at present -this is mainly because I didn't need it for the Last.FM Feed, I may alter that later.

Source code for a dasBlog XSL based RSS reader

using System;
using System.IO;
using System.Security.Cryptography;
using System.Diagnostics;
using System.Text;
using System.Web;
using System.Web.UI;

using newtelligence.DasBlog.Runtime;
using newtelligence.DasBlog.Web.Core;

namespace TSDMacros
{
    public class TheSiteDoctor
    {
        protected SharedBasePage requestPage;
        protected Entry currentEntry;

        public TheSiteDoctor(SharedBasePage page, Entry entry)
        {
            requestPage = page;
            currentEntry = entry;
        }

        /// <summary>
        /// A dasBlog macro to retrieve an RSS feed and apply XSL to 
        /// it before caching it for x minutes
        /// </summary>
        /// <param name="xslVPath">The virtual path of the XSL file</param>
        /// <param name="rssPath">The RSS feed URL</param>
        /// <param name="minutesToCache">Number of minutes to cache the file for</param>
        /// <param name="debugMode">Output the debug information</param>
        /// <returns>A control that can be inserted into a dasBlog template</returns>
        public virtual Control GetRSS(string xslVPath, string rssPath, int minutesToCache, bool debugMode)
        {
            string cacheVDir = "./content/getrsscache/";
            string cachedFileLoc = String.Empty;
            StringBuilder output = new StringBuilder();

            bool writeToCache = false;
            bool cacheExpired = false;
            bool cacheExists = false;

            #region Debug output
            if (debugMode)
            {
                output.Append("<strong>&lt;start debug&gt;</strong><hr />\r\n");
                output.AppendFormat("<i>RssPath</i>: {0}<br />\r\n", rssPath);
                output.AppendFormat("<i>minutesToCache</i>: {0}<br />\r\n", minutesToCache);
                output.AppendFormat("<i>CacheStorageFolder</i>: {0}<br />\r\n", cacheVDir);
                output.Append("<hr />\r\n");
            }
            #endregion

            #region Check whether we need to cache or not
            if (minutesToCache > 0)
            {
                writeToCache = true;
                //Find the cache directory
                string cacheDir = HttpContext.Current.Server.MapPath(cacheVDir);
                //Work out what the file would be called based on the RSS URL
                cachedFileLoc = Path.Combine(cacheDir, HttpUtility.UrlEncode(TheSiteDoctor.GetMd5Sum(rssPath)) + ".cache");
                #region Debug output
                if (debugMode)
                {
                    output.AppendFormat("<i>cache file</i>: {0}\r\n", cachedFileLoc);
                }
                #endregion
                if (!File.Exists(cachedFileLoc))
                {
                    cacheExpired = true;
                    #region Debug output
                    if (debugMode)
                    {
                        output.Append("<i>cache age</i>: no file exists<br />");
                    }
                    #endregion
                }
                else
                {
                    FileInfo info1 = new FileInfo(cachedFileLoc);
                    TimeSpan span1 = (TimeSpan)(DateTime.Now - info1.LastWriteTime);
                    if (span1.TotalMinutes > minutesToCache)
                    {
                        cacheExists = true;
                        cacheExpired = true;
                    }
                    #region Debug output
                    if (debugMode)
                    {
                        output.AppendFormat("<i>cache age</i>: : {0} min old <br />\r\n", span1.TotalMinutes);
                    }
                    #endregion
                }
            }
            else
            {
                #region Debug output
                if (debugMode)
                {
                    output.Append("<strong>caching disabled - CacheStorageAgeLimit=0</strong><br /><span style=\"color:red; font-weight: bold;\">FYI: All requests to this page will cause a new server request to the RssPath</span><br />");
                }
                #endregion
                cacheExpired = true;
            }

            #endregion

            #region Debug output
            if (debugMode)
            {
                output.Append("<hr />");
            }
            #endregion
            //Check whether or not the cache has expired
            if (cacheExpired)
            {
                #region Debug output
                if (cacheExists & debugMode)
                {
                    output.Append("<strong>file cache is expired, getting a new copy right now</strong><br />");
                }
                else if (debugMode)
                {
                    output.Append("<strong>no cache, getting file</strong><br />");
                }
                #endregion
                //The cache has expired so retrieve a new copy
                output.Append(TheSiteDoctor.delegateRss(xslVPath, rssPath, 0, writeToCache, cachedFileLoc, debugMode));
            }
            else
            {
                #region Debug output
                if (debugMode)
                {
                    output.Append("<strong>cool, we got the file from cache</strong><br />");
                }
                #endregion
                //The cache still exists and is valid
                StreamReader reader1 = File.OpenText(cachedFileLoc);
                output.Append(reader1.ReadToEnd());
                reader1.Close();
            }
            #region Debug output
            if (debugMode)
            {
                output.Append("<hr /><strong>&lt;end debug&gt;</strong>");
            }
            #endregion

            output.Append("\r\n<!-- \r\ndasBlog RSS feed produced using the macro from Tim Gaunt\r\nhttp://blogs.thesitedoctor.co.uk/tim/\r\n-->");

            return new LiteralControl(output.ToString());
        }

        /// <summary>
        /// RSS feed retrieval worker method. Retrieves the RSS feed 
        /// and applies the specified XSL document to it before caching 
        /// a copy to the disk -this should be called after it has been 
        /// established the cache is out of date.
        /// </summary>
        /// <param name="xslVPath">The virtual path of the XSL file</param>
        /// <param name="rssPath">The RSS feed URL</param>
        /// <param name="timeoutSeconds">Number of seconds before the request should timeout</param>
        /// <param name="writeCache">Whether to cache a copy on disk</param>
        /// <param name="xmlPath">Physical path of the XML file on the disk</param>
        /// <param name="debugMode">Output the debug information</param>
        /// <returns>An XML document as a string</returns>
        private static string delegateRss(string xslVPath, string rssPath, int timeoutSeconds, bool writeCache, string xmlPath, bool debugMode)
        {
            StringBuilder output = new StringBuilder();
            bool errorThrown = false;
            string cacheVDir = "./content/getrsscache/";
            string xslPath = HttpContext.Current.Server.MapPath(xslVPath);

            try
            {
                //TODO: Replace this with a HttpRequest and timeout to ensure the visitor is not left waiting for the file to load
                //Load the XML
                System.Xml.XmlDocument xmlDoc = new System.Xml.XmlDocument();
                xmlDoc.Load(rssPath);

                //Load the XSL
                System.Xml.Xsl.XslTransform xslDoc = new System.Xml.Xsl.XslTransform();
                xslDoc.Load(xslPath);
                
                StringBuilder sb = new StringBuilder();
                StringWriter sw = new StringWriter(sb);

                //Apply the XSL to the XML document
                xslDoc.Transform(xmlDoc, null, sw);

                //Append the resulting code to the output file
                output.Append(sb.ToString());
            }
            catch (Exception ex)
            {
                errorThrown = true;
                #region Debug output
                if (debugMode)
                {
                    //Log the exception to the dasBlog exception handler
                    ErrorTrace.Trace(TraceLevel.Error, ex);
                    output.AppendFormat("<ul style=\"\"><li><strong>RSS request failed :(</strong> <br />{0}</li></ul>", ex.ToString());
                }
                #endregion
            }

            //Save a cache of the returned RSS feed if no errors occured
            if (writeCache & !errorThrown)
            {
                //Find the cache's storage directory
                DirectoryInfo dir = new DirectoryInfo(HttpContext.Current.Server.MapPath(cacheVDir));
                //Check it exists
                if (!dir.Exists)
                {
                    dir.Create();
                    #region Debug output
                    if (debugMode)
                    {
                        output.AppendFormat("<strong>just created the directory:</strong> {0}<br />"HttpContext.Current.Server.MapPath(cacheVDir));
                    }
                    #endregion
                }
                //Create the file
                StreamWriter writer1 = File.CreateText(xmlPath);
                writer1.Write(output);
                writer1.Flush();
                writer1.Close();
                #region Debug output
                if (debugMode)
                {
                    output.Append("<strong>just wrote the new cache file</strong><br />");
                }
                #endregion
            }

            return output.ToString();
        }

        /// <summary>
        /// Worker method to identify the MD5 checksum of a string
        /// in this instance used to ensure the RSS file isn't already
        /// cached (based on the URL supplied)
        /// </summary>
        /// <param name="str"></param>
        /// <returns></returns>
        public static string GetMd5Sum(string str)
        {
            Encoder encoder1 = Encoding.Unicode.GetEncoder();
            byte[] buffer1 = new byte[str.Length * 2];
            encoder1.GetBytes(str.ToCharArray(), 0, str.Length, buffer1, 0true);
            byte[] buffer2 = new MD5CryptoServiceProvider().ComputeHash(buffer1);
            StringBuilder builder1 = new StringBuilder();
            for (int minsToCache = 0; minsToCache < buffer2.Length; minsToCache++)
            {
                builder1.Append(buffer2[minsToCache].ToString("X2"));
            }
            return builder1.ToString();
        }

    }
}

XSL that I use for Last.FM

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="html" /> 
  <xsl:template match="/">
    <h2>Recent Tracks</h2>
    <ul>
    <xsl:for-each select="recenttracks/track">
        <li>
            <a href="{url}">
                <xsl:value-of select="artist" /> - <em><xsl:value-of disable-output-escaping="yes" select="name" /></em>
            </a>
        </li>
    </xsl:for-each>
    </ul>
    <p><a href="About-Last-FM.aspx" title="last.fm - The Social Music Revolution"><img alt="last.fm - The Social Music Revolution" src="images/lastfm_mini_black.gif" /></a></p>
  </xsl:template>
</xsl:stylesheet>

To use it on the blog template

<% GetRSS("LastFM.xsl", "http://ws.audioscrobbler.com/1.0/user/timgaunt/recenttracks.xml", 25, false)|tsd %>

This is a pretty crude way of doing it IMHO because the XSL transforms the stream directly, eventually I’ll update the code so it includes a timeout (as John’s did) and having seen the performance implications on my blog, make sure the request is made asynchronously.

FWIW I have set my cache value to 25minutes, I did have it as 1min for fun but it killed the blog, why have I set it to 25mins? Well, most of my tracks I would think are 2-3minutes long, as I list 10 tracks at a time that’s 20-30minutes listening time so it’ll still keep a fairly accurate overview of my tracks without having massive performance issues on my blog :)

Incase you don't want to or know how to create this macro as a DLL I have created it for you :)

dasBlog RSS feed macro" onclick="javascript:urchinTracker('/download/zip/TSDMacros_v1_23-02-07');" href="/tim/files/TSDMacros_v1_23-02-07.zip">Download the complete dasBlog RSS feed macro (4KB - MD5 Hash: e3d7d6320109fd07259e8d246b754f13)

Saturday, February 24, 2007 2:39:04 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [2]  | 
# Friday, February 23, 2007

We’re currently reworking www.florame.co.uk to improve it’s search engine ranking from virtually non-existent to (hopefully) first page for various inflections of organic aromatherapy, organic essential oils and all sorts of other aromatherapy products.

Despite the on-going debate on whether search engine crawlers prefer pretty XHTML or not, I still believe strongly that having your site’s content as the dominant code on every page MUST be better than having a plethora of tags (aka tag soup) but that’s for another post. So, with my feelings on XHTML (or at least neat HTML) in mind one of our recommendations was to re-work the site’s code –most importantly with the removal of the JavaScript menu at the top which is seriously impeding the site’s ranking. I decided we should opt for a form of CSS menu and those in the know, know there are only a few available options, for reference we used Suckerfish drop down menu.

The Suckerfish CSS drop down menu has been fairly heavily tested but I think I’ve found an issue with Firefox, basically the JavaScript marks up the LI with a hover class (sfhover) which then ensures it works as expected (this isn’t needed in IE7 or FF btw). The catch I’ve found however is that in FF1.5 (will test in 2.0) with scripts enabled, the menus are staying shown.

After a little head scratching the issue was narrowed down to this line of JavaScript:

this.className = this.className.replace(new RegExp(" sfhover\\b"), "");

Thanks to Firebug I was able to step through the code and check out the properties at every stage, in this instance I found that Firefox trims the leading and trailing space from the className so instead of it reading class="sfhover" as it is written, it had class="sfhover" which may be correct in some ways but obviously cocked up the regEx.

The solution is really rather simple, just change the space so it’s optional:

this.className = this.className.replace(new RegExp("\\s?sfhover\\b"), "");

It’s not an ideal fix but in the case of Florame organic aromatherapy it sorted the issue :) I’m going let Mozilla know about this as in some ways I think this is a glitch (though I can see their thinking that the developer didn’t mean to add the leading space) to see what they say. It wouldn't surprise me though if it was something I had done wrong!

For reference, the entire menu script now reads:

<script type="text/javascript"><!--//--><![CDATA[//><!--
sfHover = function() {
    var sfEls = document.getElementById("nav").getElementsByTagName("LI");
    for (var i=0; i<sfEls.length; i++) {
        sfEls[i].onmouseover=function() {
            this.className += " sfhover";
        }
        sfEls[i].onmouseout=function() {
            this.className = this.className.replace(new RegExp("\\s?sfhover\\b"), "");
        }
    }
}
if (window.attachEvent) window.attachEvent("onload", sfHover);
//--><!]]></script>

Update 7th May 2007: Darren over at Forma3 has come up with a new an improved version of the SuckerFish menu which includes a number of nice improvements and is well worth checking out: CSS drop down menus with persistent top level menu styling

Friday, February 23, 2007 6:20:17 AM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [3]  | 
# Friday, February 16, 2007

I’m still somewhat shocked at how well the series on business start up advice was received, I was expecting one or two hits on it but so far I’ve had over 1,000 visits to the article which is pretty shocking as this blog in its entirety was only getting that a year(ish)! I’ve also had some fantastic feedback which is very touching so those of you who have got in touch thanks!

Ok, following the posting of my recent business start up advice mini series I was asked by a number of people to post it as a PDF which I’ve finally managed to do. It’s rather long I’m afraid weighing in at around 26 pages so it should keep you busy giving me time to write the additional articles!

Download the PDF version of the complete business start up advice article here (27 printed pages including a 1 page feedback form - 189KB).

Friday, February 16, 2007 6:54:35 AM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [2]  | 

I was planning on writing about this new gizmo that a friend of mine from the states Doug Setzer has got his hands on but then he asked me to and I didn’t really feel like it after that ;)

The WowWee FlyTech DragonFly

Just kidding, Doug’s bought a WowWee FlyTech DragonFly and I have to say it looks great fun. He’s written a review about the WowWee FlyTech DragonFly at www.mydragonfly.info which is worth checking out. As usual he’s pimped the site out with Google AdSense so before you start clicking on his adverts make sure you have a click on mine!!

Now I’ve got to plan a trip over to the states so I can have a play with his WowWee FlyTech DragonFly...

Friday, February 16, 2007 6:49:41 AM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [3]  |