SourceGrid specification and testing: Difference between revisions

From OpenPetra Wiki
Jump to navigation Jump to search
 
(50 intermediate revisions by the same user not shown)
Line 1: Line 1:
== Important ==
This page has been extensively revised in November 2013.  The content here describes the '''current implementation of the SourceGrid'''.  You can still read the previous information that applied prior to this date [[SourceGrid specification and testing (Prior to Nov 2013)|by following this link]]
== Introduction ==
OpenPetra makes extensive use of the SourceGrid control available from http://sourcegrid.codeplex.com/.  This open source control has many desirable features and is a fairly complex piece of code written by David Icardi.  We currently use version 4.40 published on 16 July 2012.  The source code comes in a ZIP file titled '''SourceGrid_4_40_src.zip'''.  A few people in the OpenPetra team have this file although it is not kept in our repository, due to its size.
OpenPetra makes extensive use of the SourceGrid control available from http://sourcegrid.codeplex.com/.  This open source control has many desirable features and is a fairly complex piece of code written by David Icardi.  We currently use version 4.40 published on 16 July 2012.  The source code comes in a ZIP file titled '''SourceGrid_4_40_src.zip'''.  A few people in the OpenPetra team have this file although it is not kept in our repository, due to its size.


With the exception of the two bugs described below, we are able to use this control 'out-of-the-box'.  However, using the control successfully requires some careful programming and some knowledge of how the events that are fired by the control can best be used by OpenPetra.
Over the course of time we have discovered a few 'simple bugs' in the code, which were easy to fix.  However, as more and more features have been introduced into OpenPetra, we have begun to find it harder to work with the way that events are fired from the grid control. So we made a decision in October 2013 to 'fork' the grid code so that we could make it more compatible with the way we workArguably the OpenPetra use-case is unusual in that we use the grid to display records which are edited and validated in separate controls.  This adds complexity to the integration of the GUIOver the previous twelve months we had incorporated the Delete functionality (including multi-row delete), Filter/Find functionality which changes the grid content and row selection and some limited edit-in-place cases.  All these new capabilities placed more and more stress on our use of grid events.
 
First we will explain the two bug fixes, then we will examine how to code for the events that we useIn the third section we outline some of the commonest methods associated with the grid that you will want to use in your manual codeIn the final section we will describe how to test that any changes that are made to the grid or to validation have not caused new bugs to appear.


On this page we will first explain the 'simple' bug fixes.  In the second section we explain the changes to the events that have to do with Focus, Selection and Highlighting.  This will lead on to consideration of how to code for the events that we use.  In the fourth section we outline some of the commonest methods associated with the grid that you will want to use in your manual code.  In the final section we will describe how to test that any changes that are made to the grid or to validation have not caused new bugs to appear.


== Bug Fixes ==
== Bug Fixes ==
''(This section updated November 2013)''


Unfortunately the grid contains two bugs that we have discovered and corrected.
Unfortunately the grid contains a few bugs that we have discovered and corrected.


The first relates to auto-sizing the grid columns and the second relates to the code that makes the horizontal and vertical scrollbars visible.
The first relates to auto-sizing the grid columns; the second relates to the code that makes the horizontal and vertical scrollbars visible; the third relates to SHIFT selection of multiple rows.


=== Auto-Sizing the Grid Columns ===
=== Auto-Sizing the Grid Columns ===
Line 94: Line 98:
</pre>
</pre>


== Handling Grid Events in OpenPetra Code ==
=== Using the SHIFT key to multi-select rows ===
Almost everywhere that we use a grid control we hook up two key events: FocusRowLeaving and FocusedRowChanged.  The FocusRowLeaving event has the possibility to programatically cancel the row changeOnce FocusedRowChanged happens the code has to respond to the fact that the grid selection has changed.
 
This bug, though very simple to fix, caused a number of unwanted effects in the original code.  First, the action of pressing the SHIFT key caused a cascade of Selection_Changed events to fire, which is a very unexpected behaviour.  The noticeable effect of this was that, if you had several rows highlighted and then pressed SHIFT, the selection returned to being just the one active row.  More importantly the same bug meant that if you SHIFT+Left_Mouse_Clicked you could see a range of cells being highlighted, but then they immediately disappeared again!
 
The required fix is in the file GridVirtual.cs at the very end of the 'ProcessSpecialGridKey' method.
 
This is the ORIGINAL code
            if (shiftPressed)
            {
                Selection.ResetSelection(true);
                Selection.SelectRange(new Range(m_firstCellShiftSelected, Selection.ActivePosition), true);
            }
 
This is the REVISED code
            if (shiftPressed && e.Handled)
            {
                Selection.ResetSelection(true);
                Selection.SelectRange(new Range(m_firstCellShiftSelected, Selection.ActivePosition), true);
            }
 
It is only required to add the '&& e.Handled' to the if clause.
 
The behaviour becomes what you would expect.  There is now no effect of pressing the SHIFT key on its own.  When using the mouse you highlight a single row, then press SHIFT and click with the mouse.  A range is selected.  You can release the SHIFT key.  If you now press the SHIFT key again and left click in a different row you will highlight the range from the original row to the new clicked row - it doesn't matter if the click was below or above the previous selection, or inside it or outside it.


The comments that follow describe the code that gets auto-generated, so normally you will not have to worry about coding anything in the manual codeHowever, in a few places it is possible that you cannot use auto-generated code and in that case you should manually code your screen according to these guidelinesIn fact the best course of action would be to copy an example of the auto-generated code into your own manual code.
=== Using the Keyboard to Edit-In-Place ===
There are a few places in OpenPetra where we edit the content of a cell directlyAn example is on the ''Transactions'' tab of the GL Batch screen, which contains a small grid that displays ''Analysis Types'' and ''Analysis Values''.  The first column displays the analysis type name and the second column displays the selected value for that typeThe value is selected from a list of choices in a drop-down ComboBox that 'appears' when you wish to edit the value.


=== Inside the FocusRowLeaving Event ===
The grid has extensive edit-in-place capabilities but the TAB, ENTER and ESC key handler code had a bug that prevented this from working at allENTER should have completed the edit, while ESC should have cancelled the edit (returning the value to what it was before the edit).
The only action that we take inside this event handler is to cancel the RowLeaving because the current data entered fails validationSo this event handler contains the line:
<pre>
    if (!ValidateAllData(true, true))
    {
        e.Cancel = true;
    }
</pre>


This call to ValidateAllData is the one that will show a message box to the user that there are uncorrected errors in the data entry and that he cannot leave the row until these have been fixed.
The original code had the lines
  CellContext focusCellContext = new CellContext(this, Selection.ActivePosition);
  if (focusCellContext != null && focusCellContext.IsEditing())
  {
However, unfortunately the IsEditing() method always returned false, even when it should have returned true.  So we have changed this (in multiple places) to
  CellContext focusCellContext = new CellContext(this, Selection.ActivePosition);
  ICellVirtual contextCell = focusCellContext.Cell;
  if (contextCell != null && contextCell.Editor != null && contextCell.Editor.IsEditing)
  {


It is worth stating here that it is important to be very aware of all the code that runs during the FocusRowLeaving event.  Although the method itself contains very few lines of code, the call to ValidateAllData executes code that is very complex and potentially re-entrant, so if not handled correctly will lead to a stack overflow.
==== New Keyboard Functionality ====
Having fixed the bug we then went on to add new functionality by modifying the keyboard actions as follows.


=== Inside the FocusedRowChanged Event ===
It is an OpenPetra 'rule' that the first column of a grid '''must not contain an editable cell'''.  When we select a row in OpenPetra we implicitly select the first cell of the row.  In this position the arrow keys (up/down only) and PgUp/PgDn can be used to select a different row and the TAB key moves you to the next control on the form.  If you click ENTER on the first column, the focus will move to the first column that contains an editable cell and the editor will open.  Now the up/done/left/right keys move you around the edit area or the drop-down list.  ENTER again finishes the edit and moves the selection to the next editable column, if it exists.  When there are no more editable columns the focussed cell goes back to the first column and no columns are 'selected'.
Once the row has changed, the only thing that we normally do is to be sure that we show the details of the current row in the matching data entry controlsSo this event handler simply consists of:
<pre>
    ShowDetails();
</pre>
although, as we shall see in a moment, this is contained inside a simple IF statement.


=== Handling Multiple Events ===
If you use SHIFT+ENTER to accept the edit '''the focus moves to the next row down and the same column'''.
This is the most difficult issue for OpenPetra programmers in respect of the grid.  Although these simple lines of code are all that we need inside each of these two methods, the complexity arises because both of these events can be fired multiple times for what would seem to be a single row change.  In particular the RowLeaving event can be fired 3 or even 4 times.  This is down to the specific implementation of code within the grid DLL and is partly (or mainly) explained by the fact that it gets fired both by a cell leaving and row leaving event as each cell is validated. So the big question becomes: how to deal with multiple event calls for what is in essence the same user action?


The first thing to say, perhaps counter-intuitively, is that it is wrong to cancel a duplicate event, just because it is a duplicate. If you get 3 events for a particular row that contains good data, you must just throw away the two duplicates and not cancel them.  Please take my word for it that if you do cancel them the code will appear to work but those cancelled events will come back to bite you later!
If you use CTRL+ENTER to accept the edit '''the focus moves to the next row down and the first column''', so no cell is being edited.


Now the important point to understand is that if you get multiple leaving events, you must only make the call to validate data once. If you get this wrong, you will soon realise it because on a row with errors you will get multiple identical Invalid Data message boxes popping up one after the other!
If you use ESC to cancel the edit the content reverts to its previous value and the focus moves to the next column that is editable (i.e. the same as ENTER would do).


We have two strategies for handling multiple RowLeaving events.  In the first case we never respond to an event in which the new, proposed row is -1This is really just a cell leaving event and not a row leaving one.  The second case, where the proposed row seems valid is literally to keep track of the time delay between consecutive eventsIf the current and proposed rows are the ones we had last time and the event is with 2 milliseconds of the previous event, we regard it as a duplicate and throw the event away.  Two milliseconds for a computer is a very long time but still much shorter than, say, any keyboard repeat time.
Note that this ENTER key behaviour overrides any ENTER functionality used by the screen - ''but only if the grid in question has editable cells''IF the grid has no editable cells, the screen is able to capture the ENTER key and take some actionThis happens on the GL Batch Journals tab, where pressing ENTER switches to the Transactions tab.


== Focus, Selection and Highlighting ==
It is important to understand the interplay between data, focus, selection and highlighting in the grid.  Actually the grid works the same way as most other Windows controls in that a whole row can be highlighted but there can only ever be one cell that is the cell with the focus.  Typically Windows displays the focused cell with a small dotted line border, or on more modern operating systems with a specific colour (often grey or light blue).  A highlighted row is typically shown in a dark blue colour.  Multiple rows can be highlighted at the same time.  There is no requirement for the focused cell to be in the same row as a highlighted (selected) row.  This has resulted in the past (when there were mistakes in our code) in having a 'stray blue cell' separate from the selected, highlighted, row.


Although the FocusedRowChanged event does not cascade like the leaving event, it is still possible to get duplicates as the user tabs around the screen and it may not be a good idea to copy the details back from the grid to the data entry controls (because while the user is tabbing around the controls should update the grid and not the other way around).  So once again we need protection to only go grid->controls once on a particular row.
Furthermore, the data that the grid displays is in no sense bound to the highlighting.  If the row filter applied to the grid is changed so that the information displayed in the grid is altered, the highlights remain the same.


In OpenPetra it is obviously important that there is a match between a particular single highlighted row and the row details shown in the details panel beneath the grid.  This has to be achieved by careful use of the grid events.


=== Handling a Sorted Grid ===
Finally it must be understood that the main grid method for focussing a cell actually places the screen focus on the grid - something that you need to bear in mind may affect keyboard users, who are expecting a particular tab order when entering data(But see below for ways to deal with this).
When a grid has sort headers, the highlighted row needs to change when the sort direction is changed.  The grid is set up with a property that means that it handles the row highlighting automatically in this case; we simply get notified that the FocusedRow has changedSince this is not a row data change there is nothing for us to do, and we ignore the event.


However, when a new row is added into a sorted grid, its position is unlikely to be the last row, which it always is on an unsorted grid.  So we have to find the position of the new row and highlight that one.  This normally gives rise to further FocusRowLeaving and FocusedRowChanged events (with different FromRow/ToRow) and everything works as you would expect - the details get shown for the new row.  However there is a special case where the selected row prior to the New button being pressed is the same row as where the new row ends upIn that case we do NOT get any RowLeaving or RowChanged events (because the row number is unchanged) - but we have to make sure that the details in the data entry controls are updated for the new row's data.
=== Grid Events ===
There are two fundamental events that we need to make use of
* an event that allows us to do row validation and, if necessary, cancel the row changeThe best event for this is ''FocusRowLeaving''.
* an event that notifies us that the row has changed so that we can update the details panel beneath the gridPrior to November 2013 we used FocusedRowChanged but now we use SelectionChanged because this comes much later in the event sequence which makes it more useful to us.  However, even to make use of this satisfactorily required some changes to the grid code so that it got fired only when all the highlighted grid rows were fully known.


=== Handling Validation ===
Please note that you can specify an '''ActionFocusRow''' in YAML and traditionally that has been set to ''FocusedRowChanged''This setting is now ignored by the YAML parser if the grid name is grdDetails.  If the grid is grdDetails the ActionFocusRow is '''always''' based on the SelectionChanged eventIf you use a grid that is not named grdDetails, you should consider using SelectionChanged in place of FocusedRowChanged, although for simple secondary grids on a form it may be that either is satisfactory.
Row data validation has introduced new complexities into the event handling mixThe way that validation works is that most data entry controls now have a validated event handler which has the effect of updating the grid column for that row as the user tabs away from the control - typically a text box or combo box.  If the column for that data entry control is sorted, the position of the row will change (but not the highlight unless we do that programatically).  '''The validation routine needs to be the only place where we transfer data entry values into the grid'''.  This is typically with the method GetDetailsFromControls(ARow)You should never call this method outside of a validation routine because, if you do, you will by-pass the possibility of handling erroneous data.  So the validation routine becomes the single place where a data row might move to a different location in the grid, and inside the routine we need to 'find' the row after every call to GetDetailsFromControls().  If it has moved we need to highlight the new position, which itself will cause further RowLeaving and RowChanged events.  The RowLeaving event will trigger another call to the validation routine, which needs to respond to that event as well.


So you can see that when we use a sorted grid and we change the data in the sorted column we will get multiple events '''that we need to respond to''', because when you glue them together you see the history of where the row started and where it ended upWith careful programming we can capture all the correct events and respond in a completely standard wayPlease understand that in the generated code we '''never''' 'turn off' any events, nor do we ever 'cancel' any (see above), but we do 'ignore' genuine duplicates, as explained above.
One of the main difficulties with writing code prior to November 2013 related to the '''FocusStyle''' property of the gridIt used to be set so that when the grid lost the screen focus the cell lost the focus tooThis had the effect of raising a ''FocusRowLeaving'' event and setting the ''ActivePosition'' to 'undefined'.  When the grid became the focussed control again we likewise had to take steps to re-focus the original cell.  This was a source of 'unwanted' events.  In fact one of the big drivers for changing the grid code was to simplify the grid code and reduce the number of duplicate or unwanted events to zero.  As a result the auto-generated code for the main screens became much simpler.


Here is the complete code for FocusRowLeaving:
Here is the code that is called when the focus row is about to change
<pre>
     /// <summary>
    /// FocusedRowLeaving can be called multiple times (e.g. 3 or 4) for just one FocusedRowChanged event.
     /// FocusedRowLeaving is called when the user (or code) requests a change to the selected row.
    /// The key is not to cancel the extra events, but to ensure that we only ValidateAllData once.
     /// </summary>
     /// We ignore any event that is leaving to go to row # -1
     private void grdDetails_FocusRowLeaving(object sender, SourceGrid.RowCancelEventArgs e)
     /// We validate on the first of a cascade of events that leave to a real row.
     /// We detect a duplicate event by testing for the elapsed time since the event we validated on...
    /// If the elapsed time is &lt; 2 ms it is a duplicate, because repeat keypresses are separated by 30 ms
    /// and these duplicates come with a gap of fractions of a microsecond, so 2 ms is a very long time!
    /// All we do is store the previous row from/to and the previous UTC time
    /// These three form level variables are totally private to this event call.
     private void FocusRowLeaving(object sender, SourceGrid.RowCancelEventArgs e)
     {
     {
         if (!grdDetails.Sorting && e.ProposedRow >= 0)
         if (!ValidateAllData(true, true))
         {
         {
             double elapsed = (DateTime.UtcNow - FDtPrevLeaving).TotalMilliseconds;
             e.Cancel = true;
            bool bIsDuplicate = (e.Row == FPrevLeavingFrom && e.ProposedRow == FPrevLeavingTo && elapsed < 2.0);
            if (!bIsDuplicate)
            {
                if (!ValidateAllData(true, true))
                {
                    e.Cancel = true;
                }
            }
            FPrevLeavingFrom = e.Row;
            FPrevLeavingTo = e.ProposedRow;
            FDtPrevLeaving = DateTime.UtcNow;
         }
         }
     }
     }
</pre>


Here is the complete code for FocusedRowChanged:
Here is the code that notifies us that a row change has occurred.
<pre>
     /// <summary>
     /// <summary>
     /// This variable is managed by the generated code.  It is used to manage row changed events, including changes that occur in data validation on sorted grids.
     /// This is the main event handler for changes in the grid selection
    /// Do not set this variable in manual code.
    /// You may read the variable.  Its value always tracks the index of the highlighted grid row.
     /// </summary>
     /// </summary>
    private int FPrevRowChangedRow = -1;
     private void grdDetails_RowSelected(object sender, RangeRegionChangedEventArgs e)
     private void FocusedRowChanged(System.Object sender, SourceGrid.RowEventArgs e)
     {
     {
         // The FocusedRowChanged event simply calls ShowDetails for the new 'current' row implied by e.Row
         int gridRow = grdDetails.Selection.ActivePosition.Row;
         // We do get a duplicate event if the user tabs round all the controls multiple times
         if (grdDetails.Sorting)
         // It is not advisable to call it on duplicate events because that would re-populate the controls from the table,
         {
         //  which may not now be up to date, so we compare e.Row and FPrevRowChangedRow first.
            // No need to ShowDetails - just update our (obsolete) variable
         if (!grdDetails.Sorting && e.Row != FPrevRowChangedRow)
            FPrevRowChangedRow = gridRow;
         }
         else
         {
         {
             ShowDetails();
             ShowDetails(gridRow);
         }
         }
        FPrevRowChangedRow = e.Row;
     }
     }
</pre>


=== Adding and Deleting Rows Using the Keyboard ===
Notice that, as a result of the change to the FocusStyle, we can now use the ''ActivePosition'' property of the grid selection to know the current row.
The grid now responds to the INS and DEL key presses on the keyboard and attempts a row insert and delete respectively. To do this, it looks for the existence on the form of buttons called btnNew and btnDelete and executes their click event accordingly. To access this functionality, you obviously need the buttons named correctly, but this doesn't stop you changing the label of the button if New or Delete do not best fit the form's context, e.g. the GL Batch form uses buttons with labels Add and Cancel.
 
==== Selecting a Row in Code ====
There are now three methods that select a row in the grid, but only two of them are likely to be used in manual code.
* '''SelectRowInGrid(ARowNumber)'''.  This is still the commonest method.  It selects the nearest row that it can to the requested row and places the focus on the grid control.  Because it uses a 'focus' method it does fire the ''FocusRowLeaving'' event which causes the current details to be updated into the data set and a validation check before the row moves. If the row move is successful, a SelectionChanged event is fired, which the auto-generated code will automatically use to update the details panel with matching details to the selected row.
* '''SelectRowWithoutFocus(ARowNumber)'''.  This also selects the nearest row it can.  When you use this method the focussed control does not become the grid.    There is no ''FocusRowLeaving'' event, so there is no validation and so no call to ''GetDetailsFromControls''.  However, there is a SelectionChanged event, so the details panel will get updated to match the newly selected row. Please be clear that it is only safe to use this method in a situation where the row you are moving '''from''' is known to contain valid data.  This method is useful
** either when you are already inside a call stack that was initiated from a row focus call, thereby preventing a runaway stack overflow
** or when you need to select a row in a grid whose data has just been updated due to another control change event.  For example, on a screen that has two grids (e.g. Partner | Contact Attributes or Finance | Analysis Types) a click on the upper grid needs to display different data in the lower grid and then select a relevant row.  By using this call the focus remains in the upper grid and the user is free to use the up/down arrow keys to move through the rows.  (The screen data has already been validated by the code associated with the upper grid).
* '''SelectRowAfterSort(ARowNumber)'''.  This is used inside Validation to handle the case where a sorted grid has moved the data row because the user has changed the value of the field that is sorted.  It selects the specific row requested, which has already been discovered  In this case there is no validation required and there is no need to change the details in the panel.  It is unlikely that you will use this in your code.
 
=== Multi-Row Selection ===
Many of our screens now support multi-row selection, particularly in the context of deletion.  The grid control supports the following ways of highlighting multiple rows:
* Mouse clicking on a row (so it becomes the only highlighted row) and then CTRL+Left clicking on one or more contiguous or non-contiguous rows.
* Mouse clicking on a row and SHIFT dragging up or down through contiguous rows
* Using the keyboard to select a row and then SHIFT+Up arrow or SHIFT+Down arrow to select a contiguous range of rows.
 
The grid control does not support mouse clicking on a row and then SHIFT+Left mouse click on another row. The reason for this is in the grid control and not in OpenPetra code.


Here is a typical yaml file section for the two buttons:
The simplest thing is to try these different options yourself.  What you will observe is that every click (or Up/Down) '''changes the selection''' to that row - and hence shows the details for that row.  To put it another way, when you multi-select using CTRL+mouse click each click changes the details in the details panel.  But if you multi-select using click and SHIFT+drag, the selected row is the first row you clicked on.  This is very standard Windows behaviour and feels entirely intuitive.
<pre>
Actions:
actNew: {Label=&New, ActionClick=NewRecord}
actDelete: {Label=&Delete, ActionClick=DeleteRecord}
Controls:
pnlContent:
Controls: [pnlGrid, pnlDetails]
Dock: Fill
pnlGrid:
Dock: Fill
Controls: [grdDetails, pnlButtons]
pnlButtons:
Dock: Right
Controls: [btnNew, btnDelete]
btnNew:
Action: actNew
btnDelete:
Action: actDelete


</pre>
=== Getting the Selected Row and Row Index ===
And then in the manual code, you call the generated code to create/delete a record:
This just got easier too in the new grid code.  Internally the auto-generated code maintains a variable FPreviouslySelectedDetailRow.  In the earlier grid (prior to Nov 2013) it also kept a variable FPrevRowChangedRow that was needed in order to cope with multiple events.  This latter variable is no longer needed but has been kept for backwards compatibility but you should consider that one day we may remove it altogether.
<pre>
private void NewRecord(Object sender, EventArgs e)
{
CreateNewPInternationalPostalType();
}


private void DeleteRecord(Object sender, EventArgs e)
In the new grid code the grid variable ''Selection.ActivePosition'' is now valid at all times and we make use of this in the auto-generated code.  But in your manual code you should always use the methods described in the next section.  This will allow us to develop the internal code in the future without having an impact on existing manual code.
{
DeletePInternationalPostalType();
}
</pre>


== Common Code Tasks ==
== Common Code Tasks ==
Line 249: Line 240:
The second returns the index of the row in the grid, where 0 corresponds to the header row and 1 is the first row of data.
The second returns the index of the row in the grid, where 0 corresponds to the header row and 1 is the first row of data.
<pre>
<pre>
  int nSelectedRow = grdDetails.SelectedRowIndex();
    /// <summary>
    /// Gets the selected Data Row index in the grid. The first data row is 1.
    /// </summary>
    /// <returns>The selected row - or -1 if no row is selected</returns>
    public Int32 GetSelectedRowIndex()
</pre>
</pre>


=== Selecting a Row ===
=== Selecting a Row ===
The basic method for selecting an grid row is to specify the row number.  The underlying source grid code always uses the row index when manipulating the grid.  But you can also select a grid row by specifying the data row that you want to select.  (Only the primary key values are used to find the row.)
In your manual code you will normally select a grid row by its index.
==== Selecting the row by its row index and setting the focus to the grid ====
There is a simple method to select a specific row in the grid.  This one call does many things.
There is a simple method to select a specific row in the grid.  This one call does many things.
<pre>
<pre>
Line 269: Line 269:
As the comment indicates, the single parameter is the row that you want to select, where the first data row is number 1.
As the comment indicates, the single parameter is the row that you want to select, where the first data row is number 1.


This method call does a number of things
This method call, which is part of the screen class, does all of the following
* Gets the current details from the controls in the details panel into the data set.
* Validates the current row and if necessary displays a message box and cancels the row change to your desired row
* Forces the specified row to be within the range 1..RowCount
* Forces the specified row to be within the range 1..RowCount
* Clears the existing selection
* Highlights the row
* Highlights the row
* Ensures that the specified row is visible in the view-port, scrolling the grid if necessary.  If possible there will be at least one other visible row above and below the selected row.
* Ensures that the specified row is visible in the view-port, scrolling the grid if necessary.  If possible there will be at least one other visible row above and below the selected row.
* Shows the details for the row
* Shows the details for the row
* Enables the grid panel unless the grid is empty or in detail protect mode
* Enables the grid panel unless the grid is empty or in detail protect mode
* Clears the controls in the details panel if the grid is empty


All these actions happen automatically and are driven by single RowLeaving and RowChanged events as described above.
All these actions happen automatically.  Finally, the focus is placed on to the grid so the user is ready to use the up/down arrow keys to select a different row.  If you want the focus on a different control, you need to focus that control after making this call. Also please note that Windows cannot focus any control until the screen has been activated.  Calling SelectRowInGrid() in the constructor of the screen class (which we do) will perform all the actions described above and all the events will fire as normal, but there will be no focus effect.


If you have manual row addition or deletion code, you can simply call this one method and the nearest available row will be selected and its details displayed.  SelectRowInGrid is a powerful method that does everything you need to highlight a row and show all its details.
If you have manual row addition or deletion code, you can simply call this one method and the nearest available row will be selected and its details displayed.  SelectRowInGrid is a powerful method that does everything you need to highlight a row and show all its details.


=== Showing the Details for the Current Row ===
You can call SelectRowInGrid(9999) or SelectRowInGrid(-99) on a grid with only 5 rows, or even no rows at all and the correct actions will be taken; there should be no need in your manual code for tests to check whether your preferred new row index is within the limits of the row count.
A simple call to ShowDetails() shows the details for the currently selected row.  There is no need to call this if you have already called SelectRowInGrid(ARowIndex), because that method already calls it.
 
It is worth pointing out that the new grid code has been written so that even if you call SelectRowInGrid with a row index that is the same as the current row index, the grid will still fire a SelectionChanged event.  This is valuable to us because there can be situations when the row index might be unchanged but the data in the row is different.  This ensures that we can never get a mismatch between the grid row and the details data.  It is for this reason that the auto-generated event handler name for the SelectionChanged event is titled RowChanged because that better describes the occasion when the SelectionChanged event is fired.
 
==== Selecting the row by its row index without setting the focus to the grid ====
There is a new method that is useful in some critical circumstances.  This method is a member of the OpenPetra TSgrdDataGrid class
  grdDetails.SelectRowWithoutFocus(ARowNumberInGrid)
When you call this method you get a sub-set of the actions taken by SelectRowInGrid described above.
* Forces the specified row to be within the range 1..RowCount
* Clears the existing selection
* Highlights the row
* Ensures that the specified row is visible in the view-port, scrolling the grid if necessary.  If possible there will be at least one other visible row above and below the selected row.
* Shows the details for the row
* Enables the grid panel unless the grid is empty or in detail protect mode
* Clears the controls in the details panel if the grid is empty
 
This method does all of the above '''without putting the screen focus onto the grid'''.  There are circumstances where this is important, but please understand that '''you should only use this method when the current data in the grid is known to be valid''' because no ''FocusRowLeaving'' event will be called - nor will a number of other grid events that you might be relying on.  The only event that you will definitely get is the SelectionChanged event that the auto-generated code will use to ensure that the details panel information matches the selected row.
 
==== Selecting a row by its row index after existing rows have been shuffled by sorting ====
There is a third row selection method that you are unlikely to use in manual code.
  grdDetails.SelectRowAfterSort(ARowNumberInGrid)
When you call this method you get an even shorted sub-set of the actions taken by SelectRowInGrid described above.
* Forces the specified row to be within the range 1..RowCount
* Clears the existing selection
* Highlights the row
* Ensures that the specified row is visible in the view-port, scrolling the grid if necessary.  If possible there will be at least one other visible row above and below the selected row.
This method assumes that the row being selected is the same as the row being moved from.  The data may or may not be valid.  This method is used simply to ensure that the highlighted row in the grid is the row that belongs with the current details.
 
==== Selecting a row by specifying the DataRow ====
Much less commonly you may want to select a row for which you know the DataRow object but do not know where it is. In this case you first find the row index and then make the standard call.
<pre>
int rowToSelect = grdDetails.DataSourceRowToIndex2(TheDataRowToSelect);
SelectRowInGrid(rowToSelect);
</pre>
As you will see, this still uses SelectRowInGrid so has all the fetaures described above.
 
=== Scrolling a Row Into View ===
This task is normally taken care of simply by selecting a row in the grid (see above), but if you need to do this explicitly there is a method to do it, which will ensure that, if possible, the highlighted row has at least one row above and below it.
<pre>
<pre>
    /// <summary>
        /// <summary>
    /// This overload shows the details for the currently highlighted row.
        /// This is the OpenPetra override.  It scrolls the window so that the specified row is shown.
    /// The method still works when the grid is empty and no row can be selected.
        /// The standard grid behaviour would be simply to ensure the selected row is within the grid.
    /// The Details panel is disabled when the grid is empty, or when in Detail Protected Mode
        /// With this method, where possible there is always one unselected row above or one row below.
    /// The variable FPreviouslySelectedDetailRow is set by this call.
        /// </summary>
    /// </summary>
        /// <param name="ARowNumberInGrid">The grid row number that needs to be inside the viewport</param>
    private void ShowDetails()
        /// <returns>False if the grid scrolls to a new position.  True if the specified row is already in the view port</returns>
        public bool ShowCell(int ARowNumberInGrid)
</pre>
</pre>


Line 299: Line 340:


The auto-generated Delete{#DETAILTABLE} procedure can call up to 3 optional manual code methods that control the deletion process.  If you want more control over the deletion process than is provided by the generic code, then you can include your own variant of any or all of the following in your manual code file.
The auto-generated Delete{#DETAILTABLE} procedure can call up to 3 optional manual code methods that control the deletion process.  If you want more control over the deletion process than is provided by the generic code, then you can include your own variant of any or all of the following in your manual code file.
'''Please note that the following code is for illustration only.  The InternationalPostalType screen does not in fact use any of these manual methods because the default behaviour is all that is necessary.'''
* We should use the standard deletion question wherever possible in order to maintain consistency between screens
* We should not normally have a completion message like 'The row was deleted successfully'.  That annoys the user and it is obvious because the row is no longer in the grid! It is good to have a completion message in cases where the behaviour is somewhat non-standard, such as where the code has already saved the changes, which happens on a few finance screens.
* We should not wrap our deletion code in a try/catch block.  There is already a try/catch block in the calling code.
* We should not display message boxes inside these methods because on multi-row delete they would get shown every time.
<pre>
<pre>
/// <summary>
/// <summary>
Line 309: Line 356:
private bool PreDeleteManual(PInternationalPostalTypeRow ARowToDelete, ref string ADeletionQuestion)
private bool PreDeleteManual(PInternationalPostalTypeRow ARowToDelete, ref string ADeletionQuestion)
{
{
/*Code to execute before the delete can take place*/
/*Code to execute before the delete can take place.  Validate that the specified row can be deleted?*/
ADeletionQuestion = String.Format(Catalog.GetString("Are you sure you want to delete Postal Type Code: '{0}'?"),
 
  ARowToDelete.InternatPostalTypeCode);
/*Code to modify the deletion question*/
 
/*return true if deleting this row is allowed*/
return true;
return true;
}
}
Line 321: Line 370:
/// <param name="ACompletionMessage">if specified, is the deletion completion message</param>
/// <param name="ACompletionMessage">if specified, is the deletion completion message</param>
/// <returns>true if row deletion is successful</returns>
/// <returns>true if row deletion is successful</returns>
private bool DeleteRowManual(PInternationalPostalTypeRow ARowToDelete, out string ACompletionMessage)
private bool DeleteRowManual(PInternationalPostalTypeRow ARowToDelete, ref string ACompletionMessage)
{
{
bool deletionSuccessful = false;
bool deletionSuccessful = false;
try
/*Code to prepare for deletion*/
{
 
//Must set the message parameters before the delete is performed if requiring any of the row values
/*Code to delete rows from dependent tables first*/
ACompletionMessage = String.Format(Catalog.GetString("Postal Type Code: '{0}' deleted successfully."),
 
  ARowToDelete.InternatPostalTypeCode);
/*Code to perform the row deletion on this table*/
ARowToDelete.Delete();
deletionSuccessful = true;
}
catch (Exception ex)
{
ACompletionMessage = ex.Message;
MessageBox.Show(ex.Message,
"Deletion Error",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
return deletionSuccessful;
return deletionSuccessful;
Line 354: Line 392:
private void PostDeleteManual(PInternationalPostalTypeRow ARowToDelete, bool AAllowDeletion, bool ADeletionPerformed, string ACompletionMessage)
private void PostDeleteManual(PInternationalPostalTypeRow ARowToDelete, bool AAllowDeletion, bool ADeletionPerformed, string ACompletionMessage)
{
{
/*Code to execute after the delete has occurred*/
/*Code to execute after the delete has occurred - for example updating the main screen*/
      if (ADeletionPerformed && ACompletionMessage.Length > 0)
 
      {
      MessageBox.Show(ACompletionMessage,
                      "Deletion Completed",
                      MessageBoxButtons.OK,
                      MessageBoxIcon.Information);
     
      if (!pnlDetails.Enabled) //set by FocusedRowChanged if grdDetails.Rows.Count < 2
      {
      ClearControls();
      }
      }
else if (!AAllowDeletion)
{
//message to user
}
else if (!ADeletionPerformed)
{
//message to user
}
}
}
</pre>
</pre>


The DeleteRowManual()'s second argument, out string ACompletionMessage (which is passed local variable completionMessage), is always sent in empty. If it is populated in the method after a successful delete and there is no PostDeleteManual() method present, then a MessageBox displaying completionMessage string will appear. If you create a PostDeleteManual() method, completionMessage is passed in as an argument.
The DeleteRowManual()'s second argument, ref string ACompletionMessage (which is passed local variable completionMessage), is always sent in empty. If it is populated in the method after a successful delete and there is no PostDeleteManual() method present, then a MessageBox displaying completionMessage string will appear. If you create a PostDeleteManual() method, completionMessage is passed in as an argument.
 
Please note that when multiple rows are deleted together the deletion question is ignored.  The completion question is displayed after all the rows have been deleted.  This means that if you check its content at each call to DeleteRowManual it will be empty the first time so you should set it to a suitable string for a single row delete.  If the string is not empty this must be a multi-row delete and you may wish to change the text.
 
You can read more about [[Adding Delete Functionality to a Screen or Control]] here.
 
=== Coding For Using the Keyboard ===
The grid control contains handlers as standard for the Home/End, Up/Down and PgUp/PgDn keys.  The Insert and Delete keys also do what their name suggests as an alternative to clicking the ''New'' or ''Delete'' buttons on a form, provided that the YAML file specification has '''btnNew''' and/or '''btnDelete'''.
 
Coding for the ''ENTER'' key only requires slightly more work as you can see, for example, in Ict.Petra.Client.MFinance.Gui.Gift.UC_GiftBatches.yaml
 
Under the grdDetails section we have:
<pre>
ActionDoubleClick: ShowTransactionTab
ActionEnterKeyPressed: ShowTransactionTab
</pre>
The code then generates:
<pre>
this.grdDetails.EnterKeyPressed += new TKeyPressedEventHandler(this.ShowTransactionTab);
</pre>
This allows the programmer to capture the ''ENTER'' key for any grid and call any code. In this example it is the same code as the double click, but it doesn't have to be, of course.
 
== Code Snippets That Should NOT Appear in Your Manual Code ==
The manual code class contains several variables and methods that are used to maintain state.  A number of them seem at first glance to be useful in manual code, but you must resist the temptation!  There are certain variables that you should not modify and the use of some of the methods may mean that important variables do not get set.
 
Here is a list of coding statements that should not appear in your code.
{| border="1" cellpadding="5" cellspacing="0"
!Code!!Comment
|-
|grdDetails.Selection.ResetSelection(false)
|This call clears the selection in the grid and is often followed by the grdDetails.SelectRowInGrid(rowToSelect) call.  You should use the standard SelectRowInGrid method instead.
|-
|grdDetails.SelectRowInGrid(rowToSelect)
|This also may appear to give the desired result, but you are using methods that are at too low a level.  You should leave this call to the low level code and use the higher level method SelectRowInGrid instead.
|-
|grdDetails.Selection.ActivePosition.Row
|This property used to be unreliable.  Since the recent grid changes it has become much more useful, but again it is a low-level method.  Your manual code will be future-proof if you use GetSelectedRowIndex() instead.
|-
|FPreviouslySelectedDetailRow = null
|You should not modify this Form variable directly, or, if you do you must be sure to follow it with a call to SelectRowInGrid(N) so that the variable can be reset to a valid object that reflects the content of the details panel.
|-
|FPrevRowChangedRow = 1
|Same comments applies as the row above
|-
|ShowDetails(ADataRow)
|This would almost certainly give rise to a mismatch between the selected row and the content of the details panel
|-
|ShowDetails()
|This line in your code would not be 'bad' but I cannot think of a reason that you would need it!  It simply shows the details for the 'current' row, which should be the details already shown.  So, usually this line would be superfluous.
|}
Unfortunately many of these snippets have appeared in our manual code over time as the auto-generated code has developed.  Now that most grid templates have more standardised methods we need to progress to using these and remove some of the older code.  Usually it will result in fewer lines of code - which will be better in itself!


== Grid Testing ==
== Grid Testing ==
Line 398: Line 467:
## ''Errors:''  It is an error if, for example, the initial highlight is on the Code in the details panel.
## ''Errors:''  It is an error if, for example, the initial highlight is on the Code in the details panel.
# '''Using the keyboard to move around the screen'''
# '''Using the keyboard to move around the screen'''
## Reload the screen.  Do not touch the mouse, but use the keyboard to TAB from control to control.  Confirm that each control is focussed in turn.  Initially the Delete button will always be disabled because you are not allowed to delete data that is already in the database.  When you have TABbed back to the grid use the up/down arrow keys to cursor through the grid rows.  Ensure that the details update correctly every time.  Resize the window so that you can experiment with the ''PgUp/PgDn'' keys.
## Reload the screen.  Do not touch the mouse, but use the keyboard to TAB from control to control.  Confirm that each control is focussed in turn.  When you have TABbed back to the grid use the up/down arrow keys to cursor through the grid rows.  Ensure that the details update correctly every time.  Resize the window so that you can experiment with the ''PgUp/PgDn'' keys.
## ''Errors:''  It is an error if, for example, TABbing from the postal code control in the details panel jumps you back to the grid.
## ''Errors:''  It is an error if, for example, TABbing from the postal code control in the details panel jumps you back to the grid.
# '''Using the mouse to move around the screen'''
# '''Using the mouse to move around the screen'''
## Reload the screen.  Repeat the previous keyboard tests using the mouse (do not click the new button yet and do not change any data).  Confirm that the details panel always shows the correct data for the row that you have highlighted.
## Reload the screen.  Repeat the previous keyboard tests using the mouse (do not click the ''New'' button yet and do not change any data).  Confirm that the details panel always shows the correct data for the row that you have highlighted.
# '''Using the keyboard to create new rows (1)'''
# '''Using the keyboard to create new rows (1)'''
## Reload the screen, shrink the height so that not all rows can be displayed, press the TAB key once, then press SPACE.  Confirm that
## Reload the screen, shrink the height so that not all rows can be displayed, press the TAB key once, then press SPACE.  Confirm that
Line 408: Line 477:
### the focus moves to the NEWCODE control
### the focus moves to the NEWCODE control
## Now edit the text for the code and press TAB.  Edit the description and press TAB.  Do not intentionally fail validation yet (that comes in a later test!).  Keep TABbing and editing until you reach the ''New'' button again.  Then press SPACE to add another row.  Repeat this until you have added three new rows.  Confirm that all your edits have been applied and that there was never a time where you TABbed to an unexpected control.
## Now edit the text for the code and press TAB.  Edit the description and press TAB.  Do not intentionally fail validation yet (that comes in a later test!).  Keep TABbing and editing until you reach the ''New'' button again.  Then press SPACE to add another row.  Repeat this until you have added three new rows.  Confirm that all your edits have been applied and that there was never a time where you TABbed to an unexpected control.
## Confirm that the ''Delete'' button is now enabled (because your new data has not yet been saved), and that you can TAB around every control, including the ''Delete'' button.
## Confirm that the ''Save'' button in the toolbar is enabled.
## Confirm that the ''Save'' button in the toolbar is enabled.
## Close the window without saving your changes.
## Close the window without saving your changes.
## ''Errors:''  it is an error if the row does not appear inside the view-port, or the focus is not on the code control.
## ''Errors:''  it is an error if the row does not appear inside the view-port, or the data in the grid does not match the data in the details panel when you have TABbed back to the grid, or the focus of the new row is not on the CODE control.
# '''Using the keyboard to create new rows (2)'''
# '''Using the keyboard to create new rows (2)'''
## Reload the screen, shrink the height so that not all rows can be displayed, then press the ''INS'' key twice.  Confirm that
## Reload the screen, shrink the height so that not all rows can be displayed, then press the ''Insert'' key.  Confirm that
### two new rows have been added at the bottom
### one new row has been added at the bottom
### the grid has scrolled to show them
### the grid has scrolled to show it
### the focus has remained on the grid
### the focus has moved to the text box for the CODE
## Edit the CODE and TAB back to the grid.  Press the ''Insert'' key again and repeat the test.
## Close the window without saving your changes.
## Close the window without saving your changes.
## ''Errors:''  In this case it is an error if the focus changes to the NEWCODE control.
## ''Errors:''  It is an error, for example, if the focus does not change to the NEWCODE control.
# '''Using the mouse to create new rows'''
# '''Using the mouse to create new rows'''
## Reload the screen, shrink the height so that not all rows can be displayed, then click the ''New'' button once.  Confirm that
## Reload the screen, shrink the height so that not all rows can be displayed, then click the ''New'' button once.  Confirm that
Line 425: Line 494:
### the focus moves to the NEWCODE control
### the focus moves to the NEWCODE control
## Close the window without saving your changes.
## Close the window without saving your changes.
## ''Errors:''  it is an error if the row does not appear inside the view-port, or the focus is not on the code control.
## ''Errors:''  it is an error if the row does not appear inside the view-port, or the data in the grid does not match the data in the details panel when you have TABbed back to the grid, or the focus of the new row is not on the CODE control.
# '''Creating new rows in a sorted grid (1)'''
# '''Creating new rows in a sorted grid (1)'''
## Reload the screen, click the Sort Header twice on the first column so that the items are sorted Z at the top and A at the bottom.  Click ''New'' twice to create 2 new rows.  Confirm that
## Reload the screen, click the Sort Header twice on the first column so that the items are sorted Z at the top and A at the bottom.  Shrink the height of the window so that not all rows can be displayed.  Click ''New'' twice to create 2 new rows.  Confirm that
### The two rows have been inserted into the grid correctly
### The two rows have been inserted into the grid correctly
### If necessary the grid has scrolled the rows into view
### the second inserted row is the upper of the two and that the details panel shows the details for this second row.
### the second inserted row is the upper of the two and that the details panel shows the details for this second row.
### the CODE in the details panel has the focus
### the CODE in the details panel has the focus
Line 435: Line 505:
## ''Errors:''  There are numerous possible errors that can occur if there are mistakes in the code.  The wrong row might be highlighted; the wrong details may be displayed; the focus may be incorrect.
## ''Errors:''  There are numerous possible errors that can occur if there are mistakes in the code.  The wrong row might be highlighted; the wrong details may be displayed; the focus may be incorrect.
# '''Creating new rows in a sorted grid (2)'''
# '''Creating new rows in a sorted grid (2)'''
## Reload the screen, click the Sort Header twice on the second column so that the Description items are sorted Z at the top and A at the bottom.  Click ''New'' twice to create 2 new rows.  Confirm that
## Reload the screen, click the Sort Header twice on the '''second''' column so that the Description items are sorted Z at the top and A at the bottom.  Shrink the height of the window so that not all rows can be displayed.  Click ''New'' twice to create 2 new rows.  Confirm that
### The two rows have been inserted into the grid correctly
### The two rows have been inserted into the grid correctly
### If necessary the grid has scrolled the rows into view
## Repeat, creating new rows but editing the description so that the rows are in different parts of the grid.
## Repeat, creating new rows but editing the description so that the rows are in different parts of the grid.
## Be sure to test the case where the highlighted row before you create a new row is the row where the new row will appear (the row where the description text is just '''before''' ''Please Enter Description'' in the alphabet).
## Close the window without saving your changes.
## Close the window without saving your changes.
## ''Errors:''  Look for the same class of error as when the first column was sorted.
## ''Errors:''  Look for the same class of error as when the first column was sorted.
Line 457: Line 529:
## ''Errors:''  There are numerous errors that can occur when the underlying code is wrong.  These errors include the wrong row being highlighted, the wrong control being focussed and wrong information being displayed in the details panel.
## ''Errors:''  There are numerous errors that can occur when the underlying code is wrong.  These errors include the wrong row being highlighted, the wrong control being focussed and wrong information being displayed in the details panel.
# '''Adding invalid rows to a sorted grid (2)'''
# '''Adding invalid rows to a sorted grid (2)'''
## You can repeat the previous test using a sorted second (Description) column, this time TABbing to the Description field and pressing ''DEL'' so that it becomes blank and the TABbing once more.  Look for all the same errors.  This time, instead of using CTRL+Z to restore the original text, simply edit it to a different value and confirm that it finds the correct location in the grid.
## You can repeat the previous test using a sorted second (Description) column, this time TABbing to the Description field and pressing ''DEL'' so that it becomes blank and the TABbing once more.  Look for all the same errors.  Instead of activating the message box by clicking ''New'', do so by clicking ''Save''.  Also, instead of using CTRL+Z to restore the original text, simply edit it to a different value and confirm that it finds the correct location in the grid.
## Close the window without saving your changes.
# '''Adding invalid rows to a sorted grid (3)'''
## Reload the screen, click the Sort Header twice on the first column so that the Code items are sorted Z at the top and A at the bottom.  Click ''New'' once to create a new row in the middle of the grid.  The CODE text box will have the focus, so without doing anything with the mouse or keyboard simply press the ''DEL'' key to make the CODE go blank.  Now click somewhere in the grid with the mouse.  Confirm that
### The highlighted row jumps to the correct position for a blank CODE
### The error dialog immediately pops up, preventing you from moving to the row you clicked on.
## Accept the dialog and confirm that
### The focus moves to the offending control
## Close the window without saving your changes.
## ''Errors:''  It is a serious error if the dialog does not appear or if the row highlight moves to the row you clicked on.
# '''Editing existing data'''
## Reload the screen and add 4 new rows (which will appear at the bottom)
## Now select each of the first two rows in turn using the mouse and edit their contents, without causing validation errors.  Confirm that the data appears correctly in the grid.
## Now using the keyboard only (arrow keys and TAB key), repeat the test on the second pair of added rows.
## Now repeat these two tests using mouse/keyboard, but make the CODE blank and make the Description blank.  Confirm that
### The tab sequence was correct and that all controls are in the TAB sequence
### The tool-tips appeared advising of Bad Data
### That you cannot leave a row that contains bad data using the cursor keys when the grid has focus
### That you cannot leave a row that contains bad data by clicking ''New'' or ''Save''
### That after the accepting the message box listing all the current errors, the first error control is focussed ready to be corrected.
## Close the window without saving your changes.
## ''Errors:''  There are numerous errors that can occur when the underlying code is wrong.  These errors include the wrong control being focussed and wrong information being displayed in the details panel.
# '''Editing existing data in a sorted grid (1)'''
## Reload the screen and click on the sort header of the first column twice, so it is sorted Z at the top and A at the bottom
## Add 1 new row (which will appear near the middle).  The CODE should be focussed.
## Edit the CODE, without causing validation errors.  TAB to the Description.  Confirm that
### the data appears correctly in the grid
### the 'current' row still has the highlight wherever it has moved to.
## Shift+TAB back to the CODE and try various other values, confirming that the highlight always tracks the row.
## Now repeat this test, but make the CODE blank and make the Description blank.  Confirm that
### The tab sequence was correct and that all controls are in the TAB sequence
### The tool-tips appeared advising of Bad Data
### That you cannot leave a row that contains bad data using the cursor keys when the grid has focus
### That you cannot leave a row that contains bad data by clicking ''New'' or ''Save''
### That after the accepting the message box listing all the current errors, the first error control is focussed ready to be corrected.
## Close the window without saving your changes.
## ''Errors:''  There are numerous errors that can occur when the underlying code is wrong.  These errors include the wrong row being highlighted, the wrong control being focussed and wrong information being displayed in the details panel.
# '''Editing existing data in a sorted grid (2)'''
## Reload the screen and click on the sort header of the '''second''' column twice, so it is sorted Z at the top and A at the bottom
## Add 1 new row (which will appear near the middle).  The CODE should be focussed.
## TAB to the Description and edit it, without causing validation errors.  TAB away from the Description.  Confirm that
### the data appears correctly in the grid
### the 'current' row still has the highlight wherever it has moved to.
## Shift+TAB back to the Description and try various other values, confirming that the highlight always tracks the row.
## Now repeat this test, but make the Description blank.  Confirm that
### The tab sequence was correct and that all controls are in the TAB sequence
### The tool-tip(s) appeared advising of Bad Data
### That you cannot leave a row that contains bad data using the cursor keys when the grid has focus
### That you cannot leave a row that contains bad data by clicking ''New'' or ''Save''
### That after the accepting the message box listing all the current errors, the first error control is focussed ready to be corrected.
## Close the window without saving your changes.
## Close the window without saving your changes.
## ''Errors:''  There are numerous errors that can occur when the underlying code is wrong.  These errors include the wrong row being highlighted, the wrong control being focussed and wrong information being displayed in the details panel.
# '''Checking the Horizontal Scrollbar'''
# '''Checking the Horizontal Scrollbar'''
## Reload the screen and resize the width so that a horizontal scrollbar appears at the bottom of the grid.  Now resize the height so that there is enough room for one more row but not for two, without the second being at least partially masked.
## Reload the screen and resize the width so that a horizontal scrollbar appears at the bottom of the grid.  Now resize the height so that there is enough room for one more row but not for two, without the second being at least partially masked.
Line 505: Line 627:
## ''Errors:''  It is an error if the focus is on the wrong control, or if you cannot TAB round all the controls in sequence.  It is an error if the highlighted grid information is not the same as its corresponding details panel.
## ''Errors:''  It is an error if the focus is on the wrong control, or if you cannot TAB round all the controls in sequence.  It is an error if the highlighted grid information is not the same as its corresponding details panel.
# '''Adding Rows Using the Keyboard (2)'''
# '''Adding Rows Using the Keyboard (2)'''
## Reload the screen, but this time add rows to the top grid by pressing INS three times.  Confirm that
## Reload the screen, but this time add a row to the top grid by pressing the ''INS'' key.  Confirm that
### three rows got added to the upper grid, each having one row in the lower grid
### one row got added to the upper grid, and one row in the lower grid
### the focus is still on the upper grid
### the focus is CODE for the upper grid
## Now TAB to the lower grid and press the INS key twice.  Confirm that  
## Now TAB to the lower grid and press the INS key.  Confirm that  
### the lower grid now has three rows for the selected row in the upper grid
### one row got added to the '''upper''' grid, and one row in the lower grid
### the lower grid still has the focus
### the focus is CODE for the '''upper''' grid
## SHIFT+TAB back to the upper grid again and use the Up/Down arrow keys to move through the rowsConfirm that
## This shows a limitation of the implementation of the keyboard shortcut handlerIt is associated with the main grid and not the child grid.
### two upper grid rows have just one lower grid row, while the third one has three rows.
## Close the window without saving your changes.
## Close the window without saving your changes.
## ''Errors:''  It is an error if the focus is on the wrong control, or if you cannot TAB round all the controls in sequence, or use the arrow keys to select different rows.  It is an error if the highlighted grid information is not the same as its corresponding details panel.
## ''Errors:''  It is an error if the focus is on the wrong control, or if you cannot TAB round all the controls in sequence, or use the arrow keys to select different rows.  It is an error if the highlighted grid information is not the same as its corresponding details panel.
Line 525: Line 646:
### the focus is on the CODE for the lower grid
### the focus is on the CODE for the lower grid
### the details are all correct
### the details are all correct
## Use the mouse or the keyboard arrow keys to confirm that there is one selection in the top grid that has four rows in the bottom grid, while the other top selections only have one bottom grid row
## Close the window without saving your changes.
## Close the window without saving your changes.
## ''Errors:''  It is an error if the focus is on the wrong control, or if you cannot TAB round all the controls in sequence, or use the arrow keys to select different rows.  It is an error if the highlighted grid information is not the same as its corresponding details panel.
## ''Errors:''  It is an error if the focus is on the wrong control, or if you cannot TAB round all the controls in sequence, or use the arrow keys to select different rows.  It is an error if the highlighted grid information is not the same as its corresponding details panel.

Latest revision as of 15:49, 26 November 2013

Important

This page has been extensively revised in November 2013. The content here describes the current implementation of the SourceGrid. You can still read the previous information that applied prior to this date by following this link

Introduction

OpenPetra makes extensive use of the SourceGrid control available from http://sourcegrid.codeplex.com/. This open source control has many desirable features and is a fairly complex piece of code written by David Icardi. We currently use version 4.40 published on 16 July 2012. The source code comes in a ZIP file titled SourceGrid_4_40_src.zip. A few people in the OpenPetra team have this file although it is not kept in our repository, due to its size.

Over the course of time we have discovered a few 'simple bugs' in the code, which were easy to fix. However, as more and more features have been introduced into OpenPetra, we have begun to find it harder to work with the way that events are fired from the grid control. So we made a decision in October 2013 to 'fork' the grid code so that we could make it more compatible with the way we work. Arguably the OpenPetra use-case is unusual in that we use the grid to display records which are edited and validated in separate controls. This adds complexity to the integration of the GUI. Over the previous twelve months we had incorporated the Delete functionality (including multi-row delete), Filter/Find functionality which changes the grid content and row selection and some limited edit-in-place cases. All these new capabilities placed more and more stress on our use of grid events.

On this page we will first explain the 'simple' bug fixes. In the second section we explain the changes to the events that have to do with Focus, Selection and Highlighting. This will lead on to consideration of how to code for the events that we use. In the fourth section we outline some of the commonest methods associated with the grid that you will want to use in your manual code. In the final section we will describe how to test that any changes that are made to the grid or to validation have not caused new bugs to appear.

Bug Fixes

(This section updated November 2013)

Unfortunately the grid contains a few bugs that we have discovered and corrected.

The first relates to auto-sizing the grid columns; the second relates to the code that makes the horizontal and vertical scrollbars visible; the third relates to SHIFT selection of multiple rows.

Auto-Sizing the Grid Columns

When the screen is resized, the grid also resizes and the column widths change to try and show as much useful information as possible. Earlier versions of the grid had a behaviour in this respect that we prefer to the later implementation, so we make a small change to preserve the previous behaviour. If a column has no data, we prefer that the column never shrinks to a size less than the text in the header for that column. So we make the following change to AutoSizeView() inside ColumnInfoCollection.cs.

   List<int> list = Grid.Rows.RowsInsideRegion(Grid.DisplayRectangle.Y, Grid.DisplayRectangle.Height, true, false);       

becomes

   List<int> list = Grid.Rows.RowsInsideRegion(Grid.DisplayRectangle.Y, Grid.DisplayRectangle.Height);       

This has the effect of including the header row in the list of visible rows, whose text widths need consideration.

Automatic positioning of the Two ScrollBars

Particularly when a horizontal scrollbar is already present on the grid, the standard code does not make a good job of displaying the vertical scrollbar correctly. As a consequence it is possible for a new row to be 'hidden' behind the horizontal scrollbar with no means of scrolling the view-port to make it visible. It is also possible for the horizontal scrollbar not to be displayed when it should be.

In order to correct this we have the following code for the RecalcCustomScrollBars() method in CustomScrollControl.cs.

	/// <summary>
	/// Recalculate the scrollbars position and size.
	/// Use this to refresh scroll bars
	/// </summary>
	public void RecalcCustomScrollBars()
	{
            SuspendLayout();

            /////////////////////////////////////////////////////////////////////
            //ALAN
            if (GetScrollColumns(base.DisplayRectangle.Width) > 0)
            {
                // we definitely need a HScroll based on the base display rectangle
                PrepareScrollBars(true, false);
                if (GetScrollRows(DisplayRectangle.Height) > 0)
                {
                    // we need a VScroll too
                    PrepareScrollBars(true, true);
                }
            }
            else
            {
                // we don't need an HScroll (yet)
                if (GetScrollRows(base.DisplayRectangle.Height) > 0)
                {
                    // we definitely need a VScroll based on the base display rectangle
                    PrepareScrollBars(false, true);
                    if (GetScrollColumns(DisplayRectangle.Width) > 0)
                    {
                        // actually now we need an HScroll after all, because the VScroll has taken up space
                        PrepareScrollBars(true, true);
                    }
                }
                else
                {
                    // No scrolls needed - everything fits in the base display rectangle
                    PrepareScrollBars(false, false);
                }
            }

            //Finally I read the actual values to use (that can be changed because I have called PrepareScrollBars)
            if (VScrollBarVisible)
            {
                int scrollRows = GetScrollRows(DisplayRectangle.Height);
                scrollRows = scrollRows - GetActualFixedRows();
                RecalcVScrollBar(scrollRows);
            }
            if (HScrollBarVisible)
            {
                int scrollCols = GetScrollColumns(DisplayRectangle.Width);
                RecalcHScrollBar(scrollCols);
            }

	    //forzo un ridisegno
	    InvalidateScrollableArea();

	    ResumeLayout(true);
	}

Using the SHIFT key to multi-select rows

This bug, though very simple to fix, caused a number of unwanted effects in the original code. First, the action of pressing the SHIFT key caused a cascade of Selection_Changed events to fire, which is a very unexpected behaviour. The noticeable effect of this was that, if you had several rows highlighted and then pressed SHIFT, the selection returned to being just the one active row. More importantly the same bug meant that if you SHIFT+Left_Mouse_Clicked you could see a range of cells being highlighted, but then they immediately disappeared again!

The required fix is in the file GridVirtual.cs at the very end of the 'ProcessSpecialGridKey' method.

This is the ORIGINAL code

           if (shiftPressed)
           {
               Selection.ResetSelection(true);
               Selection.SelectRange(new Range(m_firstCellShiftSelected, Selection.ActivePosition), true);
           }

This is the REVISED code

           if (shiftPressed && e.Handled)
           {
               Selection.ResetSelection(true);
               Selection.SelectRange(new Range(m_firstCellShiftSelected, Selection.ActivePosition), true);
           }

It is only required to add the '&& e.Handled' to the if clause.

The behaviour becomes what you would expect. There is now no effect of pressing the SHIFT key on its own. When using the mouse you highlight a single row, then press SHIFT and click with the mouse. A range is selected. You can release the SHIFT key. If you now press the SHIFT key again and left click in a different row you will highlight the range from the original row to the new clicked row - it doesn't matter if the click was below or above the previous selection, or inside it or outside it.

Using the Keyboard to Edit-In-Place

There are a few places in OpenPetra where we edit the content of a cell directly. An example is on the Transactions tab of the GL Batch screen, which contains a small grid that displays Analysis Types and Analysis Values. The first column displays the analysis type name and the second column displays the selected value for that type. The value is selected from a list of choices in a drop-down ComboBox that 'appears' when you wish to edit the value.

The grid has extensive edit-in-place capabilities but the TAB, ENTER and ESC key handler code had a bug that prevented this from working at all. ENTER should have completed the edit, while ESC should have cancelled the edit (returning the value to what it was before the edit).

The original code had the lines

  CellContext focusCellContext = new CellContext(this, Selection.ActivePosition);
  if (focusCellContext != null && focusCellContext.IsEditing())
  {

However, unfortunately the IsEditing() method always returned false, even when it should have returned true. So we have changed this (in multiple places) to

  CellContext focusCellContext = new CellContext(this, Selection.ActivePosition);
  ICellVirtual contextCell = focusCellContext.Cell;
  if (contextCell != null && contextCell.Editor != null && contextCell.Editor.IsEditing)
  {

New Keyboard Functionality

Having fixed the bug we then went on to add new functionality by modifying the keyboard actions as follows.

It is an OpenPetra 'rule' that the first column of a grid must not contain an editable cell. When we select a row in OpenPetra we implicitly select the first cell of the row. In this position the arrow keys (up/down only) and PgUp/PgDn can be used to select a different row and the TAB key moves you to the next control on the form. If you click ENTER on the first column, the focus will move to the first column that contains an editable cell and the editor will open. Now the up/done/left/right keys move you around the edit area or the drop-down list. ENTER again finishes the edit and moves the selection to the next editable column, if it exists. When there are no more editable columns the focussed cell goes back to the first column and no columns are 'selected'.

If you use SHIFT+ENTER to accept the edit the focus moves to the next row down and the same column.

If you use CTRL+ENTER to accept the edit the focus moves to the next row down and the first column, so no cell is being edited.

If you use ESC to cancel the edit the content reverts to its previous value and the focus moves to the next column that is editable (i.e. the same as ENTER would do).

Note that this ENTER key behaviour overrides any ENTER functionality used by the screen - but only if the grid in question has editable cells. IF the grid has no editable cells, the screen is able to capture the ENTER key and take some action. This happens on the GL Batch Journals tab, where pressing ENTER switches to the Transactions tab.

Focus, Selection and Highlighting

It is important to understand the interplay between data, focus, selection and highlighting in the grid. Actually the grid works the same way as most other Windows controls in that a whole row can be highlighted but there can only ever be one cell that is the cell with the focus. Typically Windows displays the focused cell with a small dotted line border, or on more modern operating systems with a specific colour (often grey or light blue). A highlighted row is typically shown in a dark blue colour. Multiple rows can be highlighted at the same time. There is no requirement for the focused cell to be in the same row as a highlighted (selected) row. This has resulted in the past (when there were mistakes in our code) in having a 'stray blue cell' separate from the selected, highlighted, row.

Furthermore, the data that the grid displays is in no sense bound to the highlighting. If the row filter applied to the grid is changed so that the information displayed in the grid is altered, the highlights remain the same.

In OpenPetra it is obviously important that there is a match between a particular single highlighted row and the row details shown in the details panel beneath the grid. This has to be achieved by careful use of the grid events.

Finally it must be understood that the main grid method for focussing a cell actually places the screen focus on the grid - something that you need to bear in mind may affect keyboard users, who are expecting a particular tab order when entering data. (But see below for ways to deal with this).

Grid Events

There are two fundamental events that we need to make use of

  • an event that allows us to do row validation and, if necessary, cancel the row change. The best event for this is FocusRowLeaving.
  • an event that notifies us that the row has changed so that we can update the details panel beneath the grid. Prior to November 2013 we used FocusedRowChanged but now we use SelectionChanged because this comes much later in the event sequence which makes it more useful to us. However, even to make use of this satisfactorily required some changes to the grid code so that it got fired only when all the highlighted grid rows were fully known.

Please note that you can specify an ActionFocusRow in YAML and traditionally that has been set to FocusedRowChanged. This setting is now ignored by the YAML parser if the grid name is grdDetails. If the grid is grdDetails the ActionFocusRow is always based on the SelectionChanged event. If you use a grid that is not named grdDetails, you should consider using SelectionChanged in place of FocusedRowChanged, although for simple secondary grids on a form it may be that either is satisfactory.

One of the main difficulties with writing code prior to November 2013 related to the FocusStyle property of the grid. It used to be set so that when the grid lost the screen focus the cell lost the focus too. This had the effect of raising a FocusRowLeaving event and setting the ActivePosition to 'undefined'. When the grid became the focussed control again we likewise had to take steps to re-focus the original cell. This was a source of 'unwanted' events. In fact one of the big drivers for changing the grid code was to simplify the grid code and reduce the number of duplicate or unwanted events to zero. As a result the auto-generated code for the main screens became much simpler.

Here is the code that is called when the focus row is about to change

   /// <summary>
   /// FocusedRowLeaving is called when the user (or code) requests a change to the selected row.
   /// </summary>
   private void grdDetails_FocusRowLeaving(object sender, SourceGrid.RowCancelEventArgs e)
   {
       if (!ValidateAllData(true, true))
       {
           e.Cancel = true;
       }
   }

Here is the code that notifies us that a row change has occurred.

   /// <summary>
   /// This is the main event handler for changes in the grid selection
   /// </summary>
   private void grdDetails_RowSelected(object sender, RangeRegionChangedEventArgs e)
   {
       int gridRow = grdDetails.Selection.ActivePosition.Row;
       if (grdDetails.Sorting)
       {
           // No need to ShowDetails - just update our (obsolete) variable
           FPrevRowChangedRow = gridRow;
       }
       else
       {
           ShowDetails(gridRow);
       }
   }

Notice that, as a result of the change to the FocusStyle, we can now use the ActivePosition property of the grid selection to know the current row.

Selecting a Row in Code

There are now three methods that select a row in the grid, but only two of them are likely to be used in manual code.

  • SelectRowInGrid(ARowNumber). This is still the commonest method. It selects the nearest row that it can to the requested row and places the focus on the grid control. Because it uses a 'focus' method it does fire the FocusRowLeaving event which causes the current details to be updated into the data set and a validation check before the row moves. If the row move is successful, a SelectionChanged event is fired, which the auto-generated code will automatically use to update the details panel with matching details to the selected row.
  • SelectRowWithoutFocus(ARowNumber). This also selects the nearest row it can. When you use this method the focussed control does not become the grid. There is no FocusRowLeaving event, so there is no validation and so no call to GetDetailsFromControls. However, there is a SelectionChanged event, so the details panel will get updated to match the newly selected row. Please be clear that it is only safe to use this method in a situation where the row you are moving from is known to contain valid data. This method is useful
    • either when you are already inside a call stack that was initiated from a row focus call, thereby preventing a runaway stack overflow
    • or when you need to select a row in a grid whose data has just been updated due to another control change event. For example, on a screen that has two grids (e.g. Partner | Contact Attributes or Finance | Analysis Types) a click on the upper grid needs to display different data in the lower grid and then select a relevant row. By using this call the focus remains in the upper grid and the user is free to use the up/down arrow keys to move through the rows. (The screen data has already been validated by the code associated with the upper grid).
  • SelectRowAfterSort(ARowNumber). This is used inside Validation to handle the case where a sorted grid has moved the data row because the user has changed the value of the field that is sorted. It selects the specific row requested, which has already been discovered In this case there is no validation required and there is no need to change the details in the panel. It is unlikely that you will use this in your code.

Multi-Row Selection

Many of our screens now support multi-row selection, particularly in the context of deletion. The grid control supports the following ways of highlighting multiple rows:

  • Mouse clicking on a row (so it becomes the only highlighted row) and then CTRL+Left clicking on one or more contiguous or non-contiguous rows.
  • Mouse clicking on a row and SHIFT dragging up or down through contiguous rows
  • Using the keyboard to select a row and then SHIFT+Up arrow or SHIFT+Down arrow to select a contiguous range of rows.

The grid control does not support mouse clicking on a row and then SHIFT+Left mouse click on another row. The reason for this is in the grid control and not in OpenPetra code.

The simplest thing is to try these different options yourself. What you will observe is that every click (or Up/Down) changes the selection to that row - and hence shows the details for that row. To put it another way, when you multi-select using CTRL+mouse click each click changes the details in the details panel. But if you multi-select using click and SHIFT+drag, the selected row is the first row you clicked on. This is very standard Windows behaviour and feels entirely intuitive.

Getting the Selected Row and Row Index

This just got easier too in the new grid code. Internally the auto-generated code maintains a variable FPreviouslySelectedDetailRow. In the earlier grid (prior to Nov 2013) it also kept a variable FPrevRowChangedRow that was needed in order to cope with multiple events. This latter variable is no longer needed but has been kept for backwards compatibility but you should consider that one day we may remove it altogether.

In the new grid code the grid variable Selection.ActivePosition is now valid at all times and we make use of this in the auto-generated code. But in your manual code you should always use the methods described in the next section. This will allow us to develop the internal code in the future without having an impact on existing manual code.

Common Code Tasks

The auto-generated code handles everything to do with the most common grid tasks, including sorting and validation. Even so, your manual code will still need to know which row is selected or to highlight a specific row in response to a row filter change, for example.

Discovering the Current Row

There are two methods to access the current row.

The first one returns the row as a typed data row object so you can access the underlying content. This is taken from the InternationalPostalType screen.

    /// <summary>
    /// Gets the highlighted Data Row as a PInternationalPostalType record from the grid
    /// </summary>
    /// <returns>The selected row - or null if no row is selected</returns>
    public PInternationalPostalTypeRow GetSelectedDetailRow()

The second returns the index of the row in the grid, where 0 corresponds to the header row and 1 is the first row of data.

    /// <summary>
    /// Gets the selected Data Row index in the grid.  The first data row is 1.
    /// </summary>
    /// <returns>The selected row - or -1 if no row is selected</returns>
    public Int32 GetSelectedRowIndex()

Selecting a Row

The basic method for selecting an grid row is to specify the row number. The underlying source grid code always uses the row index when manipulating the grid. But you can also select a grid row by specifying the data row that you want to select. (Only the primary key values are used to find the row.)

In your manual code you will normally select a grid row by its index.

Selecting the row by its row index and setting the focus to the grid

There is a simple method to select a specific row in the grid. This one call does many things.

    /// <summary>
    /// Selects the specified grid row and shows the details for the row in the details panel.
    /// The call still works even if the grid is empty (in which case no row is highlighted).
    /// Grid rows holding data are numbered 1..DataRowCount.
    /// If the specified grid row is less than 1, the first row is highlighted.
    /// If the specified grid row is greater than DataRowCount, the last row is highlighted.
    /// The details panel is disabled if the grid is empty or in Detail Protect Mode
    ///    otherwise the details are shown for the row that has been highlighted.
    /// </summary>
    /// <param name="ARowIndex">The row index to select.  Data rows start at 1</param>
    private void SelectRowInGrid(int ARowIndex)

As the comment indicates, the single parameter is the row that you want to select, where the first data row is number 1.

This method call, which is part of the screen class, does all of the following

  • Gets the current details from the controls in the details panel into the data set.
  • Validates the current row and if necessary displays a message box and cancels the row change to your desired row
  • Forces the specified row to be within the range 1..RowCount
  • Clears the existing selection
  • Highlights the row
  • Ensures that the specified row is visible in the view-port, scrolling the grid if necessary. If possible there will be at least one other visible row above and below the selected row.
  • Shows the details for the row
  • Enables the grid panel unless the grid is empty or in detail protect mode
  • Clears the controls in the details panel if the grid is empty

All these actions happen automatically. Finally, the focus is placed on to the grid so the user is ready to use the up/down arrow keys to select a different row. If you want the focus on a different control, you need to focus that control after making this call. Also please note that Windows cannot focus any control until the screen has been activated. Calling SelectRowInGrid() in the constructor of the screen class (which we do) will perform all the actions described above and all the events will fire as normal, but there will be no focus effect.

If you have manual row addition or deletion code, you can simply call this one method and the nearest available row will be selected and its details displayed. SelectRowInGrid is a powerful method that does everything you need to highlight a row and show all its details.

You can call SelectRowInGrid(9999) or SelectRowInGrid(-99) on a grid with only 5 rows, or even no rows at all and the correct actions will be taken; there should be no need in your manual code for tests to check whether your preferred new row index is within the limits of the row count.

It is worth pointing out that the new grid code has been written so that even if you call SelectRowInGrid with a row index that is the same as the current row index, the grid will still fire a SelectionChanged event. This is valuable to us because there can be situations when the row index might be unchanged but the data in the row is different. This ensures that we can never get a mismatch between the grid row and the details data. It is for this reason that the auto-generated event handler name for the SelectionChanged event is titled RowChanged because that better describes the occasion when the SelectionChanged event is fired.

Selecting the row by its row index without setting the focus to the grid

There is a new method that is useful in some critical circumstances. This method is a member of the OpenPetra TSgrdDataGrid class

  grdDetails.SelectRowWithoutFocus(ARowNumberInGrid)

When you call this method you get a sub-set of the actions taken by SelectRowInGrid described above.

  • Forces the specified row to be within the range 1..RowCount
  • Clears the existing selection
  • Highlights the row
  • Ensures that the specified row is visible in the view-port, scrolling the grid if necessary. If possible there will be at least one other visible row above and below the selected row.
  • Shows the details for the row
  • Enables the grid panel unless the grid is empty or in detail protect mode
  • Clears the controls in the details panel if the grid is empty

This method does all of the above without putting the screen focus onto the grid. There are circumstances where this is important, but please understand that you should only use this method when the current data in the grid is known to be valid because no FocusRowLeaving event will be called - nor will a number of other grid events that you might be relying on. The only event that you will definitely get is the SelectionChanged event that the auto-generated code will use to ensure that the details panel information matches the selected row.

Selecting a row by its row index after existing rows have been shuffled by sorting

There is a third row selection method that you are unlikely to use in manual code.

  grdDetails.SelectRowAfterSort(ARowNumberInGrid)

When you call this method you get an even shorted sub-set of the actions taken by SelectRowInGrid described above.

  • Forces the specified row to be within the range 1..RowCount
  • Clears the existing selection
  • Highlights the row
  • Ensures that the specified row is visible in the view-port, scrolling the grid if necessary. If possible there will be at least one other visible row above and below the selected row.

This method assumes that the row being selected is the same as the row being moved from. The data may or may not be valid. This method is used simply to ensure that the highlighted row in the grid is the row that belongs with the current details.

Selecting a row by specifying the DataRow

Much less commonly you may want to select a row for which you know the DataRow object but do not know where it is. In this case you first find the row index and then make the standard call.

int rowToSelect = grdDetails.DataSourceRowToIndex2(TheDataRowToSelect);
SelectRowInGrid(rowToSelect);

As you will see, this still uses SelectRowInGrid so has all the fetaures described above.

Scrolling a Row Into View

This task is normally taken care of simply by selecting a row in the grid (see above), but if you need to do this explicitly there is a method to do it, which will ensure that, if possible, the highlighted row has at least one row above and below it.

        /// <summary>
        /// This is the OpenPetra override.  It scrolls the window so that the specified row is shown.
        /// The standard grid behaviour would be simply to ensure the selected row is within the grid.
        /// With this method, where possible there is always one unselected row above or one row below.
        /// </summary>
        /// <param name="ARowNumberInGrid">The grid row number that needs to be inside the viewport</param>
        /// <returns>False if the grid scrolls to a new position.  True if the specified row is already in the view port</returns>
        public bool ShowCell(int ARowNumberInGrid)

Adding Rows

Standard code for adding rows is part of the templates. An appropriate new record is added to the database and the new row is highlighted - bearing in mind that we have to locate it in the grid. It will not be the last row if the grid is sorted. Usually there is a small requirement for manual code when adding a row, in order to ensure that the new row has a unique primary key.

Deleting Rows

The code for deleting rows is now in the templates. In the simplest case the manual code only needs to handle the button click event and call the standard auto-generated code method. For the InternationalPostalType screen the method is DeletePInternationalPostalType().

The auto-generated Delete{#DETAILTABLE} procedure can call up to 3 optional manual code methods that control the deletion process. If you want more control over the deletion process than is provided by the generic code, then you can include your own variant of any or all of the following in your manual code file.

Please note that the following code is for illustration only. The InternationalPostalType screen does not in fact use any of these manual methods because the default behaviour is all that is necessary.

  • We should use the standard deletion question wherever possible in order to maintain consistency between screens
  • We should not normally have a completion message like 'The row was deleted successfully'. That annoys the user and it is obvious because the row is no longer in the grid! It is good to have a completion message in cases where the behaviour is somewhat non-standard, such as where the code has already saved the changes, which happens on a few finance screens.
  • We should not wrap our deletion code in a try/catch block. There is already a try/catch block in the calling code.
  • We should not display message boxes inside these methods because on multi-row delete they would get shown every time.
/// <summary>
/// Performs checks to determine whether a deletion of the current
///  row is permissible
/// </summary>
/// <param name="ARowToDelete">the currently selected row to be deleted</param>
/// <param name="ADeletionQuestion">can be changed to a context-sensitive deletion confirmation question</param>
/// <returns>true if user is permitted and able to delete the current row</returns>
private bool PreDeleteManual(PInternationalPostalTypeRow ARowToDelete, ref string ADeletionQuestion)
{
	/*Code to execute before the delete can take place.  Validate that the specified row can be deleted?*/

	/*Code to modify the deletion question*/

	/*return true if deleting this row is allowed*/
	return true;
}

/// <summary>
/// Deletes the current row and optionally populates a completion message
/// </summary>
/// <param name="ARowToDelete">the currently selected row to delete</param>
/// <param name="ACompletionMessage">if specified, is the deletion completion message</param>
/// <returns>true if row deletion is successful</returns>
private bool DeleteRowManual(PInternationalPostalTypeRow ARowToDelete, ref string ACompletionMessage)
{
	bool deletionSuccessful = false;
	
	/*Code to prepare for deletion*/

	/*Code to delete rows from dependent tables first*/

	/*Code to perform the row deletion on this table*/
	
	return deletionSuccessful;
}

/// <summary>
/// Code to be run after the deletion process
/// </summary>
/// <param name="ARowToDelete">the row that was/was to be deleted</param>
/// <param name="AAllowDeletion">whether or not the user was permitted to delete</param>
/// <param name="ADeletionPerformed">whether or not the deletion was performed successfully</param>
/// <param name="ACompletionMessage">if specified, is the deletion completion message</param>
private void PostDeleteManual(PInternationalPostalTypeRow ARowToDelete, bool AAllowDeletion, bool ADeletionPerformed, string ACompletionMessage)
{
	/*Code to execute after the delete has occurred - for example updating the main screen*/

}

The DeleteRowManual()'s second argument, ref string ACompletionMessage (which is passed local variable completionMessage), is always sent in empty. If it is populated in the method after a successful delete and there is no PostDeleteManual() method present, then a MessageBox displaying completionMessage string will appear. If you create a PostDeleteManual() method, completionMessage is passed in as an argument.

Please note that when multiple rows are deleted together the deletion question is ignored. The completion question is displayed after all the rows have been deleted. This means that if you check its content at each call to DeleteRowManual it will be empty the first time so you should set it to a suitable string for a single row delete. If the string is not empty this must be a multi-row delete and you may wish to change the text.

You can read more about Adding Delete Functionality to a Screen or Control here.

Coding For Using the Keyboard

The grid control contains handlers as standard for the Home/End, Up/Down and PgUp/PgDn keys. The Insert and Delete keys also do what their name suggests as an alternative to clicking the New or Delete buttons on a form, provided that the YAML file specification has btnNew and/or btnDelete.

Coding for the ENTER key only requires slightly more work as you can see, for example, in Ict.Petra.Client.MFinance.Gui.Gift.UC_GiftBatches.yaml

Under the grdDetails section we have:

ActionDoubleClick: ShowTransactionTab
ActionEnterKeyPressed: ShowTransactionTab

The code then generates:

this.grdDetails.EnterKeyPressed += new TKeyPressedEventHandler(this.ShowTransactionTab);

This allows the programmer to capture the ENTER key for any grid and call any code. In this example it is the same code as the double click, but it doesn't have to be, of course.

Code Snippets That Should NOT Appear in Your Manual Code

The manual code class contains several variables and methods that are used to maintain state. A number of them seem at first glance to be useful in manual code, but you must resist the temptation! There are certain variables that you should not modify and the use of some of the methods may mean that important variables do not get set.

Here is a list of coding statements that should not appear in your code.

Code Comment
grdDetails.Selection.ResetSelection(false) This call clears the selection in the grid and is often followed by the grdDetails.SelectRowInGrid(rowToSelect) call. You should use the standard SelectRowInGrid method instead.
grdDetails.SelectRowInGrid(rowToSelect) This also may appear to give the desired result, but you are using methods that are at too low a level. You should leave this call to the low level code and use the higher level method SelectRowInGrid instead.
grdDetails.Selection.ActivePosition.Row This property used to be unreliable. Since the recent grid changes it has become much more useful, but again it is a low-level method. Your manual code will be future-proof if you use GetSelectedRowIndex() instead.
FPreviouslySelectedDetailRow = null You should not modify this Form variable directly, or, if you do you must be sure to follow it with a call to SelectRowInGrid(N) so that the variable can be reset to a valid object that reflects the content of the details panel.
FPrevRowChangedRow = 1 Same comments applies as the row above
ShowDetails(ADataRow) This would almost certainly give rise to a mismatch between the selected row and the content of the details panel
ShowDetails() This line in your code would not be 'bad' but I cannot think of a reason that you would need it! It simply shows the details for the 'current' row, which should be the details already shown. So, usually this line would be superfluous.

Unfortunately many of these snippets have appeared in our manual code over time as the auto-generated code has developed. Now that most grid templates have more standardised methods we need to progress to using these and remove some of the older code. Usually it will result in fewer lines of code - which will be better in itself!

Grid Testing

Currently, all code generated from the templates compiles and a number of basic maintain screens have been tested for correct behaviour. A small stand-alone application would be needed to test the behaviour in isolation. However any testing would need to include all the validation framework as well as addition and deletion of data and so on.

For the present time this documentation lists all the tests that need to be performed manually to ensure that the grid behaviour is correct. In the future it may be possible to automate all these tests.

The tests use two screens:

  • International Postal Type (in Client | MCommon | Gui | Setup). This screen has add and delete functionality as well as manual and automatic data validation.
  • Contact Attributes (in Client | MPersonnel | Gui | Setup). This screen has two grids dependent on each other, row filtering and add/delete.

Each test is numbered and described and the expected outcome is explained.

Tests Using International Postal Type

The demo database already contains a useful amount of data for this screen.

  1. Initial Load
    1. Load the screen and visually check that the first row in the grid is highlighted and that the details panel is showing the correct data for the first row. Check that the grid has the initial focus by pressing TAB: the New button should now be focussed.
    2. Errors: It is an error if, for example, the initial highlight is on the Code in the details panel.
  2. Using the keyboard to move around the screen
    1. Reload the screen. Do not touch the mouse, but use the keyboard to TAB from control to control. Confirm that each control is focussed in turn. When you have TABbed back to the grid use the up/down arrow keys to cursor through the grid rows. Ensure that the details update correctly every time. Resize the window so that you can experiment with the PgUp/PgDn keys.
    2. Errors: It is an error if, for example, TABbing from the postal code control in the details panel jumps you back to the grid.
  3. Using the mouse to move around the screen
    1. Reload the screen. Repeat the previous keyboard tests using the mouse (do not click the New button yet and do not change any data). Confirm that the details panel always shows the correct data for the row that you have highlighted.
  4. Using the keyboard to create new rows (1)
    1. Reload the screen, shrink the height so that not all rows can be displayed, press the TAB key once, then press SPACE. Confirm that
      1. a new row is added at the bottom and is highlighted
      2. the grid scrolls to show it
      3. the focus moves to the NEWCODE control
    2. Now edit the text for the code and press TAB. Edit the description and press TAB. Do not intentionally fail validation yet (that comes in a later test!). Keep TABbing and editing until you reach the New button again. Then press SPACE to add another row. Repeat this until you have added three new rows. Confirm that all your edits have been applied and that there was never a time where you TABbed to an unexpected control.
    3. Confirm that the Save button in the toolbar is enabled.
    4. Close the window without saving your changes.
    5. Errors: it is an error if the row does not appear inside the view-port, or the data in the grid does not match the data in the details panel when you have TABbed back to the grid, or the focus of the new row is not on the CODE control.
  5. Using the keyboard to create new rows (2)
    1. Reload the screen, shrink the height so that not all rows can be displayed, then press the Insert key. Confirm that
      1. one new row has been added at the bottom
      2. the grid has scrolled to show it
      3. the focus has moved to the text box for the CODE
    2. Edit the CODE and TAB back to the grid. Press the Insert key again and repeat the test.
    3. Close the window without saving your changes.
    4. Errors: It is an error, for example, if the focus does not change to the NEWCODE control.
  6. Using the mouse to create new rows
    1. Reload the screen, shrink the height so that not all rows can be displayed, then click the New button once. Confirm that
      1. one new row has been added at the bottom
      2. the grid has scrolled to show it
      3. the focus moves to the NEWCODE control
    2. Close the window without saving your changes.
    3. Errors: it is an error if the row does not appear inside the view-port, or the data in the grid does not match the data in the details panel when you have TABbed back to the grid, or the focus of the new row is not on the CODE control.
  7. Creating new rows in a sorted grid (1)
    1. Reload the screen, click the Sort Header twice on the first column so that the items are sorted Z at the top and A at the bottom. Shrink the height of the window so that not all rows can be displayed. Click New twice to create 2 new rows. Confirm that
      1. The two rows have been inserted into the grid correctly
      2. If necessary the grid has scrolled the rows into view
      3. the second inserted row is the upper of the two and that the details panel shows the details for this second row.
      4. the CODE in the details panel has the focus
    2. Highlight a different row, then click New again. Confirm that the new row is highlighted and its correct details displayed.
    3. Close the window without saving your changes.
    4. Errors: There are numerous possible errors that can occur if there are mistakes in the code. The wrong row might be highlighted; the wrong details may be displayed; the focus may be incorrect.
  8. Creating new rows in a sorted grid (2)
    1. Reload the screen, click the Sort Header twice on the second column so that the Description items are sorted Z at the top and A at the bottom. Shrink the height of the window so that not all rows can be displayed. Click New twice to create 2 new rows. Confirm that
      1. The two rows have been inserted into the grid correctly
      2. If necessary the grid has scrolled the rows into view
    2. Repeat, creating new rows but editing the description so that the rows are in different parts of the grid.
    3. Be sure to test the case where the highlighted row before you create a new row is the row where the new row will appear (the row where the description text is just before Please Enter Description in the alphabet).
    4. Close the window without saving your changes.
    5. Errors: Look for the same class of error as when the first column was sorted.
  9. Adding invalid rows to a sorted grid (1)
    1. Reload the screen, click the Sort Header twice on the first column so that the Code items are sorted Z at the top and A at the bottom. Click New once to create a new row in the middle of the grid. The CODE text box will have the focus, so without doing anything with the mouse or keyboard simply press the DEL key to make the CODE go blank. Now press TAB. Confirm that
      1. the highlighted row has jumped to the bottom (blank comes before A)
      2. the validation tooltip has shown invalid data
      3. the focus is now on the Description textbox
      4. all the other data entry details match the highlighted grid row information
    2. Now, using the mouse, click the New button. Confirm that
      1. a message box pops up notifying you of invalid data
    3. Close the box and confirm that
      1. the focus is now on the CODE text box once more
    4. Press CTRL+Z and confirm that
      1. the previous value of the CODE is restored
    5. Now press TAB to move to the description. Confirm that
      1. the highlighted row moves back into the middle region of the grid
    6. Close the window without saving your changes.
    7. Errors: There are numerous errors that can occur when the underlying code is wrong. These errors include the wrong row being highlighted, the wrong control being focussed and wrong information being displayed in the details panel.
  10. Adding invalid rows to a sorted grid (2)
    1. You can repeat the previous test using a sorted second (Description) column, this time TABbing to the Description field and pressing DEL so that it becomes blank and the TABbing once more. Look for all the same errors. Instead of activating the message box by clicking New, do so by clicking Save. Also, instead of using CTRL+Z to restore the original text, simply edit it to a different value and confirm that it finds the correct location in the grid.
    2. Close the window without saving your changes.
  11. Adding invalid rows to a sorted grid (3)
    1. Reload the screen, click the Sort Header twice on the first column so that the Code items are sorted Z at the top and A at the bottom. Click New once to create a new row in the middle of the grid. The CODE text box will have the focus, so without doing anything with the mouse or keyboard simply press the DEL key to make the CODE go blank. Now click somewhere in the grid with the mouse. Confirm that
      1. The highlighted row jumps to the correct position for a blank CODE
      2. The error dialog immediately pops up, preventing you from moving to the row you clicked on.
    2. Accept the dialog and confirm that
      1. The focus moves to the offending control
    3. Close the window without saving your changes.
    4. Errors: It is a serious error if the dialog does not appear or if the row highlight moves to the row you clicked on.
  12. Editing existing data
    1. Reload the screen and add 4 new rows (which will appear at the bottom)
    2. Now select each of the first two rows in turn using the mouse and edit their contents, without causing validation errors. Confirm that the data appears correctly in the grid.
    3. Now using the keyboard only (arrow keys and TAB key), repeat the test on the second pair of added rows.
    4. Now repeat these two tests using mouse/keyboard, but make the CODE blank and make the Description blank. Confirm that
      1. The tab sequence was correct and that all controls are in the TAB sequence
      2. The tool-tips appeared advising of Bad Data
      3. That you cannot leave a row that contains bad data using the cursor keys when the grid has focus
      4. That you cannot leave a row that contains bad data by clicking New or Save
      5. That after the accepting the message box listing all the current errors, the first error control is focussed ready to be corrected.
    5. Close the window without saving your changes.
    6. Errors: There are numerous errors that can occur when the underlying code is wrong. These errors include the wrong control being focussed and wrong information being displayed in the details panel.
  13. Editing existing data in a sorted grid (1)
    1. Reload the screen and click on the sort header of the first column twice, so it is sorted Z at the top and A at the bottom
    2. Add 1 new row (which will appear near the middle). The CODE should be focussed.
    3. Edit the CODE, without causing validation errors. TAB to the Description. Confirm that
      1. the data appears correctly in the grid
      2. the 'current' row still has the highlight wherever it has moved to.
    4. Shift+TAB back to the CODE and try various other values, confirming that the highlight always tracks the row.
    5. Now repeat this test, but make the CODE blank and make the Description blank. Confirm that
      1. The tab sequence was correct and that all controls are in the TAB sequence
      2. The tool-tips appeared advising of Bad Data
      3. That you cannot leave a row that contains bad data using the cursor keys when the grid has focus
      4. That you cannot leave a row that contains bad data by clicking New or Save
      5. That after the accepting the message box listing all the current errors, the first error control is focussed ready to be corrected.
    6. Close the window without saving your changes.
    7. Errors: There are numerous errors that can occur when the underlying code is wrong. These errors include the wrong row being highlighted, the wrong control being focussed and wrong information being displayed in the details panel.
  14. Editing existing data in a sorted grid (2)
    1. Reload the screen and click on the sort header of the second column twice, so it is sorted Z at the top and A at the bottom
    2. Add 1 new row (which will appear near the middle). The CODE should be focussed.
    3. TAB to the Description and edit it, without causing validation errors. TAB away from the Description. Confirm that
      1. the data appears correctly in the grid
      2. the 'current' row still has the highlight wherever it has moved to.
    4. Shift+TAB back to the Description and try various other values, confirming that the highlight always tracks the row.
    5. Now repeat this test, but make the Description blank. Confirm that
      1. The tab sequence was correct and that all controls are in the TAB sequence
      2. The tool-tip(s) appeared advising of Bad Data
      3. That you cannot leave a row that contains bad data using the cursor keys when the grid has focus
      4. That you cannot leave a row that contains bad data by clicking New or Save
      5. That after the accepting the message box listing all the current errors, the first error control is focussed ready to be corrected.
    6. Close the window without saving your changes.
    7. Errors: There are numerous errors that can occur when the underlying code is wrong. These errors include the wrong row being highlighted, the wrong control being focussed and wrong information being displayed in the details panel.
  15. Checking the Horizontal Scrollbar
    1. Reload the screen and resize the width so that a horizontal scrollbar appears at the bottom of the grid. Now resize the height so that there is enough room for one more row but not for two, without the second being at least partially masked.
    2. Click the New button to add a new row. Confirm that it appears and is highlighted. Now click the New button a second time. Confirm that
      1. a vertical scrollbar has been added to the grid and that the new row is entirely visible.
    3. Close the window without saving your changes.
    4. Errors: It is an error if the new row is partially hidden and no vertical scrollbar is visible.
  16. Checking the Horizontal and Vertical Scrollbars
    1. Reload the screen and resize the width so that a horizontal scrollbar appears at the bottom of the grid. Then increase the width carefully until the scrollbar disappears. Now resize the height so that there is enough room for one more row but not for two, without the second being at least partially masked.
    2. Click the New button to add a new row. Confirm that it appears and is highlighted. Now click the New button a second time. Confirm that
      1. a vertical and horizontal scrollbar have been added to the grid and that the new row is entirely visible and so that you can scroll horizontally to see all the columns. The horizontal scrollbar had to be added because the vertical scrollbar covered the right hand edge of the right-most column. You might have to fiddle with the window width a few times to see this effect, because the grid column widths change with the overall width.
    3. Close the window without saving your changes.
    4. Errors: It is an error if the new row is partially hidden and no vertical scrollbar is visible, or if part of a column is not visible and there is no horizontal scrollbar.
  17. Deleting Rows from the Grid
    1. Reload the screen and without sorting add five new rows, which will appear at the bottom of the grid. With the bottom row selected, click the Delete button and accept the message boxes. Confirm that
      1. the bottom row was deleted
      2. the new highlighted row is the new bottom row
      3. the details panel shows the details for the bottom row
    2. Now select the row one up from the bottom and click the Delete button once more. Confirm that
      1. the highlighted row was deleted
      2. the highlighted row index did not change, but the data has changed to the row that was at the bottom before the delete.
      3. the details panel shows the correct data.
    3. With these two tests you have covered the cases where the last row is deleted and where the deleted row has at least one row below.
    4. Close the window without saving your changes.
    5. Errors: It is an error if the wrong row is deleted, or if the highlight is wrong after deleting the row, or if the details after the delete do not match the information in the highlighted row.

Tests Using Contact Attributes

All of the tests conducted on International Postal Type can apply to the Contact Attributes screen, but there are additional tests that are important because the screen has some complex features. The information displayed in the lower grid depends on the row selected in the upper grid. It is not 'allowed' to have an empty lower grid, so when a new row is created in the upper grid the manual code automatically creates the first row in the lower grid as well.

It is important to start these tests with an empty database table, so please use your administration tool to delete all rows from p_contact_attribute and p_contact_attribute_detail before you start. Normally the demo database has no data in these tables.

  1. Initial Load
    1. Load the screen and visually check that the grids are empty and that both details panels are disabled. Check that the grid has the initial focus by pressing TAB: the New button should now be focussed. Continue TABbing and check that all the controls that are enabled get the focus in turn.
    2. Errors: It is an error if, for example, the details panel is not disabled.
  2. Adding Rows Using the Keyboard (1)
    1. Reload the screen, TAB to the New button and press SPACE. Confirm that
      1. a new row is added
      2. the focus is on the NEWCODE for the upper grid
      3. a new row is added to the lower grid
      4. both details panels match the highlighted grid rows
    2. Edit the code and continue editing/TABbing until you reach the New button for the lower grid. Press SPACE. Confirm that
      1. a new row is added to the lower grid
      2. the focus is on the CODE for the lower panel
      3. all the details match the grids
    3. Close the window without saving your changes.
    4. Errors: It is an error if the focus is on the wrong control, or if you cannot TAB round all the controls in sequence. It is an error if the highlighted grid information is not the same as its corresponding details panel.
  3. Adding Rows Using the Keyboard (2)
    1. Reload the screen, but this time add a row to the top grid by pressing the INS key. Confirm that
      1. one row got added to the upper grid, and one row in the lower grid
      2. the focus is CODE for the upper grid
    2. Now TAB to the lower grid and press the INS key. Confirm that
      1. one row got added to the upper grid, and one row in the lower grid
      2. the focus is CODE for the upper grid
    3. This shows a limitation of the implementation of the keyboard shortcut handler. It is associated with the main grid and not the child grid.
    4. Close the window without saving your changes.
    5. Errors: It is an error if the focus is on the wrong control, or if you cannot TAB round all the controls in sequence, or use the arrow keys to select different rows. It is an error if the highlighted grid information is not the same as its corresponding details panel.
  4. Adding Rows Using the Mouse
    1. Reload the screen, but this time add three rows to the top grid by clicking the New button three times. Confirm that
      1. the upper grid has three rows
      2. the focus is on the CODE for the upper grid
      3. there is one row in the lower grid
      4. all the details are correct for the highlighted grid rows
    2. Click the New button for the bottom grid three times. Confirm that
      1. the lower grid has four rows
      2. the focus is on the CODE for the lower grid
      3. the details are all correct
    3. Use the mouse or the keyboard arrow keys to confirm that there is one selection in the top grid that has four rows in the bottom grid, while the other top selections only have one bottom grid row
    4. Close the window without saving your changes.
    5. Errors: It is an error if the focus is on the wrong control, or if you cannot TAB round all the controls in sequence, or use the arrow keys to select different rows. It is an error if the highlighted grid information is not the same as its corresponding details panel.
  5. Deleting Rows
    1. Background: If you delete a row from the upper grid, all the associated rows in the lower grid are also deleted. If you delete a row in the lower grid it has no effect on the upper grid, except that the column that shows the number of detail attributes will change.
    2. Repeat each of the tests above (keyboard and mouse) and create, say three rows in the upper grid each with a different number of rows in the lower one.
    3. Now TAB or SHIFT+TAB to each of the Delete buttons in turn and press SPACE. Check the correct row(s) are deleted. Or just click the Delete buttons with the mouse.
    4. Do this on the first row in a grid and the last row in a grid.
    5. Continue deleting until all the rows have been deleted.
    6. Repeat this test multiple times in different ways
    7. In all these tests confirm that
      1. the correct row is deleted
      2. the focus remains on the relevant grid
      3. the details panel always matches the grid
    8. Close the window without saving your changes.
  6. Editing the Contact Attribute Code
    1. Background: If you change the CODE for the upper grid, you also modify a field in the table associated with the lower grid. If this doesn't work right you can appear to 'lose' the details for the attribute you just edited.
    2. Reload the screen and create a few attributes rows in the upper grid.
    3. Add a few details in the lower grid for each of these attributes.
    4. Be sure that you know the values for the details corresponding to a particular attribute, by editing them to something unique.
    5. Highlight, say, the second or third detail attribute in the lower grid.
    6. Go to the text box for the upper grid's CODE and edit it to be a different value, then TAB to the next control. Confirm that
      1. the new code value has been entered into the upper grid
      2. the lower grid content has not changed
      3. the lower grid selection has not changed
    7. Close the window without saving your changes.
    8. Errors: when you change the attribute code the detail table has to be updated and the content for the grid re-filtered. This should not change the result set to be displayed but this is a good test of several grid display issues. It is an error if the lower grid content changes or the highlighted row changes.