Tuesday 28 October 2008

Sample code from my top WCM tips presentation

Since AC suggested I post some sample code for some of the points I was making in my top 5 WCM tips presentation, I've now done just that. In the code you can find examples of the following:

  • Exception handler HTTP module - sends notification e-mails when an unhandled exception occurs and redirects the user to a configurable 'friendly' error page
  • Custom base class for master page - this is almost a skeleton class since your implementation will vary, but demonstrates how to use a custom base class
  • Trace helper class and matching ReSharper templates - for logging what happens in your code (this one's a bonus since I recommend these classes implement trace!)

These files can be downloaded in a Visual Studio project from:

http://sharepointchris.googlepages.com/wcmsamplecode

But for those of you who'd just like a quick look at the code, see below.  The actual classes in the project have comments to help you 'use' these things in your site - these have been removed below for readability:

Exception handler HTTP module code:

using System;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Web;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Utilities;
using COB.SharePoint.Utilities;

namespace COB.Demos.WcmSamples
{
public class ExceptionHandlerHttpModule : IHttpModule
{
#region -- Private members --

private TraceSwitch traceSwitch = new TraceSwitch("COB.Demos.WcmSamples.ExceptionHandlerHttpModule",
"Trace switch for the COB.Demos.WcmSamples.ExceptionHandlerHttpModule.");

private TraceHelper trace = null;

#endregion

#region -- Constructor

public ExceptionHandlerHttpModule()
{
trace = new TraceHelper(this);
}

#endregion

public void Init(HttpApplication context)
{
context.Error += context_Error;
}

private void context_Error(Object sender, EventArgs e)
{
trace.WriteLineIf(traceSwitch.TraceVerbose, TraceLevel.Verbose, "context_Error(): Entered error-handling module.");

// collect the last error..
var exception = HttpContext.Current.Server.GetLastError();

// process it/send e-mail..
processError(exception);

trace.WriteLineIf(traceSwitch.TraceVerbose, TraceLevel.Verbose, "context_Error(): Finished handling error, " +
"about to redirect.");

// and finally clear the error and redirect to the friendly error page..
HttpContext.Current.Server.ClearError();
HttpContext.Current.Response.Clear();
string sErrorUrl = ConfigStore.GetValue("Errors", "FriendlyErrorPageUrl");
HttpContext.Current.Response.Redirect(sErrorUrl);
}

private void processError(Exception exception)
{
trace.WriteLineIf(traceSwitch.TraceVerbose, TraceLevel.Verbose, "processError(): Entered.");

string sMessage = buildEmailText(exception);
trace.WriteLineIf(traceSwitch.TraceInfo, TraceLevel.Info, "processError(): Built error string '{0}', calling SendEmail.",
sMessage);

sendEmail(sMessage);

trace.WriteLineIf(traceSwitch.TraceVerbose, TraceLevel.Verbose, "processError(): Leaving.");
}

private static string buildEmailText(Exception exception)
{
var messageBuilder = new StringBuilder();
messageBuilder.AppendLine("<strong>Date: </strong>" + DateTime.Now);
messageBuilder.AppendLine("<br /><strong>Message: </strong>" + exception.Message);
messageBuilder.AppendLine("<br /><strong>Source: </strong>" + exception.Source);
messageBuilder.AppendLine("<br /><strong>Current user: </strong>" + Thread.CurrentPrincipal.Identity.Name);
messageBuilder.AppendLine("<br /><strong>Machine name: </strong>" + Environment.MachineName);
messageBuilder.AppendLine("<br /><strong>Url: </strong>" + HttpContext.Current.Request.RawUrl);
messageBuilder.AppendLine("<br /><br /><strong>Exception details: </strong>" + exception);
return messageBuilder.ToString();
}

private void sendEmail(String message)
{
trace.WriteLineIf(traceSwitch.TraceVerbose, TraceLevel.Verbose, "sendEmail(): Entered.");

try
{
var errorToAddress = ConfigStore.GetValue("Errors", "SendToEmail");
var sErrorSubject = ConfigStore.GetValue("Errors", "EmailSubject");

trace.WriteLineIf(traceSwitch.TraceInfo, TraceLevel.Info,
String.Format("SendEmail(): About to send error e-mail to recipient list '{0}' using SPUtility.SendEmail().",
errorToAddress));

SPUtility.SendEmail(SPContext.Current.Web, false, false, errorToAddress, sErrorSubject, message);
}
catch (Exception exception)
{
var traceMessage = String.Format("Exception thrown attempting to send error e-mail using SPUtility.SendEmail(). " +
"Exception Details: {0}", exception);
trace.WriteLineIf(traceSwitch.TraceError, TraceLevel.Error, String.Format("sendEmail(): {0}", traceMessage));
}

trace.WriteLineIf(traceSwitch.TraceVerbose, TraceLevel.Verbose, "sendEmail(): Leaving.");
}

public void Dispose()
{
// nothing to do here..
}
}
}



Skeleton base class for master page:


using System;
using System.Diagnostics;
using System.Web;
using System.Web.UI;
using Microsoft.SharePoint;

namespace COB.Demos.WcmSamples
{
public class BasePage : MasterPage
{
#region -- Private members --

private TraceSwitch traceSwitch = new TraceSwitch("COB.Demos.WcmSamples.BasePage",
"Trace switch for the base page class.");

private TraceHelper trace = null;

#endregion

public BasePage()
{
trace = new TraceHelper(this);
}

protected override void OnLoad(EventArgs e)
{
trace.WriteLineIf(traceSwitch.TraceVerbose, TraceLevel.Verbose, "OnLoad(): Entered.");

// TODO: add custom code here..

base.OnLoad(e);

trace.WriteLineIf(traceSwitch.TraceVerbose, TraceLevel.Verbose, "OnLoad(): Leaving.");
}

/// <summary>
/// Showing an example base page property..
/// </summary>
public bool UserIsAuthenticated
{
get { return HttpContext.Current.User.Identity.IsAuthenticated; }
}
}
}

Wednesday 22 October 2008

My top 5 WCM tips presentation

Had a great time presenting my WCM tips presentation over the last week or so - first to a record attendance at the UK SharePoint user group (we hit 200+ attendees for the first time!) and then to a Gold partner audience at Microsoft today. I'd like to think the user group record was due in part to the great agenda, but suspect it was really down to the draw of free beer, pizza and curry from LBi ;-) Instead of posting a simple link to the slides, I want to run through the info here because:

  1. I can attempt to convey some of the info which was in my demos here.
  2. I know how many of you will read a blog post but not follow a link to a PowerPoint deck!

First off, this presentation isn't about discussing the standard (but critical) WCM topics which you can easily find quality information on these days. Personally I think if you're embarking on a WCM project your starting point for information should be Andrew Connell's book (and his blog articles if you don't already subscribe), and reading in detail some of the key MSDN articles which have been published on the subject (I list my favorite resources at the end). In these places, you'll find discussion on some of the sub-topics (non-exhaustive list) which I see as the bedrock of WCM:

  • Security
  • Accessibility
  • Optimization
  • Deployment

