Introduction
Silverlight is a great technology for bringing richer user experiences to the web client. But because Silverlight is a control on a webpage, scenarios can arise in which a logical navigation has occurred within the Silverlight control (for example, the user has clicked a button which causes a panel to update) but for which no actual webpage navigation has occurred (a problem familiar to AJAX developers). Because the web's dominant paradigm is one of a navigation stack, the user expects to be able to "go back" by pressing the large, convenient Back button as happens on most other websites that they have visited. Most displeasingly, doing so can navigate them away from the control entirely, to the previous page they were visiting, thus destroying all the state that existed in the control.
Various techniques have been developed to try to fit the square peg of expected user behavior predicated on Web 1.0 into the round hole of Web 2.0 style technology that does not use the corresponding browser navigation stack. Of these, perhaps the most elegant solution is to manipulate the browser's navigation stack - so that pressing the Back button indeed moves the Silverlight control "back" to the previous logical state. This completely conforms to the user's expectations of how the web behaves, and has the additional advantage of colonizing the browser's toolbar area for application specific logical commands (back and forwards buttons might otherwise end up on the control itself).
It turns out to be quite easy to manipulate the browser's navigation stack in this way, by means of a hidden IFRAME. Each logical navigation point is stored in this IFRAME
and then restored to the control via JavaScript when the user presses the Back button. The good folks from the Yahoo User Interface (YUI) team have wrapped this use of an IFRAME
within a convenient (free and unrestricted) JavaScript module that works on most browsers and platforms.
This article shows how to combine a Silverlight control that has distinct navigable states with the YUI library module to recreate the familiar web browser paradigm.
Using the Code
To use YUI History on your page, you first need to include the YUI JavaScript libraries. These can be obtained here.
<script type="text/javascript" src="yui/yahoo-dom-event.js"></script>
<script type="text/javascript" src="yui/history-min.js"></script>
You also need to add the hidden IFRAME
and hidden INPUT
that will be used to store the control state:
<style type="text/css">
#yui-history-iframe {
position:absolute;
top:0; left:0;
width:1px; height:1px;
visibility:hidden;
}
</style>
...
<iframe id="yui-history-iframe" src="blank.htm"></iframe>
<input id="yui-history-field" type="hidden"/>
Communication from JavaScript to the Silverlight control is through the JavaScriptBridge
class. Whenever the browser's navigation state is changed, JavaScript will call the LoadState
method.
public class JavaScriptBridge
{
Page _page;
public JavaScriptBridge(Page page)
{
_page = page;
}
[ScriptableMember]
public void LoadState(string state)
{
if (String.IsNullOrEmpty(state))
_page.SetInitialState();
else
_page.LoadState(state);
}
}
private void Application_Startup(object sender, StartupEventArgs e)
{
Page page = new Page();
this.RootVisual = page;
JavaScriptBridge bridge = new JavaScriptBridge(page);
HtmlPage.RegisterScriptableObject("jsBridge", bridge);
string initialState = HtmlPage.Window.Invoke("GetInitialState") as string;
if (initialState != null)
bridge.LoadState(initialState);
}
Because there is a race condition between the YUI history manager and the Silverlight control's load event, there is a bit of logic to make sure that the control is always initialised with the initial start state (that may have been set via the query string).
The control calls GetInitialState
, which returns the initial state if YUI has loaded, otherwise null
. When YUI loads (_Init
), it checks if the control has loaded. If so, it tells the control the page's initial state.
The YAHOO.util.Event.onDOMReady
event handler will fire when the page's DOM has been loaded, and this is when the YUI history module is initialized.
var g_initialState = null, g_controlHasLoaded = false;
function GetInitialState() {
g_controlHasLoaded = true;
return g_initialState;
}
function LoadContent(state) {
try {
YAHOO.util.History.navigate("q", state.toString());
} catch (e) {
_LoadContent(state);
}
}
function _LoadContent(state) {
if (g_controlHasLoaded) {
var ctrl = document.getElementById("silverlightControl");
ctrl.Content.jsBridge.LoadState(state);
} else
g_initialState = state;
}
function _Init() {
var state = YAHOO.util.History.getCurrentState("q");
if (typeof (state) == "string") {
if (g_controlHasLoaded)
_LoadContent(state);
else
g_initialState = state;
}
}
YAHOO.util.Event.onDOMReady(new function() {
var bookmarkedState = YAHOO.util.History.getBookmarkedState("q");
var queryState = YAHOO.util.History.getQueryStringParameter("q");
var initialState = bookmarkedState || queryState || "";
YAHOO.util.History.register("q", initialState, function(state) {
_LoadContent(state);
});
YAHOO.util.History.onReady(function() {
_Init();
});
try {
YAHOO.util.History.initialize("yui-history-field", "yui-history-iframe");
} catch (e) {
_Init();
}
});
When the control wants to register a state transition, it calls the LoadContent
JavaScript function. This will store the state in the IFRAME
, and trigger a callback on the control, which the control then uses to transition to that state.
internal void NavigateToState(string state)
{
HtmlPage.Window.Invoke("LoadContent", state);
}
That's all there is to it. When running the sample application, you should make sure that you set "Default.aspx" as the start page, not the auto-generated HTML test page (which doesn't have the required JavaScript).
You'll notice that you can use the browser's Back and Forwards buttons to navigate between control states. You'll also be able to set the state via the query string. For this reason, you should always validate the incoming state to make sure it is valid.
History
- 11 May 2009: First version.