Click here to Skip to main content
11,492,639 members (59,446 online)
Click here to Skip to main content

Silverlight Unsaved Data Detection

, 4 Oct 2010 Ms-PL 45.3K 387 19
Rate this:
Please Sign up or sign in to vote.
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, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)

Share

About the Author

defwebserver
Software Developer (Senior) http://ADefWebserver.com
United States United States
Michael Washington is a Microsoft MVP. He is a ASP.NET and
C# programmer.
He is the founder of
LightSwitchHelpWebsite.com

He has a son, Zachary and resides in Los Angeles with his wife Valerie.

He is the Author of:
Follow on   Twitter

Comments and Discussions

 
GeneralGreat Article Pin
technette4-Nov-10 17:05
membertechnette4-Nov-10 17:05 
GeneralRe: Great Article Pin
defwebserver4-Nov-10 17:57
memberdefwebserver4-Nov-10 17:57 
GeneralMy vote of 5 Pin
Member 33316213-Oct-10 4:46
memberMember 33316213-Oct-10 4:46 
GeneralRe: My vote of 5 Pin
defwebserver21-Oct-10 21:26
memberdefwebserver21-Oct-10 21:26 
GeneralDoesn't seem to work Pin
petervds1238-Oct-10 3:35
memberpetervds1238-Oct-10 3:35 
GeneralRe: Doesn't seem to work Pin
defwebserver8-Oct-10 3:38
memberdefwebserver8-Oct-10 3:38 
GeneralRe: Doesn't seem to work Pin
petervds12311-Oct-10 22:49
memberpetervds12311-Oct-10 22:49 
GeneralRe: Doesn't seem to work Pin
defwebserver12-Oct-10 3:11
memberdefwebserver12-Oct-10 3:11 
GeneralRe: Doesn't seem to work Pin
petervds12312-Oct-10 21:31
memberpetervds12312-Oct-10 21:31 
GeneralRe: Doesn't seem to work Pin
Member 796885016-Jun-11 0:36
memberMember 796885016-Jun-11 0:36 
GeneralMy vote of 5 Pin
Richard Waddell7-Oct-10 17:34
memberRichard Waddell7-Oct-10 17:34 
GeneralRe: My vote of 5 Pin
defwebserver7-Oct-10 21:33
memberdefwebserver7-Oct-10 21:33 
GeneralMy vote of 5 Pin
Marcelo Ricardo de Oliveira7-Oct-10 5:06
memberMarcelo Ricardo de Oliveira7-Oct-10 5:06 
GeneralRe: My vote of 5 Pin
defwebserver7-Oct-10 6:19
memberdefwebserver7-Oct-10 6:19 
GeneralMy vote of 5 Pin
KunalChowdhury6-Oct-10 5:05
mentorKunalChowdhury6-Oct-10 5:05 
GeneralRe: My vote of 5 Pin
defwebserver7-Oct-10 14:51
memberdefwebserver7-Oct-10 14:51 
GeneralExcellent Pin
Daniel Vaughan6-Oct-10 0:59
mvpDaniel Vaughan6-Oct-10 0:59 
GeneralRe: Excellent Pin
okaygoods6-Oct-10 1:07
memberokaygoods6-Oct-10 1:07 
GeneralRe: Excellent Pin
defwebserver6-Oct-10 3:12
memberdefwebserver6-Oct-10 3:12 
GeneralI was looking for something like this... Pin
kackermann6-Oct-10 0:36
memberkackermann6-Oct-10 0:36 
GeneralRe: I was looking for something like this... Pin
defwebserver6-Oct-10 3:18
memberdefwebserver6-Oct-10 3:18 
GeneralMy vote of 5 Pin
mamta_m_d4-Oct-10 6:52
membermamta_m_d4-Oct-10 6:52 
GeneralRe: My vote of 5 Pin
defwebserver4-Oct-10 10:30
memberdefwebserver4-Oct-10 10:30 
GeneralInteresting but.... Pin
Sacha Barber4-Oct-10 3:02
mvpSacha Barber4-Oct-10 3:02 
GeneralRe: Interesting but.... Pin
defwebserver4-Oct-10 3:17
memberdefwebserver4-Oct-10 3:17 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.150520.1 | Last Updated 4 Oct 2010
Article Copyright 2010 by defwebserver
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid