Create a Snapshot of your data in Silverlight

September 24, 2008

I’ve been working on some Silverlight projects with a designer and in a word – it’s been fantastic. Alex, my brother, and I have been learning Silverlight 2 together. It’s been a great way to figure out what works best in our little Silverlight
team.

Silverlight separates concerns between me, the developer and the designer very well. I love being able to throw a design issue over to Alex to fix. He does a “get latest”, runs it up and fixes the issue/makes the app look _not_ crap.

This is all good, until the application gets a little more complex. I add WCF services, databases and other technical dependencies which require some level of set up. Alex is quite technical, but some of these things sit outside the scope of his professional experience. This leads to a problem where we sit on the phone for an hour trying to figure out why it’s not working. “Works on my machine” seal doesn’t apply here unfortunately.

So what do you do?

There are a range of ways to fix a problem like this. One way is to use dummy test data. Put the application in to “dummy” mode and load the data from another source rather than your WCF service etc.

This means that your designers can press F5 and have the application “just work” – a pretty ideal situation you have to admit. This solution is also great for sales demonstrations – keeping a sales person’s notebook up to date with the latest website version, WCF, database etc is a PITA :) I don’t know how many times I’ve been caught before a big demo fixing the demo notebook because it has some arcane issue that never seems to occur on any other notebook I’ve ever used.

The problem then becomes – how to you get _good_ dummy data? You really don’t want to be creating this by hand… Why not set your model classes up in a way that you can take a snapshot of them and re-load it in dummy mode?

This is actually pretty easy to achieve by using the DataContractSerializer class which is built in to Silverlight.

The process overview is:

  • Design model classes in a way that supports DataContractSerialization
  • Apply DataContract attributes to the classes
  • Serialise the object graph to ISO storage
  • Find the file on the hard drive and copy in to Silverlight project
  • Add a setting to place the project in to “dummy” mode
  • Load the serialised object from the XAP (it will be compiled in)
  • Enjoy a problem free relationship with your designer :)

<Sample Code>

Serialise your Silverlight Object Graph Example

</Sample Code>

Preparing for Serialisation

There are a couple of simple rules when performing serialisation in Silverlight using DataContractSerializer.

  • In partial trust (which Silverlight runs in) you can only serialise public fields
  • It’s easiest to serialise object graphs which do not have circular dependencies

With these two rules in mind let’s smash together some code.

  • Create a new Silverlight project
  • Create some simple demonstration model classes
  • Create Serialize and Deserialize methods
  • Manually populate with test data (normally this would be some real database data)
  • Serialize data, get file, copy to project and set up for deserialization

When creating a serializable data model ensure you have one starting point. That is a main object which is the root of the object graph. Ensure that you have no circular dependencies.

  • Create two simple classes called ModelMainClass and SomeOtherClass
  • Add a couple of simple properties to ModelMainClass
  • Add a couple of simple properties to SomeOtherClass
  • Add a reference to System.Runtime.Serialization
  • Decorate each class with [DataContract] attribute
  • Decorate the properties and member variables in each class you want to serialise with the [DataMember] attribute
  • Add a List property to ModelMainClass to hold references to instances of SomeOtherClass. Ensure it is also marked with the DataMemberAttribute


[DataContract]
public class ModelMainClass
{
	[DataMember]
	public string SomeProperty
	{
		get;
		set;
	}

	[DataMember]
	public long SomeValue
	{
		get;
		set;
	}
	[DataMember]
	public List<SomeOtherClass> SomeOtherClassInstances
	{
		get;
		set;
	}
}

[DataContract]
public class SomeOtherClass
{
	[DataMember]
	public string SomeOtherClassValue
	{
		get;
		set;
	}
}

Get the data in to isolated storage

Next up we need to create the serialisation and deserialisation class.

Create a new file called Persistence.cs

This file will have the following functionality

  • A method to serialise to a byte array
  • A method to deserialise from a byte array
  • A method to write a byte array to isolated storage
  • For posterity a method to read a byte array from isolated storage
  • A method to read a byte array from the XAP file

const string PERSISTFILENAME = "persist.dat";

static List<Type> knownTypes = null;

public static void ISOSave(ModelMainClass mmc)
{
	try
	{
		MemoryStream ms = new MemoryStream();

		DataContractSerializer ser = new DataContractSerializer(mmc.GetType(), getKnownTypes());

		ser.WriteObject(ms, mmc);

		byte[] data = ms.ToArray();
		commit(data);
	}
	catch (Exception ex)
	{
		Console.WriteLine("Argh");
	}
}

static void commit(byte[] data)
{
	using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
	{
		IsolatedStorageFileStream isfs = new IsolatedStorageFileStream(PERSISTFILENAME, FileMode.Create, isf);

		isfs.Write(data, 0, data.Length);

		isfs.Close();
	}
}

