none
Maintaining the selected row after sorting a DataGridView

    Question

  • Hi,

    Simple question, hopefully there's a simple answer:

    How can I maintain the same selected row even after a sort on a DataGridView?

    In other words, after new items have been added to the binding source and a sort has been forcefully been carried out (in the custom BindingList), how can I ensure that the selected row is the same row as the one selected before the sort?

    Bear in mind, I'm doing the sorting in a custom BindingList so I can't just handle the Sorted or SortCompare events and implement my own logic there.

    Thanks,

    Jiten

    Saturday, March 11, 2006 4:54 PM

Answers

  • There are two issues.

    First, I realized that the ColumnHeaderMouseClick event is too late -- the grid has already sorted the column, so you'll need to use an event prior to the click -- CellMouseDown. You can check the RowIndex being -1 for mouse down actions on the column header cells. You just need to keep track of what rows are selected.

    The next issue is when handling the DataBindingComplete event -- while you can set a row as being selected with no problems, you probably really want to set the current cell as well. Setting the current cell in the DataBindingComplete event is ignored, so we have to set it after the event fires. The easiest way I know to do that is to use C# anonymous delegates and "post" the work that I want to do so that it will occur after the DataBindingComplete event.

    Here is my code for my CellMouseDown and DataBindingComplete event. I'm assuming that only one row is selected. Also note that there is a big difference between a row being selected and keeping track of what current cell is, unless you have full row selection enabled.


    private string customerID;
    private void customersDataGridView_DataBindingComplete(object sender, DataGridViewBindingCompleteEventArgs e)
    {
        if (!String.IsNullOrEmpty(customerID) && e.ListChangedType == ListChangedType.Reset)
        {
            int row = customersBindingSource.Find("Customer ID", customerID);
            customersDataGridView.BeginInvoke((MethodInvoker)delegate()
            {
                customersDataGridView.Rows[row].Selected = true;
                customersDataGridView.CurrentCell = customersDataGridView[0, row];
            });
        }
    }

    private void customersDataGridView_CellMouseDown(object sender, DataGridViewCellMouseEventArgs e)
    {
        if (e.RowIndex == -1)
        {
            customerID = customersDataGridView.SelectedRows[0].Cells["customerIDColumn"].Value.ToString();
        }
    }

     
    -mark
    DataGridView Program Manager
    Microsoft
    This post is provided "as-is"

    Wednesday, March 22, 2006 12:30 AM
  • You will have to record some info that can uniquely identify the row (primary key?) in the ColumnHeaderMouseDown event and then after the sort is done find the row. You'll want to check for the DataBindingComplete event with the ListChangeType of Reset to know when the sort is completed.
    -mark
    DataGridView Program Manager
    Microsoft
    This post is provided "as-is"
    Monday, March 13, 2006 4:08 AM

All replies

  • You will have to record some info that can uniquely identify the row (primary key?) in the ColumnHeaderMouseDown event and then after the sort is done find the row. You'll want to check for the DataBindingComplete event with the ListChangeType of Reset to know when the sort is completed.
    -mark
    DataGridView Program Manager
    Microsoft
    This post is provided "as-is"
    Monday, March 13, 2006 4:08 AM
  • Thanks for the suggestion Mark. Unfortunately it isn't working 100%.

    I've had to use the ColumnHeaderMouseClick event because I couldnt find the ColumnHeaderMouseDown.

    Seems as though it only works the first time a column is clicked for a sort. After this, it is the same previous row that is always coming up as the currently selected row (I've stepped through the debugger to verify this).

    Here are the exact steps i'm following:

    1. Create a local variable to store the Item ID.

    2. Handle the ColumnHeaderMouseClick event:

    2.1. Check that the Left mouse button was clicked.

    2.2. Assign the Item ID value of the currently selected row [1] to the local variable.

    3. Handle the DataBindingComplete event:

    3.1. Check that the ListChangeType is Reset.

    3.2. If the local variable has not been set then set it now (this is for when the DataGridView is first populated). If the local variable is set then search through all the rows and select the one that corresponds to it.

    [1] - I get the currently selected row with the following code:

    DataGridViewRow row = null;

    if (this.itemsDataGridView.SelectedRows.Count > 0)

    row = this.itemsDataGridView.SelectedRows[0];

    How does this sound to you?

    Also, on a similar note, I need to be able to maintain the selected row even after new rows have been added through the custom BindingList (note: I do a manual merge of items and then force a sort and OnListChanged [with ListChangedType.Reset] all within the custom BindingList). Any thoughts on how i coudl do this?

    Monday, March 13, 2006 5:22 AM
  • That looks good.

    After this, it is the same previous row that is always coming up as the currently selected row
    I'm not sure I follow what is going on here. Can you explain this a bit more? Also note you don't want to store off the DataGridViewRow, but some data in the row (like a field in the row that corresponds to the primary key).

    -mark
    DataGridView Program Manager
    Microsoft
    This post is provided "as-is"

     

    Monday, March 13, 2006 5:51 AM
  • Hi Mark,

    I am indeed storing a unique string value from the row's binded data.

    After this, it is the same previous row that is always coming up as the currently selected row

    In step 3.2, i mentioned that if the local variable has not been set then it is set for the first time there. Problem is, in step 2.2 the row always seems to return the same string ID as in step 3.2. This means that the first row is always the one being selected. Seems as though the SelectedRows is not being updated in time for the event. (?)

    On a side note, how do I make the row I select programmatically to be in focus (and visible without scrolling) in the DataGridView?

    Thanks,

    Jiten

    Monday, March 13, 2006 1:04 PM
  • Seems as though the SelectedRows is not being updated in time for the event.
    I'll get someone to investigate this possibility and report back.
    how do I make the row I select programmatically to be in focus (and visible without scrolling) in the DataGridView
    Set the CurrentCell property causes the grid to scroll that cell into view.

    -mark
    DataGridView Program Manager
    Microsoft
    This post is provided "as-is"

     

    Monday, March 13, 2006 4:50 PM
  • Thanks very much.

    Here's some more information on my databinding implementation (just in case):

    CustomSortableBindingList => BindingSource.DataSource => DataGridView.DataSource

    Cheers,

    Jiten

    Monday, March 13, 2006 5:08 PM
  •  

    this.itemsDataGridView.CurrentCell = rowToSelect.Cells[0];

    This still doesn't make the DataGridView automatically scroll to the selected row and bring it into view :(

    Monday, March 13, 2006 5:19 PM
  • Hi Mark,

    I'm stil having problems sorting out these issues.

    I'd appreciate any further help.

    Thanks,

    Jiten

     

    Tuesday, March 21, 2006 8:46 PM
  • There are two issues.

    First, I realized that the ColumnHeaderMouseClick event is too late -- the grid has already sorted the column, so you'll need to use an event prior to the click -- CellMouseDown. You can check the RowIndex being -1 for mouse down actions on the column header cells. You just need to keep track of what rows are selected.

    The next issue is when handling the DataBindingComplete event -- while you can set a row as being selected with no problems, you probably really want to set the current cell as well. Setting the current cell in the DataBindingComplete event is ignored, so we have to set it after the event fires. The easiest way I know to do that is to use C# anonymous delegates and "post" the work that I want to do so that it will occur after the DataBindingComplete event.

    Here is my code for my CellMouseDown and DataBindingComplete event. I'm assuming that only one row is selected. Also note that there is a big difference between a row being selected and keeping track of what current cell is, unless you have full row selection enabled.


    private string customerID;
    private void customersDataGridView_DataBindingComplete(object sender, DataGridViewBindingCompleteEventArgs e)
    {
        if (!String.IsNullOrEmpty(customerID) && e.ListChangedType == ListChangedType.Reset)
        {
            int row = customersBindingSource.Find("Customer ID", customerID);
            customersDataGridView.BeginInvoke((MethodInvoker)delegate()
            {
                customersDataGridView.Rows[row].Selected = true;
                customersDataGridView.CurrentCell = customersDataGridView[0, row];
            });
        }
    }

    private void customersDataGridView_CellMouseDown(object sender, DataGridViewCellMouseEventArgs e)
    {
        if (e.RowIndex == -1)
        {
            customerID = customersDataGridView.SelectedRows[0].Cells["customerIDColumn"].Value.ToString();
        }
    }

     
    -mark
    DataGridView Program Manager
    Microsoft
    This post is provided "as-is"

    Wednesday, March 22, 2006 12:30 AM
  • That works perfectly. Thanks very much!


    Just a note: I had to implement a Find method in my underlying custom BindingList so that the int row = customersBindingSource.Find("Customer ID", customerID); could work. More information on this can be found here: Custom Data Binding, Part 3 - Adding Search Support


    I'm starting to really appreciate the depth of the DataGridView control now.

    There is however one more issue regarding sorting and selection:

    In my BindingList implementation I have an Update(..) method which is used when the underlying data source has changed. This method merges any new items and performs a sort on the new list THEN raises the ListChangedEvent with ListChangedType.Reset so that the DataGridView can refresh with the new items. Problem is I lose the selection again. I've tried recording the currently selected row ID in the DataGridView's SelectionChanged event handler but this doesnt do the trick.

    It would be great if could get this working because at the moment it's a really bad user experience when new items come in.

    Thanks very much for all your help so far.

    Jiten

    Wednesday, March 22, 2006 1:54 AM
  • I'm not sure I know where the problem is. I tried something similar -- In the SelectionChanged event I remember the customer ID like so:



    private void customersDataGridView_SelectionChanged(object sender, EventArgs e)
    {
        if (customersDataGridView.SelectedRows.Count > 0)
        {
            customerID = customersDataGridView.SelectedRows[0].Cells["customerIDColumn"].Value.ToString();
        }
    }

     
    I use the same DataBindingCompleted code as above. Next I put a button on my form that calls resetbindings and selection/focus remains correct. Can you debug your code a bit and ensure you are getting the correct "id" value. Also ensure that in your "Find" method you lookup the value using the new list.

    -mark
    DataGridView Program Manager
    Microsoft
    This post is provided "as-is"

    Wednesday, March 22, 2006 2:59 AM
  • Hi Mark,

    After stepping through the debugger (with checkpoints on SelectionChanged and DataBindingComplete event handlers) i've noticed that the SelectionChanged handler gets called twice BEFORE the DataBindingComplete (when my underlying BindingList calls OnListChanged). This means that the old selected row value is being lost.

    Also, i've checked that the Find method is looking up the value in the new list and this works fine.

    Thanks,

    Jiten

    Wednesday, March 22, 2006 12:20 PM
  • Do you have your custom bindinglist available where you can send it to me so I can try this out also? I'm curious specifically on the SelectionChanged being fired twice. Look in the debugger and check the dgv.SelectedRows.Count property. When the grid receives a reset it actually removes all rows and then rebinds to the "new" list, so there might be an extra SelectionChanged event but the count of rows selected would be 0.

     

    -mark

    DataGridView Program Manager

    Microsoft

    This post is provided "as-is"

    Wednesday, March 22, 2006 5:01 PM
  • Hi Mark,

    I've emailed you the custom binding list implementation.

    You're right about the extra SelectionChanged events, where the count of the rows selected is 0. In total, the event fires 3 times when a reset occurs.

    Also, when the DGV first loads up, and is bound to, SelectionChanged fires 7 (!) times. Mind you, I do re-initialise the bindingsource at runtime when it first loads so this causes some additional events to be fired.

    Thanks,

    Jiten

    Wednesday, March 22, 2006 5:23 PM
  • Updating this thread. Basically the issue is that when you perform the Merge, the DataGridView receives and processes the ListChange reset before it raises the DataBindingComplete event. As part of that processing the DataGridView sets the current cell and selection. So, in your code you when you call the Merge, the SelectionChanged event fires before you receive the DataBindingComplete event with Reset ListChangeType. This resets your “id” variable, so when the DataBindingComplete event (with Reset) does get called you have lost the real row id.

    You can do a few things here – you can override OnListChanged on your FeedItemsBindingList and create and raise a new event before calling base.OnListChanged(). Your app code would listen for this new event in place of where it was listening to the DataGridView.DataBindingComplete event. Alternatively, you can just set a variable “waitForReset” to true before calling the Merge function, check for the waitForReset value inside of selection changed (only reset the row ID when waitForReset is false) and then inside your DataBindingComplete event handler reset the waitForReset to false. Here is my code for this:


    private string uniqueID = String.Empty;
    bool waitForReset = false;

    private void dataGridView1_DataBindingComplete(object sender, DataGridViewBindingCompleteEventArgs e)
    {
        if (!String.IsNullOrEmpty(uniqueID) && e.ListChangedType == ListChangedType.Reset)
        {
            waitForReset = false;
            int row = bs.Find("MyProperty", uniqueID);
            dataGridView1.BeginInvoke((MethodInvoker)delegate()
            {
                dataGridView1.Rows[row].Selected = true;
                dataGridView1.CurrentCell = dataGridView1[0, row];
            });
        }
     
    }
     
    private void dataGridView1_SelectionChanged(object sender, EventArgs e)
    {
        if (dataGridView1.SelectedRows.Count >0 && !waitForReset)
        {
            uniqueID = dataGridView1.SelectedRows[0].Cells[0].Value.ToString();
        }
     
    }
     
    private void button1_Click(object sender, EventArgs e)
    {
        waitForReset = true;
        fbs.Merge(mergeCol);
    }

     
    -mark
    DataGridView Program Manager
    Microsoft
    This post is provided "as-is"

    Thursday, March 23, 2006 5:49 PM
  • I don't know why this isn't all done internally by the datagridview. Wouldn't it be simple to keep the "DataBoundItem" of each selected row before the datagridview is sorted and reset it yourself than have every DGV user hack the thing so that it works properly?

    What a bad control this is, looks like were back to the MFC coding way where you have to handle so many basic things yourself. Oh well...
    Thursday, October 05, 2006 7:20 PM
  • Not to dig up an old post or anything, but after reading this thread a little I was able to implement reselecting rows after sort.  The method I used was not exactly as described here.  I simply used the datagridview_sorted event, like so:

        Private Sub dgEquipment_CellMouseDown(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewCellMouseEventArgs) Handles dgEquipment.CellMouseDown '
            ' remember the currently selected row's MasterID
            If e.RowIndex = -1 Then
                If e.Button = Windows.Forms.MouseButtons.Left Then
                    SelectedRowID = dgEquipment.SelectedRows(0).Cells(0).Value
                End If
            End If
        End Sub

        Private Sub dgEquipment_Sorted(ByVal sender As Object, ByVal e As System.EventArgs) Handles dgEquipment.Sorted
            ' reselect the row that was selected before the sort by locating the MasterID
            For Each r As DataGridViewRow In dgEquipment.Rows
                If r.Cells(0).Value = SelectedRowID Then
                    r.Selected = True
                End If
            Next
        End Sub

    Keep in mind my datagridview does not allow multirow selection, but with some additional code you should be able to support it.  When trying to bring focus to the row I noticed a dilemma with the CurrentCell property where if you try to set the CurrentCell to the same cell that is already selected, nothing will happen.  So I used this:

        ' FocusCurrentRow
        ' Brings the currently selected row in the datagridview into focus by setting the current cell to one not already selected.
        Sub FocusCurrentRow()
            If dgEquipment.CurrentCell.ColumnIndex = 0 Then
                dgEquipment.CurrentCell = dgEquipment.SelectedRows(0).Cells(1)
            Else
                dgEquipment.CurrentCell = dgEquipment.SelectedRows(0).Cells(0)
            End If
        End Sub

    The datagridview control should handle these things automatically but sadly that doesn't appear so.
    Wednesday, February 07, 2007 5:34 PM
  •  

    I had the similar problem and I resolved it by using GetHashCode() function.

    Here are my steps.

    1) Before sorting, use GetHashCode() on the cell and strore it locally.

    2) After sorting, look for the cell, which has the same hashcode and there you can get the new row number.

    In my case, I was listening to cellpaint event and it was easy for me to just compare the hashcodes.

    I was surprised that there is no DisaplyIndex property in the DataGridViewRow, which is available in

    DataGridView column.

    Tuesday, September 30, 2008 5:22 PM
  • Hi
    Try "DataGridView

    .FirstDisplayedScrollingRowIndex"


    KumarShanmugam
    Wednesday, September 09, 2009 11:04 AM
  • Please note that GetHashCode() is not a replacement for Equals(). Multiple unequal objects may return the same value on GetHashCode(). Sounds like you’ve just been lucky ;-). I think that the IBindingList.Find()/BindingSource.Find() method the OP speaks of over some column with a unique ID is the least hacky way to find rows that need to be reselected.

    Tuesday, July 29, 2014 2:27 PM