Mark Everard

Hello, I'm Mark – a PhD physicist turned technologist / architect.

Archive for November, 2010

Storing recently-viewed EPiServer page visits

without comments

User tracking and delivering a personalised web experience is now a default part of any modern web-build. If you’re Amazon or another on-line retailer then there is real business value in storing and analysing as much user data as you can (hence the advent of cloud based storage solutions such as Amazon S3 which was conceived firstly for use by Amazon itself before being opened up as a service). This data allows Amazon to calculate what you actually want for Christmas before you even know yourself!

In the more simple cases where there is no commercial requirement and the only use for this data is to provide a visual reminder to the user about the pages / content they’ve visited on your site, then storing this information in a browser-based cookie is a sensible approach.

The data we will need to store in the browser cookie is just a list of EPiServer PageReference Id’s for each of the recently visited pages (I’m not going to take explicit account of any separate language version visits).

I’ve wrapped this int value type in a more meaningful domain object (RecentPageHistory) and overridden the Equals method so we can accurately compare two RecentPageHistory objects (which we’ll need to do so we don’t get two instances of the same page in our page history list).

public class RecentPageHistory
{
	public int PageReferenceId { get; set; }

	public override bool Equals(object obj)
	{
		if (obj == null)
			return false;

		if (obj is RecentPageHistory)
		{
			var assetHistory = obj as RecentPageHistory;
			return assetHistory.PageReferenceId == PageReferenceId;
		}

		return false;
	}
}

Recording a single user visit is best abstracted down to a base class inherited by all .aspx pages that you wish to record user visits for.


public class RecordUserVisitPageBase : TemplatePage
{
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
            AddPageToUserHistory();
        }

        protected void AddPageToUserHistory()
        {
            var recentPageHistoryService = new RecentPageHistoryService();
            RecentPageHistory historyItem = RecentPageHistoryFactory.Create(CurrentPageLink.ID);
            HttpCookie updatedHistoryCookie = recentPageHistoryService.AddPageHistoryItemToCookie(GetUserHistoryCookie(), historyItem);
            AddCookieToResponse(updatedHistoryCookie);
        }

        private void AddCookieToResponse(HttpCookie updatedHistoryCookie)
        {
            Response.Cookies.Add(updatedHistoryCookie);
        }

        private HttpCookie GetUserHistoryCookie()
        {
            return Request.Cookies[RecentPageHistoryHelper.CookieName] ??
                   new HttpCookie(RecentPageHistoryHelper.CookieName);
        }
    }
}

Finally there is the service level class which is responsible for the CRUD operations against the data storage mechanism (in this case a cookie). Although most of the functionality is not EPiServer specific, there is one method (GetPagesFromHistoryList) that deals with the EPiServer API. This method will populate a List of PageData objects and will also explicitly remove any PageReferenceID’s from the cookie that refer to pages that can no longer be found in EPiServer. Note that this method passes the cookie as a reference parameter so any consuming user interface class (page/user control) should also ensure that this cookie is added to the response stream so that the missing EPiServer page’s are removed from the cookie cache.

public class RecentPageHistoryService
{
	protected const int MaximumHistoryItems = 4;
	protected HttpCookie AssetHistoryCookie { get; set; }

	private List<RecentPageHistory> GetPageHistoryListFromCookie(HttpCookie cookie)
	{
		var list = new List<RecentPageHistory>(MaximumHistoryItems);
		for (int i=0; i < cookie.Values.Count; i++)
		{
			int cookiePageId;
			if (!int.TryParse(cookie.Values[i], out cookiePageId))
				continue;

			RecentPageHistory userHistory = RecentPageHistoryFactory.Create(cookiePageId);
			list.Add(userHistory);
		}
		return list;
	}

	public HttpCookie AddPageHistoryItemToCookie(HttpCookie existingCookie, RecentPageHistory pageHistoryItem)
	{
		List<RecentPageHistory> historyList = GetPageHistoryListFromCookie(existingCookie);

		if (historyList.Contains(pageHistoryItem))
			historyList.Remove(pageHistoryItem);

		historyList.Insert(0, pageHistoryItem);

		if (historyList.Count > MaximumHistoryItems)
			historyList.RemoveAt(MaximumHistoryItems);

		return PopulateCookieFromHistoryList(historyList);
	}

	private static HttpCookie PopulateCookieFromHistoryList(IEnumerable<RecentPageHistory> pageHistoryList)
	{
		var newCookie = new HttpCookie(RecentPageHistoryHelper.CookieName);

		foreach (RecentPageHistory pageHistoryItem in pageHistoryList)
			newCookie.Values.Add(pageHistoryItem.PageReferenceId.ToString(), pageHistoryItem.PageReferenceId.ToString());

		return SetCookieExpiry(newCookie);
	}

	private static HttpCookie SetCookieExpiry(HttpCookie cookie)
	{
		cookie.Expires = DateTime.Now.AddMonths(1);
		return cookie;
	}

