AJAX, ASP.NET, Silverlight

Control Silverlight by Using Browser Back and Forward Buttons

The Problem

Silverlight applications suffer from many of the same issues that AJAX applications suffer from. Both AJAX and Silverlight applications can be dynamically modified without a browser post back. This presents and interesting issue.

Say for example you have an application which presents the user with a number of wizard steps (much like a workflow of some kind). The user enters each step and clicks “Next”. The AJAX or Silverlight applications loads and displays the next step dynamically.

The user gets through a number of steps and realises they made a mistake one or two steps back. Many users will erroneously use the browser back button to perform this navigation. Most AJAX and Silverlight applications will not handle this action, and instead of correctly navigating to the previous step, the browser will actually navigate to the previous “page”. In many applications this may be a login page, or the site the user was on before they navigated to your site.

The Solution

I’ve seen a few solutions to this problem, although many of them not cross browser.

Synergist has a post here on using some of the new AJAX features in IE8. An updated post here shows this working using a dispatch timer in some more browsers.

Both of these solutions are okay but there is a lot of custom code here, and as Michael (Synergist) figured out, there were cross browser issues (IE7 problems etc).

A Better Solution

With .NET 3.5 SP1 came ASP.NET History – a great cross browser history navigation implementation. It’s very easy to use. As the user performs actions you set history points – a history point contains the page title and some data (this can be a complex JavaScript object etc). You hook in to the “user has navigated using the browser back or forward button” events and when they fire you get your piece of data returned. There is lots of information available about ASP.NET AJAX History, have a Google around. There is also a quick intro video here.

Note: ASP.NET AJAX History was added with .NET 3.5 SP1 (along with ADO.NET Data Services and Entity Framework amongst other things).

All that remains is to hook a Silverlight application in to these events and have it create the history points as required. This is all made very easy by Silverlight’s DOM bridge.

The steps involved here are:

  • Create classes in both Silverlight and JavaScript to manage the history communication between the two.
  • The Silverlight class will encapsulate communication of new history points to the JavaScript code. It will also receive notification of navigation changes and raise events to other Silverlight code.
  • The JavaScript class will watch for navigation changed events and notify the Silverlight code of the events. It will also provide an interface to the Silverlight class to add new history points.
  • The Silverlight application will instantiate the history manager and expose it to the JavaScript using HtmlPage.RegisterScriptableObject
  • Set EnableHistory to true on the ScriptManager control.

<Sample Code>

ASP.NET AJAX History and Silverlight Code Example

</Sample Code>

Please note: when running this code please ensure the web project is set to Start Up Project, and you default page is the sample ASPX page.

Demonstrate the Problem

Grab a copy of the sample code to make this section less painful 🙂

Create a new Silverlight Web Application Project (not a Web Site Project – never create one of these unless you really need to).

The first step is to create a little data model that we can use to mock up some data when the user moves back and forward through the application. Create a new class in the Silverlight application called SomeDataModel.

public class SomeDataModel
{
	public static string GetData(string dataId)
	{
		return string.Format("This is data item: {0}", dataId);
	}
}

Next add a couple of buttons to Page.xaml (back and forward) and a TextBlock to write out the result. In the Click event of each of these buttons increment/decrement a local variable and pass it to the SomeDataModel to mock up the data.

private void BtnForward_Click(object sender, RoutedEventArgs e)
{
	currentPoint++;
	TxtOutput.Text = SomeDataModel.GetData(currentPoint.ToString());
}

Now when you run the application you will be able to use your back and forward buttons to simulate paging through records or wizard steps.

Note at this stage of the application you cannot move back and forward using the browser back and forward buttons. If you had navigated to the Silverlight application from another page then clicking Back would take you back to that page, and not the previous record. A user may make this mistake.

Prepare Silverlight to Interact with ASP.NET AJAX History

If you have not previously done any JavaScript interaction from Silverlight 2 then I strongly suggest you have a bit of a Google around for terms like ScriptableMember and ScriptableType before moving forward.

Add a new class called HistoryManager to the Silverlight project. This class will be signalled from the JavaScript when a navigation event occurs and also allow other Silverlight code to create history points. This class needs to:

  • Expose events to signal when it has been signalled from JavaScript that a navigation event occurred
  • Expose methods to allow the addition of history points from other Silverlight code
  • .cs file will include a small EventHandler derived class to use when firing navigation events

Add the new event handler derived class to the bottom of the file after your new class.

public class HistoryEventArgs : EventArgs
{
	public string DataId { get; set; }
}

In the HistoryManager class expose some events to fire when a) class is ready to set history points and b) when a navigation notification is received from JavaScript. Also add a local variable to hold the reference to the instantiated JavaScript object.

//Raise this event when the JavaScript code notifies this class that Back or Forward was clicked.
public event EventHandler HistoryChanged;

//Raise this event when the object is set up and ready to accept new history points. This is 
//to ensure that history points are not set before the JavaScript code has initialised and 
//the JavaScript objects are known to this class
public event EventHandler HistoryReady;

//The JavaScript object that will be created on page load and passed in to this Silverlight class.
ScriptObject jsHistoryObj = null;

Next add some methods that will be called from JavaScript to deal with a navigation event and to set the JavaScript object on initialisation.

/// <summary>
/// Provides an interface for the JavaScript to call when a navigation JavaScript event is fired by ASP.NET AJAX
/// </summary>
/// The JavaScript object that is raising the event (will be managed ScriptObject at this point)
/// The data that was passed as part of the navigation event (in this case our "DataID")
[ScriptableMember()]
public void LoadPoint(object sender, object args)
{
	if (HistoryChanged != null &amp;&amp; args != null)
	{
		//Raising the HistoryChanged event so that subscribers can handle approriately
		HistoryChanged(this, new HistoryEventArgs() { DataId = args.ToString() });
	}
}

/// <summary>
/// This method is called from JavaScript to pass in the JavaScript class which will be used to add history points.
/// </summary>
/// The instantiated JavaScript class
[ScriptableMember()]
public void SetJSHistoryObject(ScriptObject sender)
{
	jsHistoryObj = sender;
	if (HistoryReady != null)
	{
		HistoryReady(this, EventArgs.Empty);
	}
}

Finish the class off with methods to add new history points and to set the browser page title.

 /// <summary>
/// Consumed by the Silverlight project to add a new history point. 
/// Uses the object passed in originally to SetJSHistoryObject from JavaScript.
/// </summary>
/// The internal Silverlight "DataID" - i.e. the piece of data to store
/// The page title to set (which will show up in the history of the browser)
public void AddPointData(string dataId, string title)
{
	jsHistoryObj.Invoke("addHistPoint", new object[] { dataId, title });
}

/// <summary>
/// Set the title in the browser.
/// </summary>        
public void SetPageTitle(string title)
{
	HtmlPage.Window.Eval(string.Format("document.title = '{0}'", title));            
}

Expose the HistoryManager to JavaScript

Silverlight classes need to be exposed before they may be consumed from JavaScript. The steps are:

  • Instantiate the class and store in to a local variable (usually in the App.cs file)
  • Use HtmlPage.RegisterScriptableObject to name and register the object in JavaScript

Add a local to hold the instanciated HistoryManger and alter the App() constructor to instantiate and register the object. Add a property to get the HistoryManager from other classes (like Page.xaml.cs)

//Holds the instanciated HistoryManager object for later reference.
HistoryManager historyManager;

public App()
{
	.........

	InitializeComponent(); //place the change after this

	//Instantiate the HistoryManager on application start and register it as a scriptable object.
	historyManager = new HistoryManager();
	HtmlPage.RegisterScriptableObject("silverlightHistoryManager", historyManager);
}

public HistoryManager History
{
	get
	{
		return historyManager;
	}
}

Get the JavaScript Ready

We need to create a little helper class in JavaScript to assist with the history events and to create new history points. This is the class that the code above will call.

Add a new JavaScript file to the web project and include it in the page (as an asp:ScriptReference on the ScriptManager control).

The break down of this JavaScript class will be:

  • Created as a class (JavaScript prototype)
  • On creation, navigationEventHandler function is set as a handler for Sys.Application.add_navigate which is fired when the user moves back and forward using browser buttons
  • On initiation gets the Silverlight object passed in and stores as local variable. This variable will be used to call methods back in Silverlight across the JavaScript DOM bridge
  • Exposes a method to set a new history point
  • File also includes code to instantiate the history object as well as provide an event handler for the Silverlight object OnPluginLoaded event to initialise the history controller class

Create a new JavaScript class. Include methods to initialise, handle a navigation event and to create a new history point:

historyManager = function() {
    this._silverlightControl = null;
    this._sHM = null;    
}

historyManager.prototype = {
    init: function(sender) {
        
    },
    navigationEventHandler: function(sender, args) { //This method will be called by ASP.NET AJAX when the user uses the back and forward buttons.
        if (this._sHM != null) {
            this._sHM.LoadPoint(sender, args.get_state().data);
        }
    },
    addHistPoint: function(pointData, pageTitle) { //This method is called from Silverlight to add a new history point.
        Sys.Application.addHistoryPoint({ data: pointData }, pageTitle);
    },
    setPageTitle: function(title) {
        document.title = title;
    }    
}

After the historyManager prototype definition, add some code to instantiate the object and hook it up to the ASP.NET AJAX History navigation events.

//Instantiate the historyManager. 
var historyInstance = new historyManager();

//Create a delegate to preserve scope when the navigation event handler fires.
var handler = Function.createDelegate(historyInstance, historyInstance.navigationEventHandler);

//Add the delegate tot he add_navigate event. This will cause the navigationEventHandler method of 
//historyManager to fire when the user uses the back and forward buttons in the browser.
Sys.Application.add_navigate(handler);

Add some code above the historyManager class to handle the Silverlight object’s load event.

function slLoad(sender) 
{
    var run = Function.createDelegate(historyInstance, historyInstance.init);
    run(sender);
}

In the ASPX page, add an OnPluginLoaded event to the asp:Silverlight control. This will call the JavaScript function and pass itself (the Silverlight control) in as the parameter.


The slLoad functio then creates a delegate (to preserve function scope) and calls the init function on the historyManager object.

The init function then gets out the Silverlight object and stores a reference to it. It then calls in to the Silverlight object using it’s SetJSHistoryObject method which then provides a reference to the instanciated JavaScript object from the Silverlight managed code.

Set Some Points and Off We Go!

Back in Silverlight now, open Page.xaml.cs. Add some code to create a history point in both the back and forward button event handlers that were created earlier. Add a call to addHistoryPoint from both these event handlers.

void addHistoryPoint()
{
	(App.Current as App).History.AddPointData(currentPoint.ToString(), string.Format("History when data was: {0}", currentPoint));
}

In the page constructor subscribe to the HistoryManager’s HistoryChanged and HistoryReady events.

//Hook up to the HistoryManager's HistoryChanged and HistoryReady events.
(App.Current as App).History.HistoryChanged += new EventHandler(History_HistoryChanged);
(App.Current as App).History.HistoryReady += new EventHandler(History_HistoryReady);

Add the following code to the event handlers that were created:

/// <summary>
/// Called when the HistoryManager object signals that it is initialised and ready to start accepting history point additions.
/// </summary>        
void History_HistoryReady(object sender, EventArgs e)
{
	//Create the first default history point.
	addHistoryPoint();
}

/// <summary>
/// When the ASP.NET AJAX framework signals to the JavaScript that there was a navigation event, the JavaScript object 
/// signals to the Silverlight HistoryManager object, which in-turn signals the event that this class subscribed
/// to in the Page() constuctor. This is the event handler at the end of that process.
/// </summary>
/// 
/// 
void History_HistoryChanged(object sender, HistoryEventArgs e)
{
	//Grab the history data that was passed as part of the event.
	int historyData = Convert.ToInt32(e.DataId);
	
	//Ensure that the navigation actually moved... don't perform any useless moves.
	if (historyData != currentPoint)
	{
		//Logic to load out and perform actions on the loaded data.
		currentPoint = historyData;
		TxtOutput.Text = SomeDataModel.GetData(currentPoint.ToString());
		//Manually re-set the browser title to appropriate text.
		(App.Current as App).History.SetPageTitle(string.Format("History when data was: {0}", currentPoint));                
	}
}

Basically the code creates the inital start up point when the HistoryManager signalls that it’s completed initilisation (i.e. all JavaScript objects are ready).
When the HistoryManager signals that the user has used back or forward browser buttons, the History_HistoryChanged is called at the end of the chain and it is ultimately what uses the history data to change the screen presentation.

A little side note is that you must re-set the page title youself.

That’s about it!

I know this is a bloody long post but I beleive this to be an important problem that needed solving before Silverlight could fill a true line of business application’s needs.