Detect column reordering - ColumnDisplayIndexChanged raises multiple times

Viewed 948

I am working on a .net 4.6.1 C# winforms project that has a datagridview where users can change the order of columns.

I would like to store the new order in a db table, but have trouble finding the right event for detecting when a user changed the order of the columns.

After searching here, I was pointed to the DataGridView.ColumnDisplayIndexChanged event in this thread. But that one does not solve my issue. (it only gives a solution for multiple events when you fill the datagrid view, but that is answered easily by adding the handler after setting the datasource)

That sort of works, but gets fired multiple times when a user changes the order of columns (it f.e. looks like when changing columns A,B,C,D to D,A,B,C the event gets fired 3 times (probably for A,B,D,C - A,D,B,C - D,A,B,C)

I am having a hard time finding out how I can detect if the event is the final one (since I don't want to store all these new orders, only the final one)

My questions are:

  1. Is this event the 'best' one to use for my case?

  2. If so, how can I detect the final ColumnDisplayIndexChanged event (D,A,B,C)?

2 Answers

When you reorder columns, ColumnDisplayIndexChanged will raise for all the columns which their display index has been changed. For example if you move colum A to the position after C, the event will raise for all those three columns.

There is a solution to catch the last one. DataGridViewColumn has an internal property called DisplayIndexHasChanged which is true if the event should be fired for the column. The private method which raise the event, looks into list of the columns and for each column if that property is true, first sets it to false, then raises the event. You can read internal implementations here.

You can check if there is no column having DisplayIndexHasChanged with true value, you can say it's the last event in the sequence:

private void dgv_ColumnDisplayIndexChanged(object sender, DataGridViewColumnEventArgs e)
{
    var g = (DataGridView)sender;
    var property = typeof(DataGridViewColumn).GetProperty("DisplayIndexHasChanged",
        System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
    if (g.Columns.Cast<DataGridViewColumn>().Any(x => (bool)property.GetValue(x)))
        return;
    else
        MessageBox.Show("Changed");
}

Just keep in mind, you should disable capturing that event when you add columns:

private void f_Load(object sender, EventArgs e)
{
    LoadData();
}
void LoadData()
{
    dgv.ColumnDisplayIndexChanged -= dgv_ColumnDisplayIndexChanged;
    dgv.DataSource = null;
    var dt = new DataTable();
    dt.Columns.Add("A");
    dt.Columns.Add("B");
    dt.Columns.Add("C");
    dgv.DataSource = dt;
    dgv.ColumnDisplayIndexChanged += dgv_ColumnDisplayIndexChanged;
}

My suggestion would be not to do any custom logic to find out if its the last one or something along those lines. The best approach would be to save after each event but you can debounce it.

Using a debounce approach you can cancel the old event if the new event is fired right after depending on some amount of time you wish to allow inbetween calls.

Ex: write to storage only if there is no new event after lets say 1 second or 5 seconds depending on what is accepteable for your application

Say we decide to save with a debounce of 1 second

First event occurs you trigger the action which has 1 second to execute

If another event is triggered the old action is ignored and the new action now has 1 second to execute and so on for other sequential actions

public static Action Debounce(this Action func, int milliseconds = 300)
{
    var last = 0;
    return arg =>
    {
        var current = Interlocked.Increment(ref last);
        Task.Delay(milliseconds).ContinueWith(task =>
        {
            if (current == last) func(arg);
            task.Dispose();
        });
    };
}

Assuming the following action below for saving your data Action a = (arg) => { save my data here };

first assign the debouncer to your action

var debouncedWrapper = a.Debounce(1000); //1 sec debounce

Then you can use it as follows

public void datagridchangeevent(object sender, Event e)
{
  debouncedWrapper()
}

This will ignore sequential calls and the aciton will be executed only if nothing is called for one second

Related