So instead of covering these in detail, my tips focus on key techniques I've found to be powerful in delivering successful WCM projects. They won't be suitable for all WCM projects, but hopefully they give some food for thought and provide some extra value to the WCM info already out there.

Tip #1 - Implement HTML markup in user controls, not page layouts in SPD

Explanation:

Instead of adding your page layout HTML to the page layout in SPD, create a 'parent' user control for each layout which then contains the HTML and child user controls for this layout. This means you have a 1-to-1 mapping between page layouts in SPD to 'parent' user controls within your VS web project. These parent user controls contain the actual markup and also any child user controls used on your page.

Slide bullets:

  • Faster development experience - less SPD pain [instantaneous save rather than 3-5 second save plus occasional unwanted changes to markup]
  • Page layout markup now in primary source control [alongside your other code]
  • Much simpler deployment of updates [simply XCOPY user controls when you deploy updates to other environments]

What it looks like:

So we see that our page layouts now have very little markup (note the ASP ContentPlaceholder controls must stay at the top level, so our parent user control gets referenced in PlaceholderMain):

PageLayoutMarkup

..and then all of our real markup code is in the parent user control and child user controls which it references:

UserControlMarkup

Tip #2 - create custom IIS virtual directory pointing to your web project files

Explanation:

Most project teams I see put their user controls under 12/CONTROLTEMPLATES (often in a sub-folder) so as to follow what SharePoint does. This is mandatory in some techniques (e.g. custom field controls, delegate controls), but is not required for the type of WCM page user controls discussed in tip #1, and there are arguments for not storing those in the 12 hive. In summary, having a custom IIS virtual directory pointing to your VS project means we avoid having separate design-time and run-time locations for the project files.

Slide bullets:

  • Store user controls/page furniture files (e.g. image/XSL) here. Remove code files (e.g. .cs files) for non-dev environments
  • Faster development experience – no files to copy, no post-build events. Just save and F5!
  • Important if using tip #1 – don’t want to have to compile project [for post-build event to copy files] just for a HTML change

What it looks like:

In our case we created a virtual directory with an alias of 'MIW' (the name of our website) which points to our project files:

CustomIisVirtualDir