static byte[] load(bool dummyData)
{
	if (dummyData)
	{
		StreamResourceInfo s = Application.GetResourceStream(new Uri("MockData/persisted.dat", UriKind.Relative));

		byte[] result = new byte[s.Stream.Length];
		s.Stream.Read(result, 0, result.Length);
		s.Stream.Close();
		return result;
	}
	else
	{
		using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
		{
			if (isf.FileExists(PERSISTFILENAME))
			{
				IsolatedStorageFileStream isfs = new IsolatedStorageFileStream(PERSISTFILENAME, FileMode.Open, isf);

				byte[] result = new byte[isfs.Length];
				isfs.Read(result, 0, Convert.ToInt32(isfs.Length));
				isfs.Close();
				return result;
			}
			else
			{
				return null;
			}
		}
	}
}

public static ModelMainClass ISOLoad(bool dummyData)
{
	byte[] data = load(dummyData);
	
	if (data == null)
	{
		return new ModelMainClass();
	}
	else
	{
		DataContractSerializer ser = new DataContractSerializer(typeof(ModelMainClass), getKnownTypes());
		MemoryStream ms = new MemoryStream(data);
	   
		ModelMainClass mmc = ser.ReadObject(ms) as ModelMainClass;
		return mmc;
	}

}

private static List<Type> getKnownTypes()
{
	if (knownTypes == null)
	{
		knownTypes = new List<Type>();
		knownTypes.Add(typeof(SomeOtherClass));
		knownTypes.Add(typeof(ModelMainClass));               
	}
	return knownTypes;

}

Note here the Load method has the ability to load data from isolated storage and from the dummy data in the XAP. This is for posterity and completeness :)

Generate some test data

In Page.xaml create a button to create and save the test data. In the button’s Click even drop in the following code:


ModelMainClass mmc = new ModelMainClass();

mmc.SomeProperty = string.Format("This is a test {0}", DateTime.Now.ToString());
mmc.SomeValue = DateTime.Now.Ticks;

List<SomeOtherClass> list = new List<SomeOtherClass>();

for (int i = 0; i < 100; i++)
{
	list.Add(new SomeOtherClass() { SomeOtherClassValue = i.ToString() });
}

mmc.SomeOtherClassInstances = list;

Persistence.ISOSave(mmc);

Pretend this code is getting some stuff from your WCF service etc. Serialise your completed object graph here.

Note the call to Persistence.ISOSave which is our new method…

Find the data in isolated storage

Silverlight keeps its isolated storage underneath the user’s data folder.

On Vista look under here: C:\Users\<user>\AppData\LocalLow\Microsoft\Silverlight\is
On XP look under here: C:\Documents and Settings\<user>\Local Settings\Application Data\Microsoft\Silverlight\is

You will need to have a bit of a hunt around in these folders to find what you are looking for (search doesn’t seem to work). For example on my machine the file was located here:

C:\Users\jordank\AppData\LocalLow\Microsoft\Silverlight\is\tuigc5pi.rqh\xhfonuik.kdv\1\s\rqotqem0g1s0rro4dxwbnvcvci50t01b4knyscivinuy3ejenraaaeda\f.

Nice.

Copy this file in to your Silverlight project, and set it’s Build Action to “Content”. Setting this to content allows the Persistence class to read the data using the following code:

StreamResourceInfo s = Application.GetResourceStream(new Uri("MockData/persisted.dat", UriKind.Relative));

Finishing off

Back in Page.xaml, create another button to load the data, and a TextBlock to proove the load worked. In the click event for the button drop the following code:

ModelMainClass mmc = Persistence.ISOLoad(true);
foreach (SomeOtherClass soc in mmc.SomeOtherClassInstances)
{
	SomeOutput.Text += soc.SomeOtherClassValue;
}

Too easy! You just loaded your snapshot from the XAP file!

You can see how easy it would be to build complex dummy data object graphs and distribute them with your application.

And everyone lives happily ever after


Screen Cast: Control Silverlight by Using Browser Back and Foward Buttons

September 10, 2008

I’ve uploaded a companion screen cast for my Control Silverlight by Using Browser Back and Foward Buttons post.


Control Silverlight by Using Browser Back and Forward Buttons

September 8, 2008

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<HistoryEventArgs> 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>
/// <param name="sender">The JavaScript object that is raising the event (will be managed ScriptObject at this point)</param>
/// <param name="args">The data that was passed as part of the navigation event (in this case our "DataID")</param>
[ScriptableMember()]
public void LoadPoint(object sender, object args)
{
	if (HistoryChanged != null && 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>
/// <param name="sender">The instantiated JavaScript class</param>
[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>
/// <param name="dataId">The internal Silverlight "DataID" - i.e. the piece of data to store</param>
/// <param name="title">The page title to set (which will show up in the history of the browser)</param>
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.

<asp:Silverlight ID="Xaml1" runat="server" Source="~/ClientBin/SomeApp.xap" MinimumVersion="2.0.30523" Width="100%" Height="100%" OnPluginLoaded="slLoad"/>

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<HistoryEventArgs>(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>
/// <param name="sender"></param>
/// <param name="e"></param>
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.


Follow

Get every new post delivered to your Inbox.