I thought I would conclude (?) this post by showing the code changes that I have made and explaining what the changes actually do. This has some relevance to what Chris is doing, trying to keep on top of RowChanged events and ensuring that we do not fire duplicate events un-necessessarily. Some of the techniques I have used may be useful elsewhere, because getting events in the right order is tricky.
Before I go into detail, you need to understand that there are two critical form-level variables that need to be constantly up to date and in synch: FPreviouslySelectedDetailRow is the DataRow object that is usually our current row. It is the dataRow that we are working on. Then FCurrentRow is the actual integer row in the grid that is the one that corresponds to that. We must be sure never to let these get mismatched. You might think this is quite easy - but believe me - not when the grid is sorted, either manually by clicking on the row header, or programatically as a requirement of the screen.
The first issue I resolved was to do with clicking the sort header. This causes the data grid to do a lot of work and fire events - but the key thing is that the current data row remains unchanged. The grid works out the new position and selects it. As a result we get a FocusRowChanged event, which contains a vital piece of information - the new row number, which we need to ensure is safely stored in FCurrentRow. Apart from that the goal is to not change anything in the grid or the details panel because whatever the grid does with re-ordering data rows we will end up displaying the same data. So the code that handles this is here.
private void FocusedRowChanged(System.Object sender, SourceGrid.RowEventArgs e)
{
if(e.Row != FCurrentRow && !grdDetails.Sorting)
{
// Transfer data from Controls into the DataTable
if (FPreviouslySelectedDetailRow != null)
{
GetDetailsFromControls(FPreviouslySelectedDetailRow);
}
// Display the details of the currently selected Row
FPreviouslySelectedDetailRow = GetSelectedDetailRow();
ShowDetails(FPreviouslySelectedDetailRow);
pnlDetails.Enabled = true;
}
FCurrentRow = e.Row;
}
private void GetDetailsFromControls(ADailyExchangeRateRow ARow)
{
if (ARow != null && !grdDetails.Sorting)
{
ARow.BeginEdit();
ARow.FromCurrencyCode = cmbDetailFromCurrencyCode.GetSelectedString();
ARow.ToCurrencyCode = cmbDetailToCurrencyCode.GetSelectedString();
ARow.DateEffectiveFrom = (dtpDetailDateEffectiveFrom.Date.HasValue?dtpDetailDateEffectiveFrom.Date.Value:DateTime.MinValue);
ARow.RateOfExchange = Convert.ToDecimal(txtDetailRateOfExchange.NumberValueDecimal);
GetDetailDataFromControlsManual(ARow);
ARow.EndEdit();
}
}
This uses a new property of the grid - grdDetails.Sorting which is true if the grid is doing its sorting algorithm. As you can see, if the grid is sorting, the only thing we do is update FCurrentRow. We neither supply any details to the grid (from GetDetailsFromControls), nor do we change any details in the details pane as a result of the row change event (FocusRowChanged). If you supply details to the grid it is highly likely to go into the wrong row, and if you respond to the row changed event you are highly likely to be given the wrong row to get it from!
The second issue arises during Validation. On a sorted grid, validation can cause our data row to move to a new location - but the selection does not change unless we make sure that it follows our data. So the new code that achieves this is here.
private bool ValidateAllData(bool ARecordChangeVerification, bool AProcessAnyDataValidationErrors)
{
bool ReturnValue = false;
if (FPreviouslySelectedDetailRow != null)
{
GetDetailsFromControls(FPreviouslySelectedDetailRow);
// Remember the current rowID and perform automatic validation of data, based on the DB Table specifications (e.g. 'not null' checks)
int previousRowNum = FCurrentRow;
ValidateDataDetailsManual(FPreviouslySelectedDetailRow);
// Validation might have moved the row, so we need to locate it again, updating our FCurrentRow global variable
FCurrentRow = grdDetails.DataSourceRowToIndex2(FPreviouslySelectedDetailRow) + 1;
if (FCurrentRow != previousRowNum)
{
// Yes it did move so we need to keep the row selected, without firing off the event that brought us here in the first place!
grdDetails.Selection.FocusRowLeaving -= new SourceGrid.RowCancelEventHandler(FocusRowLeaving);
grdDetails.SelectRowInGrid(FCurrentRow);
grdDetails.Selection.FocusRowLeaving += new SourceGrid.RowCancelEventHandler(FocusRowLeaving);
}
There are several things to understand here. First we need to get the data from the controls into our current DataRow object.
Then we need to remember what our current row is from FCurrentRow. We need this to test if the row has moved after validation.
Then we do the validation actions.
Then we have to find our row again, setting FCurrentRow to the returned value. This uses a new overload to the DataSourceRowToindex2 method which can take a DataRow as an argument in place of a DataRowView.
If the row of data did actually move to a new location we need to make sure we highlight the new location without firing off any events.
Then the code continues as before.
There is one final quirky thing we have to do on New Records. There is always one row that you can highlight in a sorted grid, that when you add a new row will result in the new row being displayed at the same location in the grid as the row that was highlighted before the New. When this occurs, there will be NO RowChanged event fired and as a result the details panel will still hold the previously displayed data and not the data from the new row. So we have to check if the grid row does change when the new row is added and if it does not we have to simulate a RowChange event. Here is the code.
public bool CreateNewADailyExchangeRate()
{
if(ValidateAllData(true, true))
{
int previousGridRow = grdDetails.Selection.ActivePosition.Row;
ADailyExchangeRateRow NewRow = FMainDS.ADailyExchangeRate.NewRowTyped();
FMainDS.ADailyExchangeRate.Rows.Add(NewRow);
FPetraUtilsObject.SetChangedFlag();
grdDetails.DataSource = new DevAge.ComponentModel.BoundDataView(FMainDS.ADailyExchangeRate.DefaultView);
grdDetails.Refresh();
SelectDetailRowByDataTableIndex(FMainDS.ADailyExchangeRate.Rows.Count - 1);
int currentGridRow = grdDetails.Selection.ActivePosition.Row;
if (currentGridRow == previousGridRow)
{
// The grid must be sorted so the new row is displayed where the old one was. We will not have received a RowChanged event.
// We need to enforce showing the new details.
FPreviouslySelectedDetailRow = GetSelectedDetailRow();
ShowDetails(FPreviouslySelectedDetailRow);
}
return true;
}
else
{
return false;
}
}
This code is the same as we have used up to now except that it checks the grid row before the addition (previousGridRow) and gets the grid row after the addition (currentGridRow). If they are the same it calls GetSelectedDetailRow() and sets our Form varaiable to that value and then shows the details for the new row.
None of these changes, as far as I know, will need to fire off any additional events - indeed the whole point of the changes has been to try to stop events being acted upon in such a way as to cascade any new events, and usually they try to take action without firing additional ones. But equally these changes probably do not address any existing issues with multiple events that are nothing to do with quirks of sorted behaviour. If they still exist - and that is what Chris is working on - then we should fix those issues too.
By the way, if you are reading this and are interested, on a different (but related) subject - when I was looking into the data that we submit to the server when we save changes - I did, as Christian thought, confirm that every time we select a row in the grid but do not edit it, we do indeed submit that row to the server as a 'changed' row, even though it contains the same data as before. How often when editing do you think we would select lots of detail rows that we do not change - basically a problem for keyboard users rather than mouse users I guess. But that is another story!