Friday, February 24, 2006

DataGridView - SelectionChanged escapade

We use the new .NET 2.0 DataGridView extensively in my current project, in combination with object bindingsources. We have several user-controls where the grid is the position/current source for other controls in the UC, e.g. master-details grids, selected grid-row edit form, etc. All of our grids are in single select/FullRowSelect mode. Thus, the when the selected row changes, so do all the controls bound to the grid (actually bound to the same bindingsource as the grid).

The SelectionChanged event of the DataGridView tells you when the selected row changes, and this event trigger quite often, in fact more often than you would expect. It triggers during:

  • binding: the first row always gets selected
  • unloading: the selections are cleared
  • resize: the selections are first cleared, then all of them gets selected again
  • sorting: the selections are first cleared, then the grid selects whatever rows got the same row index in the grid, which most likely are not the same records that were selected before sorting
Typically, a user-control gets instantiated and initialized by its parent, and then the parent does .Dock=Fill on the UC. Thus, the the SelectionChanged event will trigger three times during load: when a user-controls gets loaded, the DataGridView gets bound and the first row gets selected (first), then the resize event causes unselecting the row (second) and then the reselection og the first row (third).

We do layz loading of the details data, i.e. we do not access the application services to get the detail data until the user selects a row in the grid. I noted that the UC took a long time to load, and when I added a breakpoint to see why, I was quite surprised when the data got loaded three times during load. The extra load was caused by me setting a selected row after binding, as this had to be done in .NET 1.1 grids from Microsoft, but is no longer needed as the DataGridView always select the first row. I wish there was an option on the grid to turn the auto-select off.

Thus, what is needed is some extra logic to remember which record is the current in the binding source (note: not which row in the grid), then check in the SelectionChanged event whether the current record actually was changed, or whether it is just the eager event triggering. Use a helper class member to keep the ID of the selected record. Do your data loading only when the current record changes, otherwise, just rebind to the already fetched data.

The next (expected) surprise I got was when I added sorting to the binding source of the DataGridView. The same effect happended as during the load resizing: the selections got cleared and then set again, but it does not select the same records again, it only selects the same rows as before. As the records are now sorted a different way, the record previously at row 4 is now e.g. at row 17, but the grid "dutifully" selects row 4 anyway. Thus, you need to implement some find logic, or just call .ClearSelection() on the grid.

I chose to clear the selection after sorting, and wanted to suppress the reselect SelectionChanged event that happens during sorting, as this caused unnecessary binding to random record which I would immediately clear. The grid has a Sorted event that happens when the sorting has completed and after the selection events, but there is no begin sorting event. I needed another helper class member to tell the SelectionChanged event hander that the grid is currently sorting. I thought the ColumnHeaderMouseClick event would let med set an _isSorting boolean flag, but this event happens after the Sorted event, far to late for my use.

I ended up using the MouseDown event and some classical x,y hit testing to deduce that a sort operation was about to begin:

Private Sub dgwMaster_MouseDown(ByVal sender As System.Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles dgwMaster.MouseDown

Dim hti As DataGridView.HitTestInfo = dgwMaster.HitTest(e.X, e.Y)
If hti.Type = DataGridViewHitTestType.ColumnHeader Then
_isSorting = True
End If

End Sub


Private Sub dgwMaster_Sorted(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles dgwMaster.Sorted

_isSorting = False

End Sub

The final surprise when it comes to binding is about the impedance difference between object binding sources and the DataGridView: a DataGridView can have no selection (just CTRL+click a selected row), while an object bindingsource always have a .Current item when the source is not empty. Setting .Postion to -1 on a bindingsource has no other effect that selecting the first item in the binding source. Thus, to allow for no selection in bound controls, you have to remove the .DataSource of those controls temporarily when the grid have no selection.
I wish there was an option to enforce the grid to always have a selection. You can make a dervied grid and override MouseDown and then not call the base class OnMouseDown method to cancel the mouse click.

Due to this impedance difference, always remember to check that a row is selected in the grid before using the bindingsource .Current property to get the currently selected record:

If _isSorting = True Then Exit Sub


'ensure that a row is selected

If dgwMaster.SelectedRows.Count = 0 Then

dgwDetails.DataSource = Nothing

Else

Dim response As OrderDetailsResponse

'get details

Dim master As Order = CType(OrderBindingSource.Current, Order)

If master.orderId = _lastOrderId Then

'avoid reget on resize, just reconnect bindingsource

dgwDetails.DataSource = OrderOverføringBindingSource

Else

_lastOrderId = master.orderId

response = _orderMgr.GetOrderDetails(master.orderId)

_details = response.ItemList

If dgwDetails.DataSource Is Nothing Then

'sorting changes current master, need to reconnect bindingsource

dgwDetails.DataSource = OrderDetailsBindingSource

End If

End If

End If


No wonder that the DataGridView program manager Mark Rideout is quite busy answering questions at the Windows Form DataBinding forum.

9 comments:

bl0gg32 said...

I am using the BindingSource component's "PositionChaged" event instead of GridViews "SelectionChanged" event in a quite similar single-row-select scenario...

Private Sub EventsBindingSource_PositionChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles EventsBindingSource.PositionChanged

If Me.EventsBindingSource.Position < 0 Then
Return
End If

Try
Me.BeginProcess()
Me.LoadEventDetails(Me.CurrentEvent.EventID)
Me.SetStatus()
Catch ex As Exception
UIManager.HandleError(Me, ex)
Finally
Me.EndProcess()
End Try
End Sub

You might want to do that...

However, I am having another issue about User Control DataBinding.

I want to use the Designer... So I add a Bindable property to my user control , and then drag drop this user control on my master form.

(This is in a c# winforms usercontrol)

private string myKey = "";

[bindable(true)]
public string Key
{
get{ return myKey;}
set
{
if(value !=null)
{
myKey = value;
this.LoadDetailData();
}
}
}

When I bind this property to a field in the BindingSource on my master form, my LoadDetailData Method gets called twice during data binding of the master form...

I debugged it carefully... When I load Data into my master form, the value is set actually 4 times... but first two are null, so I was able to eliminate it easily.

And here is how I load data to my master form :

private dataset myData ; // initialized in InitilizeComponent by the designer

private DataSet GetData()
{
// here is the retrieval code;
}

private void LoadData()
{
this.mydata.Merge(GetData());
}

so merging the data causes the BindingSource to re-set the property 4 times ...

Any ideas?

Kjell-Sverre Jerijærvi said...

You're quite right about the bindingsource event, it is a lot simpler. But then there would always be a selection, and in our case we have to allow/support no selection in the master grid, thus we're stuck with the trigger-happy DataViewGrid selection changed.

Anonymous said...

Good to see that I'm not the only one struggling for hours and hours with the SelectionChanged event...

Anonymous said...

Are you doing a Fill? Set ClearBeforeFill to false. WaLa

Anonymous said...

Please, any sample source code, all datagridview

Thanks.

enrique.prados@a-e.es

hypo said...

I am truly mad that the winform guys at microsoft still didn't figure this out. I also have a No Selection scenario and I would rather work on the BS then the Grid, but The fact that the Position is pointing to the first row when there is no selection is freaking me out!

The Position can contain -1 when there is no resultset, so why can't it be -1 when there is no selection.

I'm gonna try to work with flags ...

Good luck to all struggling with this sh*t ;)

Unknown said...

here is the way I cancel selection changed event during datasource refresh ;)

Unknown said...

I think I forgot the code ;)

dataGridView1.SelectionChanged -= dataGridView1_SelectionChanged;
View.ApplyFilter(f => f.Match(filters));
dataGridView1.SelectionChanged += dataGridView1_SelectionChanged;

Alex said...

I had the same issue.
I solved it for me with a workaround by testing, if the SelectionChange was because of a mousebutton.

Private Sub DataGridView_SelectionChanged(...)
If DataGridView.MouseButtons = Windows.Forms.MouseButtons.Left Then...
Get the the Index or wathever
End If
End Sub

PS: Sorry for doublepost :-)