8

I have a DataGridView bound to a bindingsource which is bound to a List<T>. The user clicks a row that goes to a form with textboxes, etc. The textboxes are databound like so:

if (txtID.DataBindings.Count == 0)
    txtID.DataBindings.Add("Text", bindingSource, "Title");

I want to be able to detect if the user has modified any data in the controls when they click the close button, so I can prompt them to say "You have un-saved work. Do you want to Save?"

How do I detect this on the binding source?

UPDATE: I have worked out that I can do bindingSource.EndEdit() which pushes the changes to my item in the list. In my item, I can then say if Dirty throw a Messagebox but if they click "No" to saving the information, the CancelEdit does not work.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Jon
  • 38,814
  • 81
  • 233
  • 382

12 Answers12

8

If your object within the List support the INotifyPropertyChanged event and you replace the List<T> by a BindingList<T> you can subscribe to the ListChanged event of the BindingList to get informed about any changes made by the user.

Oliver
  • 43,366
  • 8
  • 94
  • 151
  • Hi Oliver, old post ... but do you have an example? I am asking here. http://stackoverflow.com/questions/25820078/detecting-dirty-using-winforms-data-binding – Kirsten Sep 13 '14 at 06:04
3

If you're bound to a DataSet then you're in luck: it has a HasChanges Property. You can get the actual changes by calling GetChanges on the dataset. This returns a new dataset containing a copy of all changed rows

Matt Jacobsen
  • 5,814
  • 4
  • 35
  • 49
  • The bindingsource datasource is a List – Jon Feb 05 '10 at 09:36
  • You can't watch collections. An alternative would be a bunch of TextChanged events that update a private "Changed" variable that you check before saving – Matt Jacobsen Feb 05 '10 at 09:48
  • I had that thought but its nasty – Jon Feb 05 '10 at 09:55
  • It is! Is using a DataSet/DataTable out of the question? – Matt Jacobsen Feb 05 '10 at 09:57
  • I think so because the List contains List which has all the properties etc of that item being edited/added – Jon Feb 05 '10 at 09:59
  • You could still do it: those properties in MyClass just need to be mapped to DataColumns in your new DataTable. Depends on your situation how icky this is though. – Matt Jacobsen Feb 05 '10 at 10:04
  • Or you can monitor the changes in MyClass. That's a little prettier than a bunch of TextChanged events in teh form. – Matt Jacobsen Feb 05 '10 at 10:07
  • How could I monitor my class as its properties wont get changed until you call the MyClass.Save. I thought the databinding would provide me with the monitoring but it isnt – Jon Feb 05 '10 at 11:53
2

I made this function now. You can use like:

if (changedOrNew(myBindingSource)){
    // Do something!
}

public bool changedOrNew(BindingSource bs){
    EntityObject obj = (EntityObject)bs.Current;
    if (obj==null)
        return false;
    return (obj.EntityState == EntityState.Detached ||
            obj.EntityState == EntityState.Added ||
            obj.EntityState == EntityState.Modified);
}
Tiago Gouvêa
  • 15,036
  • 4
  • 75
  • 81
  • 2
    Not useful unless the DataSource of the BindingSource is an 'EntityObject', which I will assume is an old version of an Entity Framework base class? Also, why are you passing in a 'bs' parameter that is never used? – Christopher King Dec 20 '13 at 18:31
  • You're right @ChristopherKing, I fixed the bs parameter now to be used on the first line.. thanks. The EntityObject are not a old version, all my entities inherits the EntityObject class, even in the .Net 4.5. – Tiago Gouvêa Dec 21 '13 at 12:26
2

A simpler approach would be to subscribe to the BindingSource's ListChanged event and set an IsDirty flag based on the event type.

categoryBindingSource.ListChanged += 
new System.ComponentModel.ListChangedEventHandler(categoryBindingSource_ListChanged);

and set IsDirty = true in the event method...

void customerAccountBindingSource_ListChanged(object sender, system.ComponentModel.ListChangedEventArgs e)
{
    if (e.ListChangedType == System.ComponentModel.ListChangedType.ItemChanged)
        _isDirty = true;
}

A word of caution here, it would not be able to detect when the modified value is still same as the original value. Memberwise.Clone can be used additionally if that level of accuracy is required.

gunr2171
  • 16,104
  • 25
  • 61
  • 88
prabhats.net
  • 444
  • 6
  • 17
2

After trying different thing I ended up with this piece of code:

private MyClass currentItem = null;
private bool itemDirty = false; // can be used for "do you want to save?"

private void bindingSource_CurrentChanged(object sender, EventArgs e)
{
    var handler = new PropertyChangedEventHandler((s, e2) => itemDirty = true);

    var crnt = currentItem as INotifyPropertyChanged;
    if(crnt != null) crnt.PropertyChanged -= handler;

    currentItem = (MyClass)bindingSource.Current;

    crnt = currentItem as INotifyPropertyChanged;
    if(crnt != null) crnt.PropertyChanged += handler;

    itemDirty = false;
}

It works fine for me, although I save lots of state information in the Windows Form's instance fields. However, twiddling with CurrentChanged and CurrentItemChanged did not help me.

Matthias Meid
  • 12,455
  • 7
  • 45
  • 79
1

I set up a fairly simple mechanism, as follows:

  1. After binding my controls, I run a method that finds all the bound controls and saves their current values (I do a ReadValue() just to be sure I've got the values from the DataSource) in a Dictionary that maps a control to its value (there's a small method that gets the appropriate value for each kind of control that I have).
  2. I also add a change-event handler for each one (again, the specific event is determined by the type of control, but they all point to the same handler)
  3. The change-handler checks the current value against the Dictionary value. If it's different then it acts accordingly (in my case it switches the Close button for the Cancel button). If it's the same it checks all the other bound controls, so that if nothing is different it can switch Cancel back to Close; it's a nice feature of this method that it also recognizes when a change has been undone, even if it's by re-entering the original value.
  4. Before leaving, if there are changes to be saved I loop through the bound controls again to do WriteValue(), just in case WinForms didn't get around to propagating some change.

I can share the source if anyone is interested.

Michael
  • 1,351
  • 1
  • 11
  • 25
1

If your bindingsource uses a datatable you can do this :

    public bool HasChanges()
    {
        bool Result = false;

        myBindingSource.EndEdit();
        Result = ((DataTable)myBindingSource.DataSource).GetChanges(DataRowState.Modified) != null;


        return Result;
    }
GuidoG
  • 11,359
  • 6
  • 44
  • 79
1

I know this is an old post but here is an Extended BindingSource with IsDirtyFlag - you can adapt it how you would like - I pulled this code from another posting somewhere on the net years ago - made some very minor changes I think it was originally in VB - I converted to C# ..

using System.ComponentModel.Design;
using System.Windows.Forms;
using System.ComponentModel;
using System.Data;
using System;


public class BindingSourceExIsDirty : System.Windows.Forms.BindingSource, INotifyPropertyChanged
{

    #region "DECLARATIONS AND PROPERTIES"

    private string _displayMember;
    private DataTable _dataTable;
    private DataSet _dataSet;
    private BindingSource _parentBindingSource;
    private System.Windows.Forms.Form _form;
    private System.Windows.Forms.Control _usercontrol;




    private bool _isCurrentDirtyFlag = false;
    public bool IsCurrentDirty {
        get { return _isCurrentDirtyFlag; }
        set {
            if (_isCurrentDirtyFlag != value) {
                _isCurrentDirtyFlag = value;
                this.OnPropertyChanged(value.ToString());
                //call the event when flag is set
                if (value == true) {
                    OnCurrentIsDirty(new EventArgs());

                }
            }
        }
    }


    private string _objectSource;
    public string ObjectSource {
        get { return _objectSource; }
        set {
            _objectSource = value;
            this.OnPropertyChanged(value);
        }
    }


private bool _autoSaveFlag;

public bool AutoSave {
    get { return _autoSaveFlag; }
    set {
        _autoSaveFlag = value;
        this.OnPropertyChanged(value.ToString());
    }
} 

    #endregion

    #region "EVENTS"


    //Current Is Dirty Event
    public event CurrentIsDirtyEventHandler CurrentIsDirty;

    // Delegate declaration.
    public delegate void CurrentIsDirtyEventHandler(object sender, EventArgs e);

    protected virtual void OnCurrentIsDirty(EventArgs e)
    {
        if (CurrentIsDirty != null) {
            CurrentIsDirty(this, e);
        }
    }

    //PropertyChanged Event 
//  public event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged;

    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(string info)
    {

        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(info));
        }       

    }



    #endregion

    #region "METHODS"



    private void _BindingComplete(System.Object sender, System.Windows.Forms.BindingCompleteEventArgs e)
    {

        if (e.BindingCompleteContext == BindingCompleteContext.DataSourceUpdate) {

            if (e.BindingCompleteState == BindingCompleteState.Success & !e.Binding.Control.BindingContext.IsReadOnly) {
                //Make sure the data source value is refreshed (fixes problem mousing off control)
                e.Binding.ReadValue();
                //if not focused then not a user edit.
                if (!e.Binding.Control.Focused)
                    return;

                //check for the lookup type of combobox that changes position instead of value
                if (e.Binding.Control as ComboBox != null) {
                    //if the combo box has the same data member table as the binding source, ignore it
                    if (((ComboBox)e.Binding.Control).DataSource != null) {
                        if (((ComboBox)e.Binding.Control).DataSource as BindingSource != null) {
                            if (((BindingSource)((ComboBox)e.Binding.Control).DataSource).DataMember == (this.DataMember)) {
                                return;
                            }

                        }

                    }
                }
                IsCurrentDirty = true;
                //set the dirty flag because data was changed
            }
        }



    }

    private void _DataSourceChanged(System.Object sender, System.EventArgs e)
    {
        _parentBindingSource = null;
        if (this.DataSource == null) {
            _dataSet = null;
        } else {
            //get a reference to the dataset
            BindingSource bsTest = this;
            Type dsType = bsTest.DataSource.GetType();
            //try to cast the data source as a binding source
            while ((bsTest.DataSource as BindingSource != null)) {
                //set the parent binding source reference
                if (_parentBindingSource == null)
                    _parentBindingSource = bsTest;
                //if cast was successful, walk up the chain until dataset is reached
                bsTest = (BindingSource)bsTest.DataSource;
            }
            //since it is no longer a binding source, it must be a dataset or something else
            if (bsTest.DataSource as DataSet == null) {
                //Cast as dataset did not work

                if (dsType.IsClass == false) {
                    throw new ApplicationException("Invalid Binding Source ");
                } else {
                    _dataSet = null;

                }

            } else {
                _dataSet = (DataSet)bsTest.DataSource;
            }


            //is there a data member - find the datatable
            if (!string.IsNullOrEmpty(this.DataMember)) {
                _DataMemberChanged(sender, e);
            }
            //CType(value.GetService(GetType(IDesignerHost)), IDesignerHost)
            if (_form == null)
                GetFormInstance();
            if (_usercontrol == null)
                GetUserControlInstance();
        }
    }

    private void _DataMemberChanged(System.Object sender, System.EventArgs e)
    {
        if (string.IsNullOrEmpty(this.DataMember) | _dataSet == null) {
            _dataTable = null;
        } else {
            //check to see if the Data Member is the name of a table in the dataset
            if (_dataSet.Tables(this.DataMember) == null) {
                //it must be a relationship instead of a table
                System.Data.DataRelation rel = _dataSet.Relations(this.DataMember);
                if ((rel != null)) {
                    _dataTable = rel.ChildTable;
                } else {
                    throw new ApplicationException("Invalid Data Member");
                }
            } else {
                _dataTable = _dataSet.Tables(this.DataMember);
            }
        }
    }

    public override System.ComponentModel.ISite Site {
        get { return base.Site; }
        set {
            //runs at design time to initiate ContainerControl
            base.Site = value;
            if (value == null)
                return;
            // Requests an IDesignerHost service using Component.Site.GetService()
            IDesignerHost service = (IDesignerHost)value.GetService(typeof(IDesignerHost));
            if (service == null)
                return;
            if ((service.RootComponent as Form != null)) {
                _form = (Form)service.RootComponent;
            } else if ((service.RootComponent as UserControl != null)) {
                _usercontrol = (UserControl)service.RootComponent;
            }

        }
    }

    public System.Windows.Forms.Form GetFormInstance()
    {
        if (_form == null & this.CurrencyManager.Bindings.Count > 0) {
            _form = this.CurrencyManager.Bindings[0].Control.FindForm();

        }
        return _form;
    }

    /// <summary>
    /// Returns the First Instance of the specified User Control
    /// </summary>
    /// <returns>System.Windows.Forms.Control</returns>
    public System.Windows.Forms.Control GetUserControlInstance()
    {
        if (_usercontrol == null & this.CurrencyManager.Bindings.Count > 0) {
            System.Windows.Forms.Control[] _uControls = null;
            _uControls = this.CurrencyManager.Bindings[0].Control.FindForm().Controls.Find(this.Site.Name.ToString(), true);
            _usercontrol = _uControls[0];

        }
        return _usercontrol;
    }
    public BindingSourceExIsDirty()
    {
        DataMemberChanged += _DataMemberChanged;
        DataSourceChanged += _DataSourceChanged;
        BindingComplete += _BindingComplete;
    }

// PositionChanged
private override void _PositionChanged(object sender, EventArgs e)
{
    if (IsCurrentDirty) {
        // IsAutoSavingEvent
        if (AutoSave | MessageBox.Show(_msg, "Confirm Save", MessageBoxButtons.YesNo) == DialogResult.Yes) {
            try {
                //cast table as ITableUpdate to get the Update method
                //  CType(_dataTable, ITableUpdate).Update()
            } catch (Exception ex) {
                MessageBox.Show(ex, "Position Changed Error");
                // - needs to raise an event 
            }
        } else {
            this.CancelEdit();
            _dataTable.RejectChanges();
        }
        IsCurrentDirty = false;

    }
 base(e);
}


    #endregion

}
Ken
  • 2,518
  • 2
  • 27
  • 35
0

What I always do is to capture the individual "changed" events of the controls. In the below example I used a tabcontrol in this example. The Try/Catch is a dirty solution for not having to deal with all kinds of exceptions ;-)

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
    '
    ' some code        
    '
    BindingNavigatorSaveItem.Enabled = False
    For Each tabctl As Control In Me.TabControl1.Controls
        For Each ctl As Control In tabctl.Controls
            Try
                If ctl.GetType Is GetType(TextBox) Then
                    AddHandler DirectCast(ctl, TextBox).TextChanged, AddressOf GenDataChanged
                ElseIf ctl.GetType Is GetType(NumericUpDown) Then
                    AddHandler DirectCast(ctl, NumericUpDown).ValueChanged, AddressOf GenDataChanged
                ElseIf ctl.GetType Is GetType(ComboBox) Then
                    AddHandler DirectCast(ctl, ComboBox).SelectedValueChanged, AddressOf GenDataChanged
                ElseIf ctl.GetType Is GetType(CheckBox) Then
                    AddHandler DirectCast(ctl, CheckBox).CheckStateChanged, AddressOf GenDataChanged
                End If
            Catch ex As Exception
            End Try
        Next
    Next
End Sub

Private Sub GenDataChanged(sender As System.Object, e As System.EventArgs)
    BindingNavigatorSaveItem.Enabled = True
End Sub
Jeroen Bom
  • 41
  • 1
0

From my updated question I found I had to store a current version of the object at BeginEdit using Memberwise.Clone and then in CancelEdit I restored that to the current.

Jon
  • 38,814
  • 81
  • 233
  • 382
  • 6
    What exactly do you mean by "at BeginEdit"? The `BindingSource` doesn't have any `BeginEdit` and `EndEdit` events, does it? I'm trying to grab a copy of my object at the right time as well, but I'm struggling. :( – Matthias Meid Apr 28 '11 at 12:10
  • @Jon I know your post is old - but see my post - you might find it helpful. – Ken Aug 10 '16 at 15:44
0

I aren't sure if it was available when the question was asked but I use the grid_CurrentCellDirtyStateChanged; event

Kirsten
  • 15,730
  • 41
  • 179
  • 318
0

first make Sure you set DataSourceUpdateMode.OnPropertyChanged

txrFirstName.DataBindings.Add("Text", bindingSource1, "FirstName", false,DataSourceUpdateMode.OnPropertyChanged);

then add add this code to your movenext click event

 if (((DataRowView)bindingSource1.Current).IsNew)
                {
                MessageBox.Show("Current Row IsNew");
                }
            if (((DataRowView)bindingSource1.CurrencyManager.Current).Row.HasVersion(DataRowVersion.Proposed))
                {
                MessageBox.Show("Current Row Modified");
                DialogResult dialogResult = MessageBox.Show("Current Row Modified", "Some Title", MessageBoxButtons.YesNo);
                if (dialogResult == DialogResult.Yes)
                    {
                    //do something
                    ((DataRowView)bindingSource1.CurrencyManager.Current).Row.AcceptChanges();
                    }
                else if (dialogResult == DialogResult.No)
                    {
                    //do something else
                    ((DataRowView)bindingSource1.CurrencyManager.Current).Row.RejectChanges();
                    }


                }
            else { 
                bindingSource1.MoveNext();
                }