How to detect if data has changed on a VB.net 2010 windows form

Coming from a long background in VB6 I can get creative with detecting 'isDirty' with key down, mouse down and such. With what I've found on the web, that process from VB6 seems like it is either overly complicated (or under complicated) comparatively for  vb.net.

My form contains a mixture of (all bound to Access tables)  masked text, textbox, Combobox and DateTime picker.

I've searched far and wide and am astounded by the diversity of ideas to process these events; which is why I'm back here.

Questions:
1. What are best (accepted) practices to handle this?
2. Code sample?
SchoolTeacher54Asked:
Who is Participating?
I wear a lot of hats...

"The solutions and answers provided on Experts Exchange have been extremely helpful to me over the last few years. I wear a lot of hats - Developer, Database Administrator, Help Desk, etc., so I know a lot of things but not a lot about one thing. Experts Exchange gives me answers from people who do know a lot about one thing, in a easy to use platform." -Todd S.

Éric MoreauSenior .Net ConsultantCommented:
is your form bound to a business object (a class exposing the properties)? In this case, it would be easy to set a IsDirty flag in all the setters!
Éric MoreauSenior .Net ConsultantCommented:
still if you are having a business object, you can also have it automatically implemented by using PostSharp: http://doc.postsharp.net/inotifypropertychanged
SchoolTeacher54Author Commented:
I would never have thought of that on my own.
I'm reading now!
OWASP Proactive Controls

Learn the most important control and control categories that every architect and developer should include in their projects.

Karrtik IyerSoftware ArchitectCommented:
I would do the below.
1> First define what is meant by dirty, would say a text box which contained ABC, was modified to AB or DE at some point and at later again made as ABC. User did some modification in this text box which might have resulted in some coupled changes in other fields, but later this text box was brought back to original value on load. Do you call this as dirty? Ideally I would not. But it is better to be clear.
2> For each control based on the definition in step 1, I would find out a way whether it is dirty from each control.
3> If there are controls in UI which contains a list of value (like combo or list box or list view) and it is bound to a collection of objects, it would be good to implement as Observable Collection, which gives events when Property is changed (INotifyPropertyChanged) or a collection is changed (INotifyCollectionChanged). (Observable Collection
With this I can find out whether list has been modified (whether it is combo or list box).
4> So whenever a UI control it is bound to a property of class, I can figure it has changed using INotifyPropertyChanged Interface (INotifyPropertyChanged)
5> If I have a data table, I can call GetChanges method on Data Table to see if there are any uncommitted changes, if so GetChanges shall return a non null data table.

I shall see if I can provide you some example when I pick this up tomorrow.

Thanks,
Karrtik
Jacques Bourgeois (James Burger)PresidentCommented:
2 ways that I use, depending on the needs.

-----

There is a way to associate all the controls of a specific type to a single event method.

A variable at the Form level, let's call it IsDirty.

Create a KeyDown event procedure that is associated with no control. You simply create a KeyDown event and remove the Handles at the end of the declaration:
Private Sub Common_KeyDown(sender As Object, e As KeyEventArgs)
      IsDirty = True
End Sub

Open in new window

You can then associate the event with all the types of controls that you want to consider with code like the following. The Load event of the form is a good place:
For Each ctl As Control In Me.Controls
	If TypeOf ctl Is TextBox OrElse TypeOf ctl Is ComboBox Then
		AddHandler ctl.KeyDown, AddressOf Common_KeyDown
   	End If
Next

Open in new window

Adapt as needed for the type of controls you want to consider. Do the same for other events that could trigger your IsDirty condition. If necessary, have a AddHandler for specific controls:

AddHandler DateTimePicker1.KeyDown, AddressOf Common_KeyDown

Adapt to your needs

-----

When I need to be more precise and prevent stuff such as making the thing IsDirty when the user makes a change but then undo it, I go as follow.

Once the form is filled with the original data, I run code like the following, to copy the original value in the Tag property of the controls. Once again, this need to be adjusted to your needs in situations, for instance, where not all the TextBoxes are considered to be candidates in the mechanism.
For Each ctl As Control In Me.Controls
	If TypeOf ctl Is TextBox OrElse TypeOf ctl Is ComboBox Then
		ctl.Tag = ctl.Text
   	End If
Next

DateTimePicker1.Tag = DateTimePicker1.Value

Open in new window

When time comes to detect if a change was made, I simply compare the Tag and Text properties of all the controls that were tagged previously:
For Each ctl As Control In Me.Controls
	If TypeOf ctl Is TextBox OrElse TypeOf ctl Is ComboBox Then
		If ctl.Text <> CStr(ctl.Tag) then
			IsDirty=True
			Exit For
		End If
   	End If
Next

If DateTimePicker1.Value <> CDate(DateTimePicker1.Tag) then IsDirty = True

Open in new window

Experts Exchange Solution brought to you by

Your issues matter to us.

Facing a tech roadblock? Get the help and guidance you need from experienced professionals who care. Ask your question anytime, anywhere, with no hassle.

Start your 7-day free trial
it_saigeDeveloperCommented:
Along the same lines of what James has laid out, you could then use a pair of helper classes to make the implementation more generic and thereby more reusable.

Using http://www.codeproject.com/Articles/38756/Two-Simple-Approaches-to-WinForms-Dirty-Tracking as a basis:

DirtyFormTracker.vb -
Public Class DirtyFormTracker
	Private fTrackedForm As Form
	Private fTrackedControls As List(Of DirtyControlTracker)

	Public ReadOnly Property IsDirty() As Boolean
		Get
			Return GetListOfDirtyControls().Count > 0
		End Get
	End Property

	Public Sub New(ByVal parent As Form)
		fTrackedForm = parent
		fTrackedControls = New List(Of DirtyControlTracker)
		AddControlsFromForm(parent)
	End Sub

	' utility method to add the controls from a Form to this collection
	Public Sub AddControlsFromForm(ByVal frm As Form)
		AddControlsFromCollection(frm.Controls)
	End Sub

	' recursive routine to inspect each control and add to the collection accordingly
	Public Sub AddControlsFromCollection(ByVal coll As Control.ControlCollection)
		For Each c As Control In coll
			' if the control is supported for dirty tracking, add it
			Try
				If DirtyControlTracker.IsControlTypeSupported(c) Then
					fTrackedControls.Add(New DirtyControlTracker(c))
				End If
			Catch ex As Exception
				Console.WriteLine(ex.Message)
			End Try

			' recurively apply to inner collections
			If c.HasChildren Then
				AddControlsFromCollection(c.Controls)
			End If
		Next
	End Sub

	' loop through all controls and return a list of those that are dirty
	Public Function GetListOfDirtyControls() As IEnumerable(Of DirtyControlTracker)
		Return (From [control] As DirtyControlTracker In fTrackedControls Where [control].IsDirty() Select [control])
	End Function

	Public Sub MarkControlAsClean(ByVal tracker As DirtyControlTracker)
		tracker.EstablishValueAsClean()
	End Sub

	' mark all the tracked controls as clean
	Public Sub MarkAllControlsAsClean()
		For Each c As DirtyControlTracker In fTrackedControls
			c.EstablishValueAsClean()
		Next
	End Sub
End Class

Open in new window

DirtyControlTracker.vb -
Imports System.Text

Public Class DirtyControlTracker
	Private fControl As Control
	Private fCleanValue As String

	' read only properties
	Public ReadOnly Property [Control]() As Control
		Get
			Return fControl
		End Get
	End Property
	Public ReadOnly Property CleanValue() As String
		Get
			Return fCleanValue
		End Get
	End Property

	' constructor establishes the control and uses its current value as "clean"
	Public Sub New(ByVal ctl As Control)
		' if the control type is not one that is supported, throw an exception
		If DirtyControlTracker.IsControlTypeSupported(ctl) Then
			fControl = ctl
		Else
			Throw New NotSupportedException(String.Format("The control type for '{0}' is not supported by the ControlDirtyTracker class.", ctl.Name))
		End If
	End Sub

	' method to establish the the current control value as "clean"
	Public Sub EstablishValueAsClean()
		fCleanValue = GetCurrentValue()
	End Sub

	' 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 Function IsDirty() As Boolean
		' 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(fCleanValue, GetCurrentValue(), False) <> 0)
	End Function

	' 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 Shared Function IsControlTypeSupported(ByVal ctl As Control) As Boolean
		' list of types supported
		If TypeOf ctl Is TextBox Then
			Return True
		End If
		If TypeOf ctl Is CheckBox Then
			Return True
		End If
		If TypeOf ctl Is ComboBox Then
			Return True
		End If
		If TypeOf ctl Is ListBox Then
			Return True
		End If
		If TypeOf ctl Is DateTimePicker Then
			Return True
		End If
		If TypeOf ctl Is RadioButton Then
			Return True
		End If

		' ... add additional types as desired ...                       

		' not a supported type
		Return False
	End Function

	' private method to determine the current value (as a string) of the control;
	' if the control is not supported, return a NotSupported exception
	' developers may modify this to extend support for other types
	Public Function GetCurrentValue() As String
		If TypeOf fControl Is TextBox Then
			Return TryCast(fControl, TextBox).Text
		End If

		If TypeOf fControl Is CheckBox Then
			Return TryCast(fControl, CheckBox).Checked.ToString()
		End If

		If TypeOf fControl Is ComboBox Then
			Return TryCast(fControl, ComboBox).Text
		End If

		If TypeOf fControl Is ListBox Then
			' for a listbox, create a list of the selected indexes
			Dim val As New StringBuilder()
			Dim lb As ListBox = TryCast(fControl, ListBox)
			Dim coll As ListBox.SelectedIndexCollection = lb.SelectedIndices
			For i As Integer = 0 To coll.Count - 1
				val.AppendFormat("{0};", coll(i))
			Next

			Return val.ToString()
		End If

		If TypeOf fControl Is DateTimePicker Then
			Return TryCast(fControl, DateTimePicker).Text
		End If

		If TypeOf fControl Is RadioButton Then
			Return TryCast(fControl, RadioButton).Checked.ToString()
		End If

		Return ""
	End Function
End Class

Open in new window

Once those are in place then it is a simple matter of registering a DirtyFormTracker with the form you want to watch; e.g. -

Form1.vb -
Imports System.Text

Public Class Form1
	Private tracker As DirtyFormTracker

	Private Sub OnLoad(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
		tbFirstName.Text = "Paul"
		tbLastName.Text = "Wilson"
		dtpBirthday.Text = "01/01/2015"
		cmbGender.Text = "Male"
		cbAdministrator.Checked = True
		rbGreen.Checked = True

		tracker = New DirtyFormTracker(Me)
		tracker.MarkAllControlsAsClean()
	End Sub

	Private Sub OnClick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnSave.Click, btnListDirtyControls.Click, btnClose.Click
		If TypeOf sender Is Button Then
			Dim btn As Button = DirectCast(sender, Button)
			If btn.Equals(btnClose) Then
				If tracker IsNot Nothing Then
					If tracker.IsDirty() Then
						If MessageBox.Show("Would you like to save changes before closing?", "Changes detected", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question) = DialogResult.Yes Then
							For Each ctl In tracker.GetListOfDirtyControls()
								tracker.MarkControlAsClean(ctl)
							Next
						Else
							MessageBox.Show("The form is clean, so the window closes without prompting to save", "No changed detected", MessageBoxButtons.OK, MessageBoxIcon.Information)
						End If
					End If
				End If
			ElseIf btn.Equals(btnListDirtyControls) Then
				If tracker IsNot Nothing Then
					Dim builder As New StringBuilder()
					For Each ctl In tracker.GetListOfDirtyControls()
						builder.AppendLine(String.Format("{0}; Clean Value = {1}; Current Value = {2}", ctl.Control.Name, ctl.CleanValue, ctl.GetCurrentValue()))
					Next

					If builder.Length > 0 Then
						builder.Insert(0, String.Format("The following controls are dirty:{0}", Environment.NewLine))
					Else
						builder.AppendLine("None of the controls are currently 'dirty'")
					End If

					MessageBox.Show(builder.ToString(), "Dirty Controls")
				End If
			ElseIf btn.Equals(btnSave) Then
				If tracker IsNot Nothing Then
					For Each ctl In tracker.GetListOfDirtyControls()
						tracker.MarkControlAsClean(ctl)
					Next
					btn.Enabled = False
				End If
			End If
		End If
	End Sub
End Class

Open in new window

Form1.Designer.vb -
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
Partial Class Form1
	Inherits System.Windows.Forms.Form

	'Form overrides dispose to clean up the component list.
	<System.Diagnostics.DebuggerNonUserCode()> _
	Protected Overrides Sub Dispose(ByVal disposing As Boolean)
		Try
			If disposing AndAlso components IsNot Nothing Then
				components.Dispose()
			End If
		Finally
			MyBase.Dispose(disposing)
		End Try
	End Sub

	'Required by the Windows Form Designer
	Private components As System.ComponentModel.IContainer

	'NOTE: The following procedure is required by the Windows Form Designer
	'It can be modified using the Windows Form Designer.  
	'Do not modify it using the code editor.
	<System.Diagnostics.DebuggerStepThrough()> _
	Private Sub InitializeComponent()
		Me.Label1 = New System.Windows.Forms.Label()
		Me.tbFirstName = New System.Windows.Forms.TextBox()
		Me.tbLastName = New System.Windows.Forms.TextBox()
		Me.Label2 = New System.Windows.Forms.Label()
		Me.Label3 = New System.Windows.Forms.Label()
		Me.dtpBirthday = New System.Windows.Forms.DateTimePicker()
		Me.cmbGender = New System.Windows.Forms.ComboBox()
		Me.Label4 = New System.Windows.Forms.Label()
		Me.cbAdministrator = New System.Windows.Forms.CheckBox()
		Me.GroupBox1 = New System.Windows.Forms.GroupBox()
		Me.rbWhite = New System.Windows.Forms.RadioButton()
		Me.rbBlack = New System.Windows.Forms.RadioButton()
		Me.rbYellow = New System.Windows.Forms.RadioButton()
		Me.rbBlue = New System.Windows.Forms.RadioButton()
		Me.rbGreen = New System.Windows.Forms.RadioButton()
		Me.rbRed = New System.Windows.Forms.RadioButton()
		Me.btnSave = New System.Windows.Forms.Button()
		Me.btnClose = New System.Windows.Forms.Button()
		Me.btnListDirtyControls = New System.Windows.Forms.Button()
		Me.GroupBox1.SuspendLayout()
		Me.SuspendLayout()
		'
		'Label1
		'
		Me.Label1.AutoSize = True
		Me.Label1.Location = New System.Drawing.Point(12, 8)
		Me.Label1.Name = "Label1"
		Me.Label1.Size = New System.Drawing.Size(60, 13)
		Me.Label1.TabIndex = 0
		Me.Label1.Text = "First Name:"
		'
		'tbFirstName
		'
		Me.tbFirstName.Location = New System.Drawing.Point(78, 5)
		Me.tbFirstName.Name = "tbFirstName"
		Me.tbFirstName.Size = New System.Drawing.Size(194, 20)
		Me.tbFirstName.TabIndex = 1
		'
		'tbLastName
		'
		Me.tbLastName.Location = New System.Drawing.Point(78, 31)
		Me.tbLastName.Name = "tbLastName"
		Me.tbLastName.Size = New System.Drawing.Size(194, 20)
		Me.tbLastName.TabIndex = 3
		'
		'Label2
		'
		Me.Label2.AutoSize = True
		Me.Label2.Location = New System.Drawing.Point(11, 34)
		Me.Label2.Name = "Label2"
		Me.Label2.Size = New System.Drawing.Size(61, 13)
		Me.Label2.TabIndex = 2
		Me.Label2.Text = "Last Name:"
		'
		'Label3
		'
		Me.Label3.AutoSize = True
		Me.Label3.Location = New System.Drawing.Point(24, 61)
		Me.Label3.Name = "Label3"
		Me.Label3.Size = New System.Drawing.Size(48, 13)
		Me.Label3.TabIndex = 4
		Me.Label3.Text = "Birthday:"
		'
		'dtpBirthday
		'
		Me.dtpBirthday.Location = New System.Drawing.Point(78, 57)
		Me.dtpBirthday.Name = "dtpBirthday"
		Me.dtpBirthday.Size = New System.Drawing.Size(194, 20)
		Me.dtpBirthday.TabIndex = 5
		'
		'cmbGender
		'
		Me.cmbGender.FormattingEnabled = True
		Me.cmbGender.Items.AddRange(New Object() {"Unknown", "Male", "Female", "Undeclared"})
		Me.cmbGender.Location = New System.Drawing.Point(78, 84)
		Me.cmbGender.Name = "cmbGender"
		Me.cmbGender.Size = New System.Drawing.Size(194, 21)
		Me.cmbGender.TabIndex = 6
		'
		'Label4
		'
		Me.Label4.AutoSize = True
		Me.Label4.Location = New System.Drawing.Point(27, 87)
		Me.Label4.Name = "Label4"
		Me.Label4.Size = New System.Drawing.Size(45, 13)
		Me.Label4.TabIndex = 7
		Me.Label4.Text = "Gender:"
		'
		'cbAdministrator
		'
		Me.cbAdministrator.AutoSize = True
		Me.cbAdministrator.Location = New System.Drawing.Point(175, 111)
		Me.cbAdministrator.Name = "cbAdministrator"
		Me.cbAdministrator.Size = New System.Drawing.Size(97, 17)
		Me.cbAdministrator.TabIndex = 8
		Me.cbAdministrator.Text = "Is Administrator"
		Me.cbAdministrator.UseVisualStyleBackColor = True
		'
		'GroupBox1
		'
		Me.GroupBox1.Controls.Add(Me.rbWhite)
		Me.GroupBox1.Controls.Add(Me.rbBlack)
		Me.GroupBox1.Controls.Add(Me.rbYellow)
		Me.GroupBox1.Controls.Add(Me.rbBlue)
		Me.GroupBox1.Controls.Add(Me.rbGreen)
		Me.GroupBox1.Controls.Add(Me.rbRed)
		Me.GroupBox1.Location = New System.Drawing.Point(72, 134)
		Me.GroupBox1.Name = "GroupBox1"
		Me.GroupBox1.Size = New System.Drawing.Size(200, 85)
		Me.GroupBox1.TabIndex = 9
		Me.GroupBox1.TabStop = False
		Me.GroupBox1.Text = "Favorite Color"
		'
		'rbWhite
		'
		Me.rbWhite.AutoSize = True
		Me.rbWhite.Location = New System.Drawing.Point(103, 65)
		Me.rbWhite.Name = "rbWhite"
		Me.rbWhite.Size = New System.Drawing.Size(53, 17)
		Me.rbWhite.TabIndex = 5
		Me.rbWhite.TabStop = True
		Me.rbWhite.Text = "White"
		Me.rbWhite.UseVisualStyleBackColor = True
		'
		'rbBlack
		'
		Me.rbBlack.AutoSize = True
		Me.rbBlack.Location = New System.Drawing.Point(103, 42)
		Me.rbBlack.Name = "rbBlack"
		Me.rbBlack.Size = New System.Drawing.Size(52, 17)
		Me.rbBlack.TabIndex = 4
		Me.rbBlack.TabStop = True
		Me.rbBlack.Text = "Black"
		Me.rbBlack.UseVisualStyleBackColor = True
		'
		'rbYellow
		'
		Me.rbYellow.AutoSize = True
		Me.rbYellow.Location = New System.Drawing.Point(104, 19)
		Me.rbYellow.Name = "rbYellow"
		Me.rbYellow.Size = New System.Drawing.Size(56, 17)
		Me.rbYellow.TabIndex = 3
		Me.rbYellow.TabStop = True
		Me.rbYellow.Text = "Yellow"
		Me.rbYellow.UseVisualStyleBackColor = True
		'
		'rbBlue
		'
		Me.rbBlue.AutoSize = True
		Me.rbBlue.Location = New System.Drawing.Point(7, 66)
		Me.rbBlue.Name = "rbBlue"
		Me.rbBlue.Size = New System.Drawing.Size(46, 17)
		Me.rbBlue.TabIndex = 2
		Me.rbBlue.TabStop = True
		Me.rbBlue.Text = "Blue"
		Me.rbBlue.UseVisualStyleBackColor = True
		'
		'rbGreen
		'
		Me.rbGreen.AutoSize = True
		Me.rbGreen.Location = New System.Drawing.Point(7, 43)
		Me.rbGreen.Name = "rbGreen"
		Me.rbGreen.Size = New System.Drawing.Size(54, 17)
		Me.rbGreen.TabIndex = 1
		Me.rbGreen.TabStop = True
		Me.rbGreen.Text = "Green"
		Me.rbGreen.UseVisualStyleBackColor = True
		'
		'rbRed
		'
		Me.rbRed.AutoSize = True
		Me.rbRed.Location = New System.Drawing.Point(7, 20)
		Me.rbRed.Name = "rbRed"
		Me.rbRed.Size = New System.Drawing.Size(45, 17)
		Me.rbRed.TabIndex = 0
		Me.rbRed.TabStop = True
		Me.rbRed.Text = "Red"
		Me.rbRed.UseVisualStyleBackColor = True
		'
		'btnSave
		'
		Me.btnSave.Location = New System.Drawing.Point(116, 227)
		Me.btnSave.Name = "btnSave"
		Me.btnSave.Size = New System.Drawing.Size(75, 23)
		Me.btnSave.TabIndex = 10
		Me.btnSave.Text = "Save"
		Me.btnSave.UseVisualStyleBackColor = True
		'
		'btnClose
		'
		Me.btnClose.Location = New System.Drawing.Point(197, 227)
		Me.btnClose.Name = "btnClose"
		Me.btnClose.Size = New System.Drawing.Size(75, 23)
		Me.btnClose.TabIndex = 11
		Me.btnClose.Text = "Close"
		Me.btnClose.UseVisualStyleBackColor = True
		'
		'btnListDirtyControls
		'
		Me.btnListDirtyControls.Location = New System.Drawing.Point(12, 227)
		Me.btnListDirtyControls.Name = "btnListDirtyControls"
		Me.btnListDirtyControls.Size = New System.Drawing.Size(98, 23)
		Me.btnListDirtyControls.TabIndex = 12
		Me.btnListDirtyControls.Text = "List Dirty Controls"
		Me.btnListDirtyControls.UseVisualStyleBackColor = True
		'
		'Form1
		'
		Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!)
		Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font
		Me.ClientSize = New System.Drawing.Size(284, 262)
		Me.Controls.Add(Me.btnListDirtyControls)
		Me.Controls.Add(Me.btnClose)
		Me.Controls.Add(Me.btnSave)
		Me.Controls.Add(Me.GroupBox1)
		Me.Controls.Add(Me.cbAdministrator)
		Me.Controls.Add(Me.Label4)
		Me.Controls.Add(Me.cmbGender)
		Me.Controls.Add(Me.dtpBirthday)
		Me.Controls.Add(Me.Label3)
		Me.Controls.Add(Me.tbLastName)
		Me.Controls.Add(Me.Label2)
		Me.Controls.Add(Me.tbFirstName)
		Me.Controls.Add(Me.Label1)
		Me.Name = "Form1"
		Me.Text = "Form1"
		Me.GroupBox1.ResumeLayout(False)
		Me.GroupBox1.PerformLayout()
		Me.ResumeLayout(False)
		Me.PerformLayout()

	End Sub
	Friend WithEvents Label1 As System.Windows.Forms.Label
	Friend WithEvents tbFirstName As System.Windows.Forms.TextBox
	Friend WithEvents tbLastName As System.Windows.Forms.TextBox
	Friend WithEvents Label2 As System.Windows.Forms.Label
	Friend WithEvents Label3 As System.Windows.Forms.Label
	Friend WithEvents dtpBirthday As System.Windows.Forms.DateTimePicker
	Friend WithEvents cmbGender As System.Windows.Forms.ComboBox
	Friend WithEvents Label4 As System.Windows.Forms.Label
	Friend WithEvents cbAdministrator As System.Windows.Forms.CheckBox
	Friend WithEvents GroupBox1 As System.Windows.Forms.GroupBox
	Friend WithEvents rbWhite As System.Windows.Forms.RadioButton
	Friend WithEvents rbBlack As System.Windows.Forms.RadioButton
	Friend WithEvents rbYellow As System.Windows.Forms.RadioButton
	Friend WithEvents rbBlue As System.Windows.Forms.RadioButton
	Friend WithEvents rbGreen As System.Windows.Forms.RadioButton
	Friend WithEvents rbRed As System.Windows.Forms.RadioButton
	Friend WithEvents btnSave As System.Windows.Forms.Button
	Friend WithEvents btnClose As System.Windows.Forms.Button
	Friend WithEvents btnListDirtyControls As System.Windows.Forms.Button

End Class

Open in new window

Which produces the following output -Initial loadAfter changing some values and listing the dirty controlsAttempting to close the form with dirty controls results in a dialog that asks if you want to save the changesFrom here you can do many different things depending upon how far you want to go.  For example, you can make it so that the save button is only enabled when there are dirty controls (requires hooking into an event on the controls).  You could even create a custom event on the dirty form tracker that alerts the form when it has dirty controls.  Perhaps something like:

CleanlinessChangedEventHandler.vb -
Public Delegate Sub CleanlinessChangedEventHandler(ByVal sender As Object, ByVal e As EventArgs)

Open in new window

DirtyFormTracker.vb -
Public Class DirtyFormTracker
	Private ReadOnly fCleanlinessChanged As New List(Of CleanlinessChangedEventHandler)
	Public Custom Event CleanlinessChanged As CleanlinessChangedEventHandler
		AddHandler(ByVal value As CleanlinessChangedEventHandler)
			fCleanlinessChanged.Add(value)
		End AddHandler

		RemoveHandler(ByVal value As CleanlinessChangedEventHandler)
			fCleanlinessChanged.Remove(value)
		End RemoveHandler

		RaiseEvent(ByVal sender As Object, ByVal e As EventArgs)
			For Each handler As CleanlinessChangedEventHandler In fCleanlinessChanged
				Try
					handler.Invoke(sender, e)
				Catch ex As Exception
					Debug.WriteLine(String.Format("Exception while invoking event handler: {0}", ex))
				End Try
			Next
		End RaiseEvent
	End Event

	Private fTrackedForm As Form
	Private fTrackedControls As List(Of DirtyControlTracker)

	Public ReadOnly Property IsDirty() As Boolean
		Get
			Return GetListOfDirtyControls().Count > 0
		End Get
	End Property

	Public Sub New(ByVal parent As Form)
		fTrackedForm = parent
		fTrackedControls = New List(Of DirtyControlTracker)
		AddControlsFromForm(parent)
	End Sub

	' utility method to add the controls from a Form to this collection
	Public Sub AddControlsFromForm(ByVal frm As Form)
		AddControlsFromCollection(frm.Controls)
	End Sub

	' recursive routine to inspect each control and add to the collection accordingly
	Public Sub AddControlsFromCollection(ByVal coll As Control.ControlCollection)
		For Each c As Control In coll
			' if the control is supported for dirty tracking, add it
			Try
				If DirtyControlTracker.IsControlTypeSupported(c) Then
					Dim controlTracker As New DirtyControlTracker(c)
					AddHandler controlTracker.CleanlinessChanged, AddressOf OnCleanLinessChanged
					fTrackedControls.Add(controlTracker)
				End If
			Catch ex As Exception
				Console.WriteLine(ex.Message)
			End Try

			' recurively apply to inner collections
			If c.HasChildren Then
				AddControlsFromCollection(c.Controls)
			End If
		Next
	End Sub

	' loop through all controls and return a list of those that are dirty
	Public Function GetListOfDirtyControls() As IEnumerable(Of DirtyControlTracker)
		Return (From [control] As DirtyControlTracker In fTrackedControls Where [control].IsDirty() Select [control])
	End Function

	Public Sub MarkControlAsClean(ByVal tracker As DirtyControlTracker)
		tracker.EstablishValueAsClean()
	End Sub

	' mark all the tracked controls as clean
	Public Sub MarkAllControlsAsClean()
		For Each c As DirtyControlTracker In fTrackedControls
			c.EstablishValueAsClean()
		Next
	End Sub

	Private Sub OnCleanLinessChanged(ByVal sender As Object, ByVal e As EventArgs)
		RaiseEvent CleanlinessChanged(Me, e)
	End Sub
End Class

Open in new window

DirtyControlTracker.vb -
Imports System.Text

Public Class DirtyControlTracker
	Private ReadOnly fCleanlinessChanged As New List(Of CleanlinessChangedEventHandler)
	Public Custom Event CleanlinessChanged As CleanlinessChangedEventHandler
		AddHandler(ByVal value As CleanlinessChangedEventHandler)
			fCleanlinessChanged.Add(value)
		End AddHandler

		RemoveHandler(ByVal value As CleanlinessChangedEventHandler)
			fCleanlinessChanged.Remove(value)
		End RemoveHandler

		RaiseEvent(ByVal sender As Object, ByVal e As EventArgs)
			For Each handler As CleanlinessChangedEventHandler In fCleanlinessChanged
				Try
					handler.Invoke(sender, e)
				Catch ex As Exception
					Debug.WriteLine(String.Format("Exception while invoking event handler: {0}", ex))
				End Try
			Next
		End RaiseEvent
	End Event

	Private fControl As Control
	Private fCleanValue As String

	' read only properties
	Public ReadOnly Property [Control]() As Control
		Get
			Return fControl
		End Get
	End Property
	Public ReadOnly Property CleanValue() As String
		Get
			Return fCleanValue
		End Get
	End Property

	' constructor establishes the control and uses its current value as "clean"
	Public Sub New(ByVal ctl As Control)
		' if the control type is not one that is supported, throw an exception
		If DirtyControlTracker.IsControlTypeSupported(ctl) Then
			fControl = ctl
			AssignHandlerForDataChanged(ctl)
		Else
			Throw New NotSupportedException(String.Format("The control type for '{0}' is not supported by the ControlDirtyTracker class.", ctl.Name))
		End If
	End Sub

	' method to establish the the current control value as "clean"
	Public Sub EstablishValueAsClean()
		fCleanValue = GetCurrentValue()
	End Sub

	' 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 Function IsDirty() As Boolean
		' 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(fCleanValue, GetCurrentValue(), False) <> 0)
	End Function

	' 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 Shared Function IsControlTypeSupported(ByVal ctl As Control) As Boolean
		' list of types supported
		If TypeOf ctl Is TextBox Then
			Return True
		ElseIf TypeOf ctl Is CheckBox Then
			Return True
		ElseIf TypeOf ctl Is ComboBox Then
			Return True
		ElseIf TypeOf ctl Is ListBox Then
			Return True
		ElseIf TypeOf ctl Is DateTimePicker Then
			Return True
		ElseIf TypeOf ctl Is RadioButton Then
			Return True
		End If

		' ... add additional types as desired ...                       

		' not a supported type
		Return False
	End Function

	' private method to determine the current value (as a string) of the control;
	' if the control is not supported, return a NotSupported exception
	' developers may modify this to extend support for other types
	Public Function GetCurrentValue() As String
		If TypeOf fControl Is TextBox Then
			Return TryCast(fControl, TextBox).Text
		ElseIf TypeOf fControl Is CheckBox Then
			Return TryCast(fControl, CheckBox).Checked.ToString()
		ElseIf TypeOf fControl Is ComboBox Then
			Return TryCast(fControl, ComboBox).Text
		ElseIf TypeOf fControl Is ListBox Then
			' for a listbox, create a list of the selected indexes
			Dim val As New StringBuilder()
			Dim lb As ListBox = TryCast(fControl, ListBox)
			Dim coll As ListBox.SelectedIndexCollection = lb.SelectedIndices
			For i As Integer = 0 To coll.Count - 1
				val.AppendFormat("{0};", coll(i))
			Next

			Return val.ToString()
		ElseIf TypeOf fControl Is DateTimePicker Then
			Return TryCast(fControl, DateTimePicker).Text
		ElseIf TypeOf fControl Is RadioButton Then
			Return TryCast(fControl, RadioButton).Checked.ToString()
		End If

		Return ""
	End Function

	Private Sub AssignHandlerForDataChanged(ByVal ctl As Control)
		If TypeOf ctl Is TextBox Then
			AddHandler ctl.TextChanged, AddressOf OnChanged
		ElseIf TypeOf ctl Is CheckBox Then
			AddHandler DirectCast(ctl, CheckBox).CheckedChanged, AddressOf OnChanged
		ElseIf TypeOf ctl Is ComboBox Then
			AddHandler DirectCast(ctl, ComboBox).SelectedValueChanged, AddressOf OnChanged
		ElseIf TypeOf ctl Is ListBox Then
			AddHandler DirectCast(ctl, ListBox).SelectedValueChanged, AddressOf OnChanged
		ElseIf TypeOf ctl Is DateTimePicker Then
			AddHandler DirectCast(ctl, DateTimePicker).ValueChanged, AddressOf OnChanged
		ElseIf TypeOf ctl Is RadioButton Then
			AddHandler DirectCast(ctl, RadioButton).CheckedChanged, AddressOf OnChanged
		End If
	End Sub

	Private Sub OnChanged(ByVal sender As Object, ByVal e As EventArgs)
		RaiseEvent CleanlinessChanged(Me, e)
	End Sub
End Class

Open in new window

Form1.vb -
Imports System.Text

Public Class Form1
	Private WithEvents tracker As DirtyFormTracker

	Private Sub OnLoad(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
		tbFirstName.Text = "Paul"
		tbLastName.Text = "Wilson"
		dtpBirthday.Text = "01/01/2015"
		cmbGender.Text = "Male"
		cbAdministrator.Checked = True
		rbGreen.Checked = True

		tracker = New DirtyFormTracker(Me)
		tracker.MarkAllControlsAsClean()
		btnSave.Enabled = tracker.IsDirty()
	End Sub

	Private Sub OnCleanlinessChanged(ByVal sender As Object, ByVal e As EventArgs) Handles tracker.CleanlinessChanged
		If TypeOf sender Is DirtyFormTracker Then
			Dim formTracker As DirtyFormTracker = DirectCast(sender, DirtyFormTracker)
			btnSave.Enabled = formTracker.IsDirty()
		End If
	End Sub

	Private Sub OnClick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnSave.Click, btnListDirtyControls.Click, btnClose.Click
		If TypeOf sender Is Button Then
			Dim btn As Button = DirectCast(sender, Button)
			If btn.Equals(btnClose) Then
				If tracker IsNot Nothing Then
					If tracker.IsDirty() Then
						If MessageBox.Show("Would you like to save changes before closing?", "Changes detected", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question) = DialogResult.Yes Then
							For Each ctl In tracker.GetListOfDirtyControls()
								tracker.MarkControlAsClean(ctl)
							Next
						Else
							MessageBox.Show("The form is clean, so the window closes without prompting to save", "No changed detected", MessageBoxButtons.OK, MessageBoxIcon.Information)
						End If
					End If
				End If
			ElseIf btn.Equals(btnListDirtyControls) Then
				If tracker IsNot Nothing Then
					Dim builder As New StringBuilder()
					For Each ctl In tracker.GetListOfDirtyControls()
						builder.AppendLine(String.Format("{0}; Clean Value = {1}; Current Value = {2}", ctl.Control.Name, ctl.CleanValue, ctl.GetCurrentValue()))
					Next

					If builder.Length > 0 Then
						builder.Insert(0, String.Format("The following controls are dirty:{0}", Environment.NewLine))
					Else
						builder.AppendLine("None of the controls are currently 'dirty'")
					End If

					MessageBox.Show(builder.ToString(), "Dirty Controls")
				End If
			ElseIf btn.Equals(btnSave) Then
				If tracker IsNot Nothing Then
					For Each ctl In tracker.GetListOfDirtyControls()
						tracker.MarkControlAsClean(ctl)
					Next
					btn.Enabled = False
				End If
			End If
		End If
	End Sub
End Class

Open in new window

Form1.Designer.vb -
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
Partial Class Form1
	Inherits System.Windows.Forms.Form

	'Form overrides dispose to clean up the component list.
	<System.Diagnostics.DebuggerNonUserCode()> _
	Protected Overrides Sub Dispose(ByVal disposing As Boolean)
		Try
			If disposing AndAlso components IsNot Nothing Then
				components.Dispose()
			End If
		Finally
			MyBase.Dispose(disposing)
		End Try
	End Sub

	'Required by the Windows Form Designer
	Private components As System.ComponentModel.IContainer

	'NOTE: The following procedure is required by the Windows Form Designer
	'It can be modified using the Windows Form Designer.  
	'Do not modify it using the code editor.
	<System.Diagnostics.DebuggerStepThrough()> _
	Private Sub InitializeComponent()
		Me.Label1 = New System.Windows.Forms.Label()
		Me.tbFirstName = New System.Windows.Forms.TextBox()
		Me.tbLastName = New System.Windows.Forms.TextBox()
		Me.Label2 = New System.Windows.Forms.Label()
		Me.Label3 = New System.Windows.Forms.Label()
		Me.dtpBirthday = New System.Windows.Forms.DateTimePicker()
		Me.cmbGender = New System.Windows.Forms.ComboBox()
		Me.Label4 = New System.Windows.Forms.Label()
		Me.cbAdministrator = New System.Windows.Forms.CheckBox()
		Me.GroupBox1 = New System.Windows.Forms.GroupBox()
		Me.rbWhite = New System.Windows.Forms.RadioButton()
		Me.rbBlack = New System.Windows.Forms.RadioButton()
		Me.rbYellow = New System.Windows.Forms.RadioButton()
		Me.rbBlue = New System.Windows.Forms.RadioButton()
		Me.rbGreen = New System.Windows.Forms.RadioButton()
		Me.rbRed = New System.Windows.Forms.RadioButton()
		Me.btnSave = New System.Windows.Forms.Button()
		Me.btnClose = New System.Windows.Forms.Button()
		Me.btnListDirtyControls = New System.Windows.Forms.Button()
		Me.GroupBox1.SuspendLayout()
		Me.SuspendLayout()
		'
		'Label1
		'
		Me.Label1.AutoSize = True
		Me.Label1.Location = New System.Drawing.Point(12, 8)
		Me.Label1.Name = "Label1"
		Me.Label1.Size = New System.Drawing.Size(60, 13)
		Me.Label1.TabIndex = 0
		Me.Label1.Text = "First Name:"
		'
		'tbFirstName
		'
		Me.tbFirstName.Location = New System.Drawing.Point(78, 5)
		Me.tbFirstName.Name = "tbFirstName"
		Me.tbFirstName.Size = New System.Drawing.Size(194, 20)
		Me.tbFirstName.TabIndex = 1
		'
		'tbLastName
		'
		Me.tbLastName.Location = New System.Drawing.Point(78, 31)
		Me.tbLastName.Name = "tbLastName"
		Me.tbLastName.Size = New System.Drawing.Size(194, 20)
		Me.tbLastName.TabIndex = 3
		'
		'Label2
		'
		Me.Label2.AutoSize = True
		Me.Label2.Location = New System.Drawing.Point(11, 34)
		Me.Label2.Name = "Label2"
		Me.Label2.Size = New System.Drawing.Size(61, 13)
		Me.Label2.TabIndex = 2
		Me.Label2.Text = "Last Name:"
		'
		'Label3
		'
		Me.Label3.AutoSize = True
		Me.Label3.Location = New System.Drawing.Point(24, 61)
		Me.Label3.Name = "Label3"
		Me.Label3.Size = New System.Drawing.Size(48, 13)
		Me.Label3.TabIndex = 4
		Me.Label3.Text = "Birthday:"
		'
		'dtpBirthday
		'
		Me.dtpBirthday.Location = New System.Drawing.Point(78, 57)
		Me.dtpBirthday.Name = "dtpBirthday"
		Me.dtpBirthday.Size = New System.Drawing.Size(194, 20)
		Me.dtpBirthday.TabIndex = 5
		'
		'cmbGender
		'
		Me.cmbGender.FormattingEnabled = True
		Me.cmbGender.Items.AddRange(New Object() {"Unknown", "Male", "Female", "Undeclared"})
		Me.cmbGender.Location = New System.Drawing.Point(78, 84)
		Me.cmbGender.Name = "cmbGender"
		Me.cmbGender.Size = New System.Drawing.Size(194, 21)
		Me.cmbGender.TabIndex = 6
		'
		'Label4
		'
		Me.Label4.AutoSize = True
		Me.Label4.Location = New System.Drawing.Point(27, 87)
		Me.Label4.Name = "Label4"
		Me.Label4.Size = New System.Drawing.Size(45, 13)
		Me.Label4.TabIndex = 7
		Me.Label4.Text = "Gender:"
		'
		'cbAdministrator
		'
		Me.cbAdministrator.AutoSize = True
		Me.cbAdministrator.Location = New System.Drawing.Point(175, 111)
		Me.cbAdministrator.Name = "cbAdministrator"
		Me.cbAdministrator.Size = New System.Drawing.Size(97, 17)
		Me.cbAdministrator.TabIndex = 8
		Me.cbAdministrator.Text = "Is Administrator"
		Me.cbAdministrator.UseVisualStyleBackColor = True
		'
		'GroupBox1
		'
		Me.GroupBox1.Controls.Add(Me.rbWhite)
		Me.GroupBox1.Controls.Add(Me.rbBlack)
		Me.GroupBox1.Controls.Add(Me.rbYellow)
		Me.GroupBox1.Controls.Add(Me.rbBlue)
		Me.GroupBox1.Controls.Add(Me.rbGreen)
		Me.GroupBox1.Controls.Add(Me.rbRed)
		Me.GroupBox1.Location = New System.Drawing.Point(72, 134)
		Me.GroupBox1.Name = "GroupBox1"
		Me.GroupBox1.Size = New System.Drawing.Size(200, 85)
		Me.GroupBox1.TabIndex = 9
		Me.GroupBox1.TabStop = False
		Me.GroupBox1.Text = "Favorite Color"
		'
		'rbWhite
		'
		Me.rbWhite.AutoSize = True
		Me.rbWhite.Location = New System.Drawing.Point(103, 65)
		Me.rbWhite.Name = "rbWhite"
		Me.rbWhite.Size = New System.Drawing.Size(53, 17)
		Me.rbWhite.TabIndex = 5
		Me.rbWhite.TabStop = True
		Me.rbWhite.Text = "White"
		Me.rbWhite.UseVisualStyleBackColor = True
		'
		'rbBlack
		'
		Me.rbBlack.AutoSize = True
		Me.rbBlack.Location = New System.Drawing.Point(103, 42)
		Me.rbBlack.Name = "rbBlack"
		Me.rbBlack.Size = New System.Drawing.Size(52, 17)
		Me.rbBlack.TabIndex = 4
		Me.rbBlack.TabStop = True
		Me.rbBlack.Text = "Black"
		Me.rbBlack.UseVisualStyleBackColor = True
		'
		'rbYellow
		'
		Me.rbYellow.AutoSize = True
		Me.rbYellow.Location = New System.Drawing.Point(104, 19)
		Me.rbYellow.Name = "rbYellow"
		Me.rbYellow.Size = New System.Drawing.Size(56, 17)
		Me.rbYellow.TabIndex = 3
		Me.rbYellow.TabStop = True
		Me.rbYellow.Text = "Yellow"
		Me.rbYellow.UseVisualStyleBackColor = True
		'
		'rbBlue
		'
		Me.rbBlue.AutoSize = True
		Me.rbBlue.Location = New System.Drawing.Point(7, 66)
		Me.rbBlue.Name = "rbBlue"
		Me.rbBlue.Size = New System.Drawing.Size(46, 17)
		Me.rbBlue.TabIndex = 2
		Me.rbBlue.TabStop = True
		Me.rbBlue.Text = "Blue"
		Me.rbBlue.UseVisualStyleBackColor = True
		'
		'rbGreen
		'
		Me.rbGreen.AutoSize = True
		Me.rbGreen.Location = New System.Drawing.Point(7, 43)
		Me.rbGreen.Name = "rbGreen"
		Me.rbGreen.Size = New System.Drawing.Size(54, 17)
		Me.rbGreen.TabIndex = 1
		Me.rbGreen.TabStop = True
		Me.rbGreen.Text = "Green"
		Me.rbGreen.UseVisualStyleBackColor = True
		'
		'rbRed
		'
		Me.rbRed.AutoSize = True
		Me.rbRed.Location = New System.Drawing.Point(7, 20)
		Me.rbRed.Name = "rbRed"
		Me.rbRed.Size = New System.Drawing.Size(45, 17)
		Me.rbRed.TabIndex = 0
		Me.rbRed.TabStop = True
		Me.rbRed.Text = "Red"
		Me.rbRed.UseVisualStyleBackColor = True
		'
		'btnSave
		'
		Me.btnSave.Location = New System.Drawing.Point(116, 227)
		Me.btnSave.Name = "btnSave"
		Me.btnSave.Size = New System.Drawing.Size(75, 23)
		Me.btnSave.TabIndex = 10
		Me.btnSave.Text = "Save"
		Me.btnSave.UseVisualStyleBackColor = True
		'
		'btnClose
		'
		Me.btnClose.Location = New System.Drawing.Point(197, 227)
		Me.btnClose.Name = "btnClose"
		Me.btnClose.Size = New System.Drawing.Size(75, 23)
		Me.btnClose.TabIndex = 11
		Me.btnClose.Text = "Close"
		Me.btnClose.UseVisualStyleBackColor = True
		'
		'btnListDirtyControls
		'
		Me.btnListDirtyControls.Location = New System.Drawing.Point(12, 227)
		Me.btnListDirtyControls.Name = "btnListDirtyControls"
		Me.btnListDirtyControls.Size = New System.Drawing.Size(98, 23)
		Me.btnListDirtyControls.TabIndex = 12
		Me.btnListDirtyControls.Text = "List Dirty Controls"
		Me.btnListDirtyControls.UseVisualStyleBackColor = True
		'
		'Form1
		'
		Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!)
		Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font
		Me.ClientSize = New System.Drawing.Size(284, 262)
		Me.Controls.Add(Me.btnListDirtyControls)
		Me.Controls.Add(Me.btnClose)
		Me.Controls.Add(Me.btnSave)
		Me.Controls.Add(Me.GroupBox1)
		Me.Controls.Add(Me.cbAdministrator)
		Me.Controls.Add(Me.Label4)
		Me.Controls.Add(Me.cmbGender)
		Me.Controls.Add(Me.dtpBirthday)
		Me.Controls.Add(Me.Label3)
		Me.Controls.Add(Me.tbLastName)
		Me.Controls.Add(Me.Label2)
		Me.Controls.Add(Me.tbFirstName)
		Me.Controls.Add(Me.Label1)
		Me.Name = "Form1"
		Me.Text = "Form1"
		Me.GroupBox1.ResumeLayout(False)
		Me.GroupBox1.PerformLayout()
		Me.ResumeLayout(False)
		Me.PerformLayout()

	End Sub
	Friend WithEvents Label1 As System.Windows.Forms.Label
	Friend WithEvents tbFirstName As System.Windows.Forms.TextBox
	Friend WithEvents tbLastName As System.Windows.Forms.TextBox
	Friend WithEvents Label2 As System.Windows.Forms.Label
	Friend WithEvents Label3 As System.Windows.Forms.Label
	Friend WithEvents dtpBirthday As System.Windows.Forms.DateTimePicker
	Friend WithEvents cmbGender As System.Windows.Forms.ComboBox
	Friend WithEvents Label4 As System.Windows.Forms.Label
	Friend WithEvents cbAdministrator As System.Windows.Forms.CheckBox
	Friend WithEvents GroupBox1 As System.Windows.Forms.GroupBox
	Friend WithEvents rbWhite As System.Windows.Forms.RadioButton
	Friend WithEvents rbBlack As System.Windows.Forms.RadioButton
	Friend WithEvents rbYellow As System.Windows.Forms.RadioButton
	Friend WithEvents rbBlue As System.Windows.Forms.RadioButton
	Friend WithEvents rbGreen As System.Windows.Forms.RadioButton
	Friend WithEvents rbRed As System.Windows.Forms.RadioButton
	Friend WithEvents btnSave As System.Windows.Forms.Button
	Friend WithEvents btnClose As System.Windows.Forms.Button
	Friend WithEvents btnListDirtyControls As System.Windows.Forms.Button

End Class

Open in new window

Which now produces the following ouput -Save button disabled because no controls are dirtyChanging a control value results in the save button becoming enabledThis is all reposted from a previous EE PAQ: http:/Q_28692186.html

I would definitely recommend reading through the posts there as well.  Ark discusses using the RowState for databound items in order to determine dirtiness.

-saige-
SchoolTeacher54Author Commented:
A very complete tutorial and more. It is appreciated and I thank you!
it_saigeDeveloperCommented:
Glad we could be of assistance.

-saige-
It's more than this solution.Get answers and train to solve all your tech problems - anytime, anywhere.Try it for free Edge Out The Competitionfor your dream job with proven skills and certifications.Get started today Stand Outas the employee with proven skills.Start learning today for free Move Your Career Forwardwith certification training in the latest technologies.Start your trial today
Visual Basic.NET

From novice to tech pro — start learning today.