Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Silverlight Unsaved Data Detection

0.00/5 (No votes)
4 Oct 2010 1  
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 = "JohnDoe@Whitehouse.gov";
    }
    
    // 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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here