	private HttpCookie RemoveHistoryItemFromCookie(HttpCookie existingCookie, RecentPageHistory pageHistoryItem)
	{
		List<RecentPageHistory> historyList = GetPageHistoryListFromCookie(existingCookie);

		if (historyList.Contains(pageHistoryItem))
			historyList.Remove(pageHistoryItem);

		return PopulateCookieFromHistoryList(historyList);
	}

	public IEnumerable<PageData> GetPagesFromHistoryList(ref HttpCookie cookie)
	{
		var pagesInHistoryList = new List<PageData>();
		var pageHistoryList = GetPageHistoryListFromCookie(cookie);

 		foreach (var pageHistoryItem in pageHistoryList)
		{
			var assetReferenceId = new PageReference(pageHistoryItem.PageReferenceId);
			try
			{
				 pagesInHistoryList.Add(DataFactory.Instance.GetPage(assetReferenceId));
			}
			catch (PageNotFoundException e)
			{
 				cookie = RemoveHistoryItemFromCookie(cookie, pageHistoryItem);
			}
		}
		return pagesInHistoryList;
	}
}

I’ve not shown the code for a few of the small helper methods I’ve used. All that’s left is to write a user interface element that displays the pages (and updates the cookie). I won’t show that here as its even more straight forward than the rest of this walk through!

All the code needed to run this has been uploaded to the EPiServer World Code section, including the necessary User Interface elements to display the data on an EPiServer CMS6 Public Templates project home page, where (as long as you remember to add the RecordUserVisitPageBase to the set of page templates) you should end up with something like (in the bottom right)…….

Written by mark

November 19th, 2010 at 3:41 am

Posted in ASP.NET,EPiServer

Developing a custom workflow in EPiServer : Part three

without comments

This is the third post, in a series of five about developing a custom workflow in EPiServer.

Designing the Workflow

Now we’ve defined the communication pathway between the application (EPiServer) and the Workflow runtime, it’s time to use the designer surface to piece together pre-existing Windows workflow activities, the EPiServer specific activities and the bespoke EventDriven Activities that are raised by the host application.

This is done using a Visual Studio design surface which auto-magically generates code files descibing your workflow based on what you drag-and-drop onto the surface. Personally I hate developing in this manner, the one redeeming feature is that it does make for some easily created diagrams demonstrating whats going on!

workflow-design-surface

Here you can see the Two-stage workflow laid out on the designer surface. Each box represents a unique State within this StateMachine workflow, with the arrows indicating the possible transitions between states. Initally the workflow process is created in the InitialState (which is set by right-clicking and selecting the relevant option from the dropdown).

Each State contains a list of Activities, for example the PageApproverState contains four activities; three bespoke EventListener activites and a StateInitialization Activity. You’ll notice I’ve used an StateInitialization Activity as a common pattern across all of the States (barring the FinalizedState – which has no additional work to do). This will initialise / modify any data within the workflow to be persisted (which in our case is the data encapsulated by our TwoStageEventArgs).

StateInitializationActivity

StateInitialization Activities, available by dragging and dropping from the Visual Studio Toolbox 🙁 are performed by the workflow runtime as soon as the StateMachine workflow is moved into that particular state. They can contain any number of other Activities, so as to make up your exact Workflow requirements.

We’ll be using them to perform the following common tasks:

1) Email the workflow creator to inform them of the change of workflow state.
2) Create an EPiServer Task (which are designed for informing users or groups about workflow tasks), using the information provided in the passed TwoStageEventArgs.
3) Moving the workflow into a sequential state when there is no input expected from an external user (for example in the case of moving from PageApprovedState to FinalisedState)

The code for each of these three types of Activity is already implemented for us (either within the Windows Workflow library or the EPiServer Workflow library), so all we have to do is drag them from the Visual Studio toolbox onto our workflow surface and bind the relevant data to them. Sending emails is handled by the SendEmailActivity available within Windows Workflow Foundation library, as is the SetStateActivity – which moves the StateMachine into a defined State.

The CreateTaskActivity (defined in EPiServer.WorkflowFoundation.dll – make sure this assembly in added to the Visual Studio toolbox) creates a Task associated with a User or Group and also sends an Email notification to them. EPiServer Tasks nicely bind the workflow to the EPiServer user interface, so the User/Group will see them and be able to interact as soon as they log in to EPiServer.

Additionally there are some activities that must occur in particular workflow states, such as initially associating the workflow with an EPiServer page so that the EPiServer UI is aware that the current workflow instance is asscociated with a page and can populate the Workflow tab in Edit mode with the workflow instance details. We’ll look at these in the next post

EventDrivenActivity

EventDriven activities are the most important part of a StateMachine workflow as they are the activities which are execute when a pre-defined user event is raised from the host application. Microsoft have helpfully provided a tool for creating the bespoke EventDriven Activites based on the events and methods that are exposed from any class marked with the [ExternalDataExchange()] attribute.

