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.

About these ads

24 Responses to Control Silverlight by Using Browser Back and Forward Buttons

  1. Hi Jordan,

    This is awesome! I really liked your chalk talk, and how you can re-use the ASP.NET AJAX 3.5 SP1 libraries to do cross-browser history integration. The solution I’ve seen so far isn’t as elegant as this. I’ll do a follow-up post later this week adding back/forward navigation to my Dive Log sample application. I’ll see if I can find a way to make it fit nicely with the design patterns applied in the application.

    Also, I’ll see if I can find a way to limit the JavaScript part of your solution even more, making it more “self contained” inside the Silverlight project.

    Talk to you later!

    - Jonas

  2. Jordan says:

    Hi Jonas,

    It’s always nice to have good feedback. I also enjoyed your Silverlight 2 for Developers talk :)

    You are right on the self contained aspect it would be much nicer to tuck this stuff away. The only benefit of the current method is that it can be applied to standard AJAX apps as well with very little modification.

  3. Nice one Jordan, this is an important step in the acceptance of Silverlight for general consumption in LOB apps.

  4. [...] gives a great rundown on Controlling Silverlight by Using Browser Back and Foward Buttons John Papa shares his tips on Declaring Events Pavan Podila on having the Lawson Smart client using [...]

  5. Hi Jordan,

    very cool sample. I like the fact that you built it from scratch and illustrated the problem.

    I’d suggest that the next step is taking it from sample code towards becoming an OS framework.
    One thing i’ve noticed using back/forward managers like this in the past, is that patterns of use quickly surface. a good framework should support those patterns easily without too much fussing around.
    The 64M$ question for this kind of framework would be “What kind of common changes does the user want to store in his histroy?”. I’d love seeing a history manager that takes that into account.

    All and all, pretty cool stuff.

    One thing perplexes me though, if you’ve already go HtmlBridge access from Silverlight, why are you using a Javascript prototype? you could build a fully managed class that access “Sys.*” for you without using a Javascript prototype.

  6. [...] Control Silverlight by Using Browser Back and Foward Buttons [...]

  7. Jordan says:

    Hey Justin-Josef,

    Very good point about the JavaScript. Jonas made the same comment earlier…

    The reason I placed the code outside of the Silverlight application was to demonstrate the ASP.NET AJAX History stuff a little more clearly. The JavaScript code there could be used separately from a Silverlight application.

    On a similar note, a lot of Silverlight applications will be little islands of functionality inserted in to older AJAX sites – so this code could control the AJAX and Silverlight bits at the same time.

    My next few posts are going to concentrate on interoperability between AJAX and Silverlight applications – discovering nice ways to slowly upgrade existing applications.

    I like your comments on a framework style package. Perhaps down the track there could be something similar to the AJAX Control Toolkit for Silverlight :)

    Cheers,

    Jordan.

  8. [...] September 10, 2008 — Jordan I’ve uploaded a companion screen cast for my Control Silverlight by Using Browser Back and Foward Buttons [...]

  9. blogesh says:

    Good stuff, Jordan.

    Looking forward to seeing your presentation at Vic. NET User group :)

  10. Rob Burke says:

    Sometimes the ‘bloody long posts’ are exactly what’s called for :)

    Thanks for your really thorough explanation and the best solution to browser history for Silverlight I’ve seen, for exactly the reasons you mention at the top of the article.

    Thank you!

  11. Jordan says:

    @Rob Burke -> thanks for that mate! And thanks for referenceing me in your training material :)

  12. CromyCicFoemy says:

    good resourse Anyway by sight very much it is pleasant to me

  13. [...] See this post for an effective use of Silverlight and the HTML DOM Bridge to control Silverlight navigation with the browser back and forward buttons. [...]

  14. Ricardo Fiel says:

    This is sooo coool! For sure the best implementation of browser history in silverlight I’ve seen. Congrats for an excellent work.

    Can I use this on commercial websites?

    Cheers

    • Jordan says:

      Hey Ricardo,

      I have no problem with using this stuff commercially. Good luck with your project :)

      Jordan.

  15. [...] As of SP1 3.5, ASP.NET exposes a History Control that simplifies the solution significantly (see Control Silverlight by Using Browser Back and Forward Buttons). There is a variety of solutions available online in case you are not using ASP.NET 3.5 SP1, but [...]

  16. Anita says:

    Hi,

    I am trying to use this method. I have plugged in all the necessary code, but unable to call the function slLoad. In Silverlight.createObject, I am using OnPluginLoaded: slLoad, but the function never gets called. I am using Silverlight 3. What am I doing wrong? Thanks!

  17. Jordan says:

    Hey Anita,

    I’ve not tested this on SL3, although I’m not sure why it wouldn’t be working..

    Can you please post some of your JS code so I can see what you are doing?

    As a side note, have you tried out the new SL3 navigation framework? It has browser back and forward navigation built in at the core…

  18. Martin says:

    For anyone looking to use this on a SL3 website I made the following changes to get it working:

    1) Change the definition of the silverlight control in the Run ATestPage.aspx file from an asp:Silverlight control to an SL3-style object tag. Specify that the onLoad event will point to the slLoad nethod.

    2) Change the following line in HistoryControl.js:

    this._silverlightControl = sender.get_element();

    to

    this._silverlightControl = document.getElementById(‘silverlightControl’);

    There are probably better ways of fixing this but this seems to work for me.

    We would love to use the Navigation Framework but due to it lack of extensibility (which I believe is solved in SL4) we can’t

  19. TomTom GO910 says:

    Hello there, Just what an nice website this is

  20. MrZer0 says:

    Best solution for this problem I’ve seen so far. Furthermore one of the best readable tutorials even for none native English speakers ;-)

  21. I discovered your web site via Google while looking for a related subject, lucky for me your web site came up, its a great website. I have bookmarked it in my google bookmarks.
    Please Visit ===> Physician Assistant Training Class

  22. Best Knight Online Bots , Cheats and bugs…

    [...]Control Silverlight by Using Browser Back and Forward Buttons « Developer Flotsam[...]…

  23. Lukas says:

    Hi, unfortunately your Sample Code Link is dead…navigating there to webjak.net does not work.
    I really would like to see your solution since I haven’t found a good one yet for this problem :-)

    Cheers,
    Lukas

Follow

Get every new post delivered to your Inbox.

Join 32 other followers

%d bloggers like this: