Click here to Skip to main content
Click here to Skip to main content

Tagged as

Two Simple Approaches to WinForms Dirty Tracking

, 4 Aug 2009
Rate this:
Please Sign up or sign in to vote.
This article describes two approaches for tracking whether values in input controls on a form have changed since a previous save, coded to be reusable across forms.

Introduction

A common requirement for WinForms applications is to track whether or not a user has changed document values since a previous save. If a value has changed, the form is considered “dirty”; when the user attempts to close a dirty form, it is desirable to prompt the user to save changes first. This article describes two simple approaches to “dirty tracking” that are easy to code and reusable across forms and projects.

Approach #1: Handle the “changed” events for input controls

To conceptualize this approach, consider what may be done within a form to track changes in input controls. You might establish a form-level member variable called _isDirty and set it to true when an input control fires its appropriate Changed event. For example, if the form has a TextBox control called textBox1, the code to signal a change could look like this:

public partial class Form1 : Form
{
    // our variable for tracking the "dirty" status of the form
    private bool _isDirty = false;

    ...

    // handler for the TextChanged event of textBox1
    private void textBox1_TextChanged(object sender, EventArgs e)
    {
        _isDirty = true;
    }
    ...
}

Whatever method the form uses to save the current document would set the dirty flag to false upon successful saving:

public partial class Form1 : Form
{
    ...
    private void SaveMyDocument()
    {
        // ... do whatever to save the file ...

        // and indicate that the form is now "clean"
        _isDirty = false;
    }

   ...

}

Then, the FormClosing event for the form is handled to check the dirty flag and prompt to save if necessary:

public partial class Form1 : Form
{
    ...

    private void Form1_FormClosing(object sender, FormClosingEventArgs e)
    {
        // upon closing, check if the form is dirty; if so, prompt
        // to save changes
        if (_isDirty)
        {
            DialogResult result
                = (MessageBox.Show(
                   "Would you like to save changes before closing?"
                   , "Save Changes"
                   , MessageBoxButtons.YesNoCancel
                   , MessageBoxIcon.Question));

            switch (result)
            {
                case DialogResult.Yes:
                    // save the document
                    SaveMyDocument();
                    break;

                case DialogResult.No:
                    // just allow the form to close
                    // without saving
                    break;

                case DialogResult.Cancel:
                    // cancel the close
                    e.Cancel = true;
                    break;
            }
        }
    }
    ...
}

The approach is very simple, but one would not want to code this way. Manually handling each input control’s xxxChanged event is cumbersome, and the solution is not reusable across forms, let alone projects.

It doesn’t take much, however, to improve on this approach, making it reusable across forms and less tedious to use. The same functionality may be coded in its own class that can be instantiated within a form; in other words, the form offloads its dirty tracking to a helper class.

To begin, we’ll define our helper class with an _isDirty flag and expose it as a property along with methods to force a dirty or clean state (the latter used when saving changes):

public class SimpleDirtyTracker
{
  private Form _frmTracked;  
  private bool _isDirty;

  // property denoting whether the tracked form is clean or dirty
  public bool IsDirty
  {
      get { return _isDirty; }
      set { _isDirty = value; }
  }

  // methods to make dirty or clean
  public void SetAsDirty()
  {
      _isDirty = true;
  }

  public void SetAsClean()
  {
      _isDirty = false;
  }

  ...
  
}

We’ll then establish event handlers for all input control types we wish to support. In this example, we’re only supporting TextBox and CheckBox controls, but it would be simple to add additional types.

public class SimpleDirtyTracker
{
  ...
  
  // event handlers
  private void SimpleDirtyTracker_TextChanged(object sender, EventArgs e)
  {
      _isDirty = true;
  }

  private void SimpleDirtyTracker_CheckedChanged(object sender, EventArgs e)
  {
      _isDirty = true;
  }

  ...
}

Next, we will add a method to loop through the controls in a ControlCollection, determine if the control is of a supported type, and assign the appropriate event handler if so. If a control in the collection has child controls of its own, the method is recursively called on the child collection.

public class SimpleDirtyTracker
{
  ...
  
  // recursive routine to inspect each control and assign handlers accordingly
  private void AssignHandlersForControlCollection(
       Control.ControlCollection coll)
  {
      foreach (Control c in coll)
      {
          if (c is TextBox)
              (c as TextBox).TextChanged 
                += new EventHandler(SimpleDirtyTracker_TextChanged);

          if (c is CheckBox)
              (c as CheckBox).CheckedChanged 
                += new EventHandler(SimpleDirtyTracker_CheckedChanged);

          // ... apply for other desired input types similarly ...

          // recurively apply to inner collections
          if (c.HasChildren)
              AssignHandlersForControlCollection(c.Controls);
      }
  }

  ... 
}

Finally, we’ll add a constructor that defines the form to track as an argument. The constructor starts the recursive assignment of handlers on the form’s Controls collection.

public class SimpleDirtyTracker
{
  ...
  
  // initialize in the constructor by assigning event handlers
  public SimpleDirtyTracker(Form frm)
  {
      _frmTracked = frm;
      AssignHandlersForControlCollection(frm.Controls);
  }
  
  ...
}

That’s it! Our simple approach is now usable in any form with minimal code. We simply instantiate the tracker in the form’s Load event:

public partial class Form1 : Form
{
    // our object for tracking the "dirty" status of the form
    private SimpleDirtyTracker _dirtyTracker;

    ...
   
    private void Form1_Load(object sender, EventArgs e)
    {
        // in the Load event initialize our tracking object
        _dirtyTracker = new SimpleDirtyTracker(this);
        _dirtyTracker.SetAsClean();
    }

    ...
}

Then, in the form’s FormClosing event, check the status of _dirtyTracker.IsDirty to see if the user should be prompted to save changes:

public partial class Form1 : Form
{
    ...

    private void Form1_FormClosing(object sender, FormClosingEventArgs e)
    {
        // upon closing, check if the form is dirty; if so, prompt
        // to save changes
        if (_dirtyTracker.IsDirty)
        {
            DialogResult result
                = (MessageBox.Show(
                    "Would you like to save changes before closing?"
                    , "Save Changes"
                    , MessageBoxButtons.YesNoCancel
                    , MessageBoxIcon.Question));

            switch (result)
            {
                case DialogResult.Yes:
                    // save the document
                    SaveMyDocument();
                    break;

                case DialogResult.No:
                    // just allow the form to close
                    // without saving
                    break;

                case DialogResult.Cancel:
                    // cancel the close
                    e.Cancel = true;
                    break;
            }
        }
    }
    ...
}

Approach #2: Track the “clean” values of controls

The previous approach works and is simple. It is, however, prone to false positives when considering a form dirty. For example, a user may check a CheckBox, change his or her mind, and uncheck it again. Our previous code would recognize the CheckedChanged event as triggered – twice! – and flag IsDirty as true. A (slightly) more sophisticated approach would recognize that, should the user close the form at this point, the value of the checkbox hasn’t actually changed since the last document save and therefore the user should not be prompted to save.

We can still code our dirty-tracking code in its own class, but now, instead of responding to a control’s xxxChanged event, we will need to track the control’s value as of the last save. We’ll call this the control’s “clean” value. When it is time to check if changes have been made to the form, we’ll compare the control’s current value to its remembered “clean” value; if the two are different, the control is considered “dirty” and thus, so is the form.

The bulk of the work will be handled in a class called ControlDirtyTracker. We’ll start its definition with a property for the tracked control and one to remember its clean value. For the purpose of this illustration, we will support TextBox, CheckBox, ComboBox, and ListBox controls. We’ll also define a static method to return whether a type for a given control is supported.

public class ControlDirtyTracker
{
    private Control _control;
    private string _cleanValue;

    // read only properties
    public Control Control { get { return _control; } }
    public string CleanValue { get { return _cleanValue; } }              

    // static class utility method; return whether or not the control type 
    // of the given control is supported by this class;
    // developers may modify this to extend support for other types
    public static bool IsControlTypeSupported(Control ctl)
    {
        // list of types supported
        if (ctl is TextBox) return true;
        if (ctl is CheckBox) return true;
        if (ctl is ComboBox) return true;
        if (ctl is ListBox) return true;

        // ... add additional types as desired ...

        // not a supported type
        return false;
    }

