3

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)?

Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
Montfrooij
  • 85
  • 9
  • You can use `Form.Closing`, this way you only commit changes once, not every time when user change ordering. – Sinatr Dec 19 '18 at 10:43
  • Thanks for the suggestion @Sinatr. I have thought about that. But that will still require a lot of data, since users will close forms frequently (and most forms have several datagridvies) – Montfrooij Dec 19 '18 at 10:49
  • Yes, you problem is saving not finding out when to save. I cant propose an answer because it was closed but i can point you to the right way. Try to use a debouncer and set the timout to what you need to. So when the event is changed debounce the action let say 5 seconds or 1 second based on whats accepteable to you. So sequenctial changes for the same thing will be halted into one final change. If the event is called 5 times for one change. The debouncer will only call the last one so you only save once. A debouncer function can be found here – npo Dec 19 '18 at 10:49
  • A debounce action can be found here https://stackoverflow.com/questions/28472205/c-sharp-event-debounce. – npo Dec 19 '18 at 10:51
  • Thanks @npo, I think I re opened the question by changing it (as it was not a duplicate in my opinion) – Montfrooij Dec 19 '18 at 10:53
  • *"if the event is the final one"* - you can simply delay the action by e.g. using `Timer` and restarting it after each event is fired. This way several events one after another will lead to just one commit. – Sinatr Dec 19 '18 at 10:56
  • I posted an answer, you might need to tweak it a little, but should work and in my opinion a debouncer is the way to go here. So you only save if on last event for multiple events firing at the same time – npo Dec 19 '18 at 10:56
  • I reopened the question. While I believe the question is valid, but I'd suggest saving the settings before closing the form. – Reza Aghaei Dec 19 '18 at 10:58
  • While saving on form close is valid, the application could be terminated forcefully, for any number of reasons and the changes wouldn't be saved anymore for that case – npo Dec 19 '18 at 11:00
  • Then an auto save feature on some configurable intervals is another option. – Reza Aghaei Dec 19 '18 at 11:02
  • Saving at close time is a must, IMO. Saving in intervals is optional. – Reza Aghaei Dec 19 '18 at 11:03
  • @RezaAghaei, Thanks for re opening and the suggestion. My issue with that approach is that the user probably won't make many changes (so I won't need to update the table with custom column order that often). If I save on every form closing, I will make a lot of insert / updates to this table that are totally unneeded (and checking if needed will also create a lot of data selecting.) I really like to minimize the communication between the app and SQL when possible – Montfrooij Dec 19 '18 at 11:04
  • A flag on your form which will be set to true by `ColumnDisplayIndexChanged` will prevent from unnecessary database call. (The flag change should be disabled during loading data). – Reza Aghaei Dec 19 '18 at 11:05
  • @RezaAghaei I have thought about that indeed. There is a couple of things I ran into with that approach. Mainly because my 'DataGridView' logic is in a general class (try to work OOP) and I could not find a good way to do that in the general class without having to add that flag on all the forms (not really OOP).Also I have multiple dgv's on some forms. And if you have one dgv that changes, you still will store all. – Montfrooij Dec 19 '18 at 11:17

2 Answers2

2

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;
}
Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
  • 1
    Great answer that clearly explains the inner workings of the DataGridView's column headers: when the user drags one to a different location, not only one, but a _series_ of events is triggered for _every_ column header affected by the dropped column's new position (i.e. one for every column _between_ the old and the new location). – Fabien Teulieres Mar 23 '21 at 07:51
0

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

npo
  • 1,060
  • 8
  • 9
  • I will give this answer a go. I am still in doubt if this is 'the best' option for me, but time will tell. If I find better ways (for me) I will also post that here. – Montfrooij Dec 19 '18 at 11:33