All our user control/page furniture file paths then look like '/MIW/PageLayoutsControls/foo.ascx'  etc.

Tip #3 - make life easier for the site authors/admins [reduce their stress and they'll be on your side]

Explanation:

This one is a non-technical tip I wanted to throw in there - whilst we're busy getting the front-end of the website right, I think it pays to also think about how authors/admins will use the back-end of the website (N.B. here I mean 'business admin' rather than IT pro). Although this is probably verging on the 'political' side of things, I'd advocate making their life as easy as possible - they often have a loud voice within the client organisation, and if they have bad feedback on what you're building that's a black mark against you.

Slide bullets:

  • Consider providing custom tools if the ‘SharePoint way’ is not simple enough (e.g. user management)
  • If you use custom lists for site data, provide a link for authors to find them (e.g. using CustomAction)
  • Remember these people are rarely SharePoint gurus!

What it looks like:

Clearly this will look different for every project, but for our client we created a custom Site Actions sub-menu with some key links:

CustomSiteActionsOptions

This sub-menu provides navigation to a couple of key lists used to power the site, but also to some custom screens we created for user management. Here, we also did some work to simplify things by wrapping up the relatively complex process of creating a user in the membership provider, adding them to various security groups across 2 sites (we had 2 'sister' MOSS sites with single sign-on) and setting certain profile fields, into some simple screens which give a convenient summary of what just happened after creating a new user:

CustomCreateUser

Finally, the 'edit profile' screens used by business administrators were adapted from those used by end users, so that the admins became very familiar with each step of the 'profile wizard' and were better able to support their users.

Tip #4 - plan for unexpected errors

This is an interesting area, partly because we're talking about that category of things which 'should never happen', but sometimes do. Having this conversation with a client (or non-technical management within your own organization) can be fun because the typical response is "whaddya mean the website going to go wrong?", but anyone familiar with software development principles knows there is no such thing as bug-free software. So the important thing is how do we handle these cases when they do occur.

There are several tips here, so I'll break them down into 4.1, 4.2 and 4.3:

 Tip #4.1 - implement 'friendly' pages for 404s and unhandled errors

Explanation:

In brief, users of a public-facing website should never see a .Net error if something goes wrong on the website! If this ever does happen, the user is left with the feeling that your website is unreliable and can lose confidence in the organisation - think about it, do you ever remember seeing this kind of error on amazon.com/eBay.com/microsoft.com?

Slide bullets:

  • Typically use custom HTTP module to override SharePoint's default error-handling behaviour, checking for:
    • HttpContext.Current.Server.GetLastError()
    • HttpContext.Current.Response.StatusCode = 404

What it looks like:

On the Standard Chartered site, our 'friendly' message looks like:

CustomErrorScreen

Tip #4.2 - implement e-mail notifications to developers for errors

Explanation:

Sorting the user experience is one thing, but what about actually fixing the source of the problem? A key element of this is being alerted whenever a user does experience a problem, rather than relying on them reporting it. When I first told the team we'd be implementing this, I was really thinking about the UAT phase, and also that critical week or two after go-live when you'll occasionally discover some latent issue which had managed to hide itself throughout all the testing. However, what we found is that we got even more value from this in the earlier dev/testing phases. At LBi, the test team sit near the development team, so when the Outlook 'toast' popped up with an exception message which wasn't 'known' by the team, I'd use the information in the e-mail to work out which tester triggered the error, then armed with the stack trace and other information, shout over and ask exactly what they had done to arrive at the problem. Much more efficient than waiting for a full bug report at the end of the day/week!

Slide bullets:

  • Means an error cannot happen without the team being aware
  • We built this for production, but was even more useful in dev/testing!
  • Implemented in same custom HTTP module as error page redirection

What it looks like:

ExceptionEmail

Tip #4.3 - implement proper tracing in your code

Explanation:

So being alerted about unhandled exceptions is one thing, but the stack trace and other details in the e-mail are often not enough information to actually fix the bug easily. This can be because we can't see exactly what happened in the code (e.g. values of variables) leading up to the error. The only real way to obtain this information from production servers (or anywhere where we can't use the debugger) is to add trace/log statements to your code, which when enabled will write out these details as the code executes. Since this has to be done manually, clearly the trade-off here is the time to implement this robustness. I would strongly recommend using a code productivity tool such as ReSharper (e.g. going beyond Visual Studio snippets) to drop in these statements quickly rather than relying on typing or copy/paste.

Slide bullets:

  • Provides ability to quickly locate bugs in your code
  • Trade off is time/effort to implement
  • Consider productivity tools such as ReSharper/CodeRush to lessen impact

What it looks like:

This is showing trace output with DebugView - notice I've configured DebugView to show me 'warning' trace statements in yellow and 'error' statements in red:

TracingError

Tip #5 - design for flexibility

Again, this one is broken down into two tips:

Tip #5.1 - using SharePoint lists for configuration data

Explanation:

As I said in my presentation abstract, since the only certainties in life are death, taxes and clients changing their minds, we should always aim to develop a solution which is flexible enough to accommodate some of the changes we may be asked to implement. We're probably all used to the idea of storing site data which may change in SharePoint lists, but I like to extend this to 'configuration' information which dictates how the site behaves. The 'Config Store' framework I wrote for this can be found on Codeplex at www.codeplex.com/SPConfigStore - this provides the list, content type, caching and API to retrieve 'config items' in code.  So to take an example from our project, we had a switch for whether newly-created users need to change their password on first logon. Clearly this is something we need enabled in production, but can be a pain in our test environments where the client needs to create users and then get on with running specific test scripts. So, by having such a switch in a SharePoint list, we get the benefit that key configuration of the site can be changed as easily as changing a SharePoint list item, as opposed to perhaps being stored in web.config where I'd need to RDP onto the server, open a file, trigger an app pool recycle etc.

We stored 130+ configuration settings in our Config Store list, and of course, applied appropriate permissions so that unauthorized users could not access the list.

Slide bullets:

  • Use SP lists for values the client may wish to edit, but consider caching

What it looks like:

ConfigStore

Tip #5.2 - implement a custom master page class

Explanation:

Although SharePoint gets master pages from .Net 2.0, these really deal with implementing a consistent look and feel only. If you want consistent functionality or behaviour across your pages, a custom master page class can help here. To implement this, the key is to modify the class which your master page derives from in the @Master directive. We used this approach to implement code which needs to execute on every page load, and even if you don't have this requirement from the start, I'd advocate using it so you have a convenient place to put such code if the need arises.  

Slide bullets:

  • Use for any code which should execute on every page load
  • Examples on our site:
    • Check if trial user/when trial access ends
    • Check if accepted Terms & Conditions
    • Check has supplied their initial user profile info
    • Enforce use of HTTPS
  • Can also use to expose properties for commonly checked items (e.g. username, logged in time)

What it looks like:

<%@ Master language="C#" Inherits="MyCompany.MyClient.MyProject.BasePage" %>

Conclusion/resources

So that's it for my tips, all feedback/suggestions welcome! In terms of key resources, in addition to AC's book here are some selected articles and so on I'd recommend looking into:

Monday 6 October 2008

Speaking at the UK SharePoint user group on WCM next week

Next week's UK user group is conveniently being hosted by LBi, my current employer for a Web Content Management (WCM) special. Now I could be biased here, given that the two presenters are me and my current boss, but I think we have some great sessions. In the first, we'll do a case study session partly based on the site I talked about previously in SharePoint WCM in the finance sector and Developer lessons learnt - SharePoint WCM in the finance sector, and in the second we'll talk about practical steps/best practices to consider when building such sites.

The date is Thursday 16th October, location is London (Brick Lane/Liverpool St), and we kick off at 6pm for a 6:30pm start - for folks who aren't local or can't make it down, slides etc. will be posted either here or on the SUGUK site. The link for full details and registration can be found at the end of this post - the full rundown on the sessions is:

Session 1 : Web Content Management in MOSS, real life case study – Riaz Ahmed
This session will showcase how SharePoint content management has helped the world’s most international bank, a top 25 FTSE 100 company, deliver an online digital solution for clients, analysts and employees.
See how SharePoint has helped empower analysts worldwide to create, publish and share research information. See how SharePoint has helped clients and investors get access to market information to help decide their next multi-million pound deal. See how flexible, extensible and customisable the SharePoint web content management platform can really be.

Session 2 : Best practices/lessons learnt: building MOSS Content Management sites – Chris O’Brien

This presentation explores key tips and developer techniques for building WCM sites, based on the experience of delivering sites such as that shown in the earlier session. As well as key topics such as security, optimisation and deployment, much of the presentation will cover practices which are not widely documented but which can shave time and make the difference on a very tight timescale. And, since the only certainties in life are death, taxes, and clients changing their minds, we’ll examine how to build a site which is as flexible as possible whilst not being substantially more difficult to implement. Finally, we’ll wrap up with a discussion on how developers can keep their sanity by being able to fix bugs quickly (especially when the boss is stood over their shoulder!) regardless of where they occur in the code, before going on to open Q & A.

For full location details and to register see http://suguk.org/forums/thread/13880.aspx.

As usual, we'll be going for a pint to continue the chat afterwards - all are welcome!