The (wca.exe) tool lives in:

“Program Files””Microsoft SDKs”Windowsv6.0Abin

and when run it needs to be passed arguments informing it of the path of the assembly to scan and also the namespace and location in which to create the bespoke EventDriven activities. In our solution it will find the ITwoStageService and create EventDriven activities based against the four events defined in that service, PageApproved, PageDeclined, PagePassedToApprover and PagePassedToPublisher.

Each of these bespoke event-activities needs to persist the current state of the workflow, and also move the StateMachine to a subsequent State. The first of these responsibilities is handled by a custom piece of code with the workflow code file, which must be bound to the Invoked handler on each custom Event activity.


private void OnExternalUiEventHandled(object sender, ExternalDataEventArgs e)
{
   var currentArgs = e as TwoStageEventArgs;
   if (currentArgs == null)
      throw new ArgumentException("Workflow event args were not of type TwoStageEventArgs");
   WorkflowStateArgs = currentArgs;
}

This code stores the passed TwoStageEventArgs locally in the workflow so that when the workflow runtime persists the current state, the TwoStageEventArgs are serialised and stored too.

After Structure – comes Databinding….

After you’ve set up your structure you will need to bind specific data to your Activities within each State. This is done using the Visual Studio Properties window, where you will need to hook up and bind a code property to the Activity property. In most cases these properties will be encompassed in your TwoStageEventArgs (which effectively represent the current Workflow State), however sometimes there are specific pieces of data needed, such as the SMTPsettings or simple strings. In these cases, add additional code properties (with getters) within you workflow class to get the data and then bind your Activity property to these properties.

Next post , we’ll cover EPiServer Tasks and also how to present Workflow User Interfaces in Edit and Admin mode.

Written by mark

November 10th, 2010 at 10:19 am

Using NuGet for EPiServer third-party libraries

with 2 comments

NuGet (formerly known as NuPack) is an third party library package management system, heavily integrated with Visual Studio 2010. Its received a lot of attention since Microsoft first announced it, and for good reason. It enables you to simply include third-party libraries into your solution, where NuGet will do all of the heavy lifting and set-up for you.

This can be a real time saver over installing a library manually where you’d have to download the library, unzip it, copy it to your solution, add reference in Visual Studio, and then possibly update the configuration. NuGet offers a better way….

EPiServer developers already have a package manager of sorts via  EPiServer’s own Deployment Center – which allows you to deploy pre-made .epimodule or .zip packages to existing sites. This is actually a similar concept to NuGet – but is implemented against IIS sites rather than integrated directly with Visual Studio, and to be honest not many developers go as far as to create deployment modules for their EPiServer contributions.

For me, the great advantage to NuGet is the ability to browse and search multiple package feeds for the latest modules, and then with a single click have them and their dependencies included in your solution. NuGet by default, points to a Microsoft feed (that anybody can contribute packages to), but it can also point to a private feed hosted on a network share or hosted through IIS (using an included Microsoft MVC web application).

This means that along with the main Microsoft public feed you could have:

  1. A company feed of approved packages to be used in standard company ASP.NET solutions.
  2. A public feed of packages specific to a particular product  (anybody see what I’m getting at here…..)

So EPiServer, how about setting up your very own NuGet feed? (It’s very simple). Along with the EPiServer code section, this would provide a central and consistent way of searching for and installing new EPiServer modules, rather than the increasing fragmented methods at the minute (within blogs, EPiCode and a growing number of CodePlex projects).

As a small encouragement I’ve put together (the very first) EPiServer NuGet package.

An Example Package

Its very straightforward to create your own package using the tools provided on the codeplex site

I’ve made a NuGet package for every developer’s favourite EPiServer third-party library – Page Type Builder (version 1.3). This is a pretty simple package which includes the two core PageTypeBuilder libraries, and references to two other NuGet packages (Castle.Core and StructureMap) that Page Type Builder depends upon.

I’ve temporarily hosted this package on my blog – so if any of you fancy trying it out, first download and install NuGet, then from within Visual Studio, right-click on your references folder in a solution and click ‘add package reference’. Click settings and add http://www.markeverard.com/nuget/feed.xml as your default package server. Trying to add a package reference will then get you a screen like this…..

nuget-pagetypebuilder

If you now click the install Page Type Builder, NuGet will automatically download the package and the Castle.Core and StructureMap dependencies and add them to your solution. How easy is that!

NuGet is currently only in CTP, so its still rough around the edges, especially the UI. So if its a bit flakey at the minute then rest assured it will get better. One missing feature is the ability to resolve dependencies from other public feeds, meaning that I’ve also had to host Castle.Core and StructureMap packages rather than using the packages available on the public Microsoft feed.

Written by mark

November 4th, 2010 at 1:37 pm

Posted in ASP.NET,EPiServer