Silverlight Unsaved Data Detection






4.84/5 (16 votes)
Detect that a user has un-saved changes and popup a box that allows them to stop navigating away from the page (using ViewModel / MVVM)

Protect Your Users From Losing Un-Saved Changes
Live example: http://silverlight.adefwebserver.com/UnsavedDataDetection
One of the nice things about using Silverlight for business applications is that the users can enter a lot of information and not worry about the page "timing out". However, if they enter a lot of information and they accidentally navigate away from the page, or they accidentally close the web browser, they will lose any un-saved changes.
This article describes a way to pop up a box, that gives the user an opportunity to save any un-saved changes.
The Sample Application

When you load the application, you see the sample information. The Save button is disabled, and the ISDirty checkbox is un-checked.

If you make a change and hit the Tab key, the Save button is now enabled, and the ISDirty checkbox is now checked.

If you try to navigate away from the page while the form is "Dirty", you will see a Popup that indicates the number of un-saved changes, and asks if you want to continue leaving the page, or if you want to stay and fix any un-saved changes.

If you click the Save button, the Save button will be disabled, and the ISDirty
checkbox will be un-checked.
You will now be able to navigate away from the page, or close the web browser, and you will not see any warnings.
How LightSwitch Does it
The Microsoft LightSwitch program has this functionality built-in. This is the JavaScript that is used:
function checkDirty(e) {
var needConform = false;
var message = 'You may lose all unsaved data in the application.'; // default message
var silverlightControl = document.getElementById("SilverlightApplication").Content;
if (silverlightControl) {
var applicationState = silverlightControl.ApplicationState;
if (applicationState) {
if (applicationState.IsDirty) {
needConform = true;
message = applicationState.Message;
}
}
else {
needConform = true;
}
}
if (needConform) {
if (!e) e = window.event;
e.returnValue = message;
// IE
e.cancelBubble = true;
//e.stopPropagation works in Firefox.
if (e.stopPropagation) {
e.stopPropagation();
e.preventDefault();
}
// Chrome
return message;
}
}
window.onbeforeunload = checkDirty;
I was surprised because this is all that it uses. Everything else is buried inside the LightSwitch
program, and Microsoft is not sharing any of the code. I decided to make my version work using their JavaScript because I figure they spent a lot of money on the best and the brightest people to write it.
There is a surprisingly lack of information on how to do this. I was only able to find one example by Daniel Vaughan, Calling Web Services from Silverlight as the Browser is Closed, that pops up the box like LightSwitch
does. However, his example goes into a lot more, such as calling a web service, that I still needed to create my own implementation. However, his example did show me how it is done.
The ApplicationState Class
The basic functionality that I need to implement is:
- Detect when property has changed (it is Dirty)
- Detect when a property has changed back to the original value (it is no longer Dirty)
- Allow all properties to be reset to not Dirty (for example when the Save button is pressed)
Here is the class that does that:
namespace UnsavedDataDetection
{
public class ApplicationState
{
// Properties
#region IsDirty
[ScriptableMember]
public bool IsDirty
{
get
{
// Return bool if there are Dirty Elements
return (Elements.Where(x => x.IsDirty == true).Count() > 0);
}
}
#endregion
#region Message
[ScriptableMember]
public string Message
{
get
{
// Return a message indicating how many Dirty Elements there are
return string.Format("There are {0} unsaved changes",
Elements.Where(x => x.IsDirty == true).Count().ToString());
}
}
#endregion
// Methods
#region AddElement
public void AddElement(ApplicationElement paramElementName)
{
// Do we already have the Element?
var CurrentElement = (from Element in Elements
where Element.ElementKey == paramElementName.ElementKey
select Element).FirstOrDefault();
if (CurrentElement == null)
{
// Ensure that the Element has been marked not Dirty
paramElementName.IsDirty = false;
// Set the Initial Value
paramElementName.ElementInitialValue =
paramElementName.ElementCurrentValue;
// Add the element
Elements.Add(paramElementName);
}
else
{
// Update the element
CurrentElement.ElementCurrentValue =
paramElementName.ElementCurrentValue;
// Set IsDirty
CurrentElement.IsDirty = (CurrentElement.ElementCurrentValue
!= CurrentElement.ElementInitialValue);
}
}
#endregion
#region ClearIsDirty
public void ClearIsDirty()
{
// Clear all the ISDirty flags
foreach (var item in Elements)
{
item.ElementInitialValue = item.ElementCurrentValue;
item.IsDirty = false;
}
}
#endregion
// Collections
#region Elements
private List<ApplicationElement> _Elements = new List<ApplicationElement>();
public List<ApplicationElement> Elements
{
get { return _Elements; }
set
{
if (Elements == value)
{
return;
}
_Elements = value;
}
}
#endregion
}
#region ApplicationElement
public class ApplicationElement
{
public string ElementKey { get; set; }
public string ElementName { get; set; }
public string ElementCurrentValue { get; set; }
public string ElementInitialValue { get; set; }
public bool IsDirty { get; set; }
}
#endregion
}
Note that some of the properties are marked, [ScriptableMember]
, so that they can be called by the JavaScript.
Registering It With the Application
The ApplicationState
class needs to be instantiated and invoked on the application level. We open the App.xaml.cs file, and add the following code:
#region ApplicationState
private ApplicationState _objApplicationState = new ApplicationState();
public ApplicationState objApplicationState
{
get { return _objApplicationState; }
set
{
if (objApplicationState == value)
{
return;
}
_objApplicationState = value;
}
}
#endregion
We also add this to the constructor of the application class:
HtmlPage.RegisterScriptableObject("ApplicationState", objApplicationState);
This allows the JavaScript to access the IsDirty
and Message
properties in the ApplicationState
class.
The Implementation
The final step is to implement the functionality in each page of the application. Essentially, we need to register any properties that change with the ApplicationState
class and it will do the rest of the work.
First, we start off with a basic ViewModel
:
public class HomeViewModel : INotifyPropertyChanged
{
public HomeViewModel()
{
// Set default values
FullName = "John Doe";
Email = "[email protected]";
}
// Properties
#region IsDirty
private bool _IsDirty;
public bool IsDirty
{
get { return _IsDirty; }
set
{
if (IsDirty == value)
{
return;
}
_IsDirty = value;
this.NotifyPropertyChanged("IsDirty");
}
}
#endregion
#region FullName
private string _FullName;
public string FullName
{
get { return _FullName; }
set
{
if (FullName == value)
{
return;
}
_FullName = value;
this.NotifyPropertyChanged("FullName");
}
}
#endregion
#region Email
private string _Email;
public string Email
{
get { return _Email; }
set
{
if (Email == value)
{
return;
}
_Email = value;
this.NotifyPropertyChanged("Email");
}
}
#endregion
// Utility
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#endregion
}
We add a PropertyChanged handler to the constructor that will fire whenever any property is changed:
// Wire-up property changed event handler
PropertyChanged += new PropertyChangedEventHandler(HomeViewModel_PropertyChanged);
The implementation of the method is as follows:
#region HomeViewModel_PropertyChanged
void HomeViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// Run this method for any property other than the IsDirty property
// otherwise you will be in in infinite loop
if (e.PropertyName != "IsDirty")
{
// Create a new ApplicationElement
ApplicationElement objApplicationElement = new ApplicationElement();
objApplicationElement.ElementKey =
string.Format("HomeViewModel_{0}", e.PropertyName);
objApplicationElement.ElementName = e.PropertyName;
// Set ElementCurrentValue
PropertyInfo pi = this.GetType().GetProperty(e.PropertyName);
objApplicationElement.ElementCurrentValue =
Convert.ToString(pi.GetValue(this, null));
// Get an instance of the App class
App AppObj = (App)App.Current;
// Add the ApplicationElement to the objApplicationState object
AppObj.objApplicationState.AddElement(objApplicationElement);
// Set IsDirty
IsDirty = (AppObj.objApplicationState.Elements.Where
(x => x.IsDirty == true).Count() > 0);
}
}
#endregion
Note that the ElementKey
is using "HomeViewModel_{0}
". You can replace "HomeViewModel
" with the name of the current page to easily keep track of multiple pages.
We also add this Save
command that will clear all the IsDirty
flags:
#region SaveCommand
public ICommand SaveCommand { get; set; }
public void Save(object param)
{
// Clear IsDirty Flag
// (normally you would actually perform a save first)
// Get an instance of the App class
App AppObj = (App)App.Current;
// Clear all the ISDirty flags
AppObj.objApplicationState.ClearIsDirty();
// Set IsDirty on this class
IsDirty = false;
}
private bool CanSave(object param)
{
// Only enable if form is Dirty
return (IsDirty);
}
#endregion
The User Interface (The View)

The diagram above shows how the UI is bound to the ViewModel
.
Collections (DataGrid)
This does not handle collections. When using a control like the DataGrid
, it automatically tracks when the DataGrid
is Dirty. I would hook into that property rather than trying to track changes in the DataGrid
using the ApplicationState
class.
Further Reading
- Calling Web Services from Silverlight as the Browser is Closed
http://danielvaughan.orpius.com/category/WCF.aspx - Communicating between JavaScript & Silverlight
http://blogs.silverlight.net/blogs/msnow/archive/2008/07/08/tip-of-the-day-15-communicating-between-javascript-amp-silverlight.aspx - Clean shutdown in Silverlight and WPF applications
http://blog.galasoft.ch/archive/2009/10/18/clean-shutdown-in-silverlight-and-wpf-applications.aspx