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.';
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;
e.cancelBubble = true;
if (e.stopPropagation) {
e.stopPropagation();
e.preventDefault();
}
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
{
#region IsDirty
[ScriptableMember]
public bool IsDirty
{
get
{
return (Elements.Where(x => x.IsDirty == true).Count() > 0);
}
}
#endregion
#region Message
[ScriptableMember]
public string Message
{
get
{
return string.Format("There are {0} unsaved changes",
Elements.Where(x => x.IsDirty == true).Count().ToString());
}
}
#endregion
#region AddElement
public void AddElement(ApplicationElement paramElementName)
{
var CurrentElement = (from Element in Elements
where Element.ElementKey == paramElementName.ElementKey
select Element).FirstOrDefault();
if (CurrentElement == null)
{
paramElementName.IsDirty = false;
paramElementName.ElementInitialValue =
paramElementName.ElementCurrentValue;
Elements.Add(paramElementName);
}
else
{
CurrentElement.ElementCurrentValue =
paramElementName.ElementCurrentValue;
CurrentElement.IsDirty = (CurrentElement.ElementCurrentValue
!= CurrentElement.ElementInitialValue);
}
}
#endregion
#region ClearIsDirty
public void ClearIsDirty()
{
foreach (var item in Elements)
{
item.ElementInitialValue = item.ElementCurrentValue;
item.IsDirty = false;
}
}
#endregion
#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()
{
FullName = "John Doe";
Email = "JohnDoe@Whitehouse.gov";
}
#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
#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:
PropertyChanged += new PropertyChangedEventHandler(HomeViewModel_PropertyChanged);
The implementation of the method is as follows:
#region HomeViewModel_PropertyChanged
void HomeViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != "IsDirty")
{
ApplicationElement objApplicationElement = new ApplicationElement();
objApplicationElement.ElementKey =
string.Format("HomeViewModel_{0}", e.PropertyName);
objApplicationElement.ElementName = e.PropertyName;
PropertyInfo pi = this.GetType().GetProperty(e.PropertyName);
objApplicationElement.ElementCurrentValue =
Convert.ToString(pi.GetValue(this, null));
App AppObj = (App)App.Current;
AppObj.objApplicationState.AddElement(objApplicationElement);
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)
{
App AppObj = (App)App.Current;
AppObj.objApplicationState.ClearIsDirty();
IsDirty = false;
}
private bool CanSave(object param)
{
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