    ...
}

We then need a method to return the current value of the tracked control. For the sake of simplicity in comparison, we’ll standardize on a string value. Again, this method may be extended at the discretion of the developer to support additional types not present in the sample.

public class ControlDirtyTracker
{
    ...
 
    // private method to determine the current value (as a string) 
    // of the control;
    // developers may modify this to extend support for other types
    private string GetControlCurrentValue()
    {
        if (_control is TextBox)
            return (_control as TextBox).Text;

        if (_control is CheckBox)
            return (_control as CheckBox).Checked.ToString();

        if (_control is ComboBox)
            return (_control as ComboBox).Text;

        if (_control is ListBox)
        {
            // for a listbox, create a list of the selected indexes
            StringBuilder val = new StringBuilder();
            ListBox lb = (_control as ListBox);
            ListBox.SelectedIndexCollection coll = lb.SelectedIndices;
            for (int i = 0; i < coll.Count; i++)
                val.AppendFormat("{0};", coll[i]);

            return val.ToString();
        }

        // ... add additional types as desired ...

        return "";
    }

    ...
}

We code the constructor to pass the tracked control as an argument, capturing its current value as the clean value.

public class ControlDirtyTracker
{
    ...
 
    // constructor establishes the control and uses its current 
    // value as "clean"
    public ControlDirtyTracker(Control ctl)
    {            
        // if the control type is not one that is supported, 
        // throw an exception
        if (ControlDirtyTracker.IsControlTypeSupported(ctl))
        {
            _control = ctl;
            _cleanValue = GetControlCurrentValue();
        }
        else
          throw new NotSupportedException(
                string.Format(
                 "The control type for '{0}' "
                   + "is not supported by the ControlDirtyTracker class."
                  , ctl.Name)
                );
    }

    ...
}

Finally, our ControlDirtyTracker will expose two methods: one to establish the current control value as clean (to be called when saving the document), and one that tests if the control is dirty.

public class ControlDirtyTracker
{
    ...
    
    // method to establish the the current control value as "clean"
    public void EstablishValueAsClean()
    {
        _cleanValue = GetControlCurrentValue();
    }


    // determine if the current control value is considered "dirty"; 
    // i.e. if the current control value is different than the one
    // remembered as "clean"
    public bool DetermineIfDirty()
    {
        // compare the remembered "clean value" to the current value;
        // if they are the same, the control is still clean;
        // if they are different, the control is considered dirty.
        return (
          string.Compare(
            _cleanValue, GetControlCurrentValue(), false
            ) != 0
        );
    }

    ...
}

Most of the work for this approach is now done. As we will be tracking multiple controls, we’ll create a ControlDirtyTrackerCollection class. It is copied here in its entirety, with utility methods to add controls from a form, list all controls that are dirty in the collection, and mark all controls in the collection as clean.

public class ControlDirtyTrackerCollection: List<ControlDirtyTracker>
{

    // constructors
    public ControlDirtyTrackerCollection() : base() { }
    public ControlDirtyTrackerCollection(Form frm) : base() 
    { 
        // initialize to the controls on the passed in form
        AddControlsFromForm(frm); 
    }


    // utility method to add the controls from a Form to this collection
    public void AddControlsFromForm(Form frm)
    {
        AddControlsFromCollection(frm.Controls);
    }


    // recursive routine to inspect each control and add to the 
    // collection accordingly
    public void AddControlsFromCollection(Control.ControlCollection coll)
    {
        foreach (Control c in coll)
        {
            // if the control is supported for dirty tracking, add it
            if (ControlDirtyTracker.IsControlTypeSupported(c))
                this.Add(new ControlDirtyTracker(c));

            // recurively apply to inner collections
            if (c.HasChildren)
                AddControlsFromCollection(c.Controls);
        }
    }

    // loop through all controls and return a list of those that are dirty
    public List<Control> GetListOfDirtyControls()
    {
        List<Control> list = new List<Control>();

        foreach (ControlDirtyTracker c in this)
        {
            if (c.DetermineIfDirty())
                list.Add(c.Control);
        }

        return list;
    }


    // mark all the tracked controls as clean
    public void MarkAllControlsAsClean()
    {
        foreach (ControlDirtyTracker c in this)
            c.EstablishValueAsClean();
    }
}

We could be done at this point with approach #2. To use on our form, we would simply instantiate a ControlDirtyTrackerCollection object. We could also wrap the collection object in a class that tracks the form and exposes the classic IsDirty property, which is what I have done below:

public class SlightlyMoreSophisticatedDirtyTracker
{
  private Form _frmTracked;
  private ControlDirtyTrackerCollection _controlsTracked;

  // property denoting whether the tracked form is clean or dirty;
  // used if the full list of dirty controls isn't necessary
  public bool IsDirty
  {
      get 
      {
          List<Control> dirtyControls
              = _controlsTracked.GetListOfDirtyControls();

          return (dirtyControls.Count > 0);
      }
  }


  // public method for accessing the list of currently
  // "dirty" controls
  public List<Control> GetListOfDirtyControls()
  {
      return _controlsTracked.GetListOfDirtyControls();
  }

  
  // establish the form as "clean" with whatever current
  // control values exist
  public void MarkAsClean()
  {
      _controlsTracked.MarkAllControlsAsClean();
  }


  // initialize in the constructor by assigning controls to track
  public SlightlyMoreSophisticatedDirtyTracker(Form frm)
  {
      _frmTracked = frm;
      _controlsTracked = new ControlDirtyTrackerCollection(frm);
  }
}

To apply this SlightlyMoreSophisticatedDirtyTracker on the form is as easy as as it was with our previous SimpleDirtyTracker. The sample download contains a project demonstrating its use.

Summary

Tracking whether or not a user has changed document values is a common requirement for WinForms applications, but fortunately, one that doesn’t have to be difficult to code. For a simple approach, one may assign handlers to the appropriate Changed events on the form’s input controls. For a slightly more sophisticated approach, one may track the values of controls and compare against a remembered “clean” value. Either approach may be encapsulated in a helper class, making it reusable across forms and projects and simple to apply.

Acknowledgements

The inspiration for this article came from a CodeProject forum question[^] and was initially addressed in parts 1[^] and 2[^] on my blog: www.MishaInTheCloud.com[^].

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Mike Ellison

United States United States
I work for the University of Nevada, Las Vegas in the Office of Institutional Analysis and Planning. Among other things, our office is charged with the mission of deriving useful information in support of administrative decision-making from institutional data. Within the context of that mission, my office mates and I apply technology in the form of custom data processing applications, data extraction and analysis tools, reporting tools, relational databases, OLAP solutions, data warehousing, and data mining.
 
Visit my blog at MishaInTheCloud.com


Comments and Discussions

 
QuestionTestSimpleDirtyTracker without Button click Pinmemberschalla12328-Oct-13 5:27 
QuestionNumericUpDown wierdness. Pinmemberkommi18-Feb-11 9:24 
GeneralFantastic arcticle for Beginners Pinmembersenthil7316-Feb-11 9:10 
GeneralNice Job. Pinmemberjoelcarroll10-Aug-09 13:40 
GeneralRe: Nice Job. PinmemberMike Ellison10-Aug-09 14:09 
GeneralChanging a value and changing it back... Pinmembersupercat95-Aug-09 4:50 
GeneralRe: Changing a value and changing it back... PinmemberMike Ellison5-Aug-09 6:33 
GeneralI am not sure about this for the following reasons PinmvpSacha Barber4-Aug-09 21:49 
GeneralRe: I am not sure about this for the following reasons PinmemberMike Ellison5-Aug-09 2:55 
GeneralRe: I am not sure about this for the following reasons PinmvpSacha Barber5-Aug-09 3:20 
GeneralRe: I am not sure about this for the following reasons PinmemberMike Ellison5-Aug-09 3:36 

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 | Mobile
Web01 | 2.8.140827.1 | Last Updated 4 Aug 2009
Article Copyright 2009 by Mike Ellison
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid