SourceGrid specification and testing: Difference between revisions

From OpenPetra Wiki
Jump to navigation Jump to search
No edit summary
Line 95: Line 95:


== Handling Grid Events in OpenPetra Code ==
== Handling Grid Events in OpenPetra Code ==
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 change.  Once FocusedRowChanged happens the code has to respond to the fact that the grid selection has changed.


===Event - FocusRowLeaving===
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 code.  However, 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 guidelines.
This is the even that fires most often and needs to be curtailed and checked for repeats. Here's the code from the template with supporting methods etc.:


=== Event - FocusedRowChanged ===
=== Inside the FocusRowLeaving Event ===
Basically, the supporting code needs to identify the user action and behave accordingly, this includes selecting, sorting, adding and deleting rows. The InvokeFocusedRowChanged() method allows the calling of the FocusedRowChanged event code when it needs to be called manually.
The only action that we might take inside this event handler is to cancel the RowLeaving because the current data entered fails validation.  So 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.
 
It is worth stating here that it is important to be very aware of all the code that runs during the FocusRowLeaving event - it is quite easy to do something in validation that will cause another FocusRowLeaving event to occur, which, if not handled correctly will lead to a stack overflow.
 
=== Inside the FocusedRowChanged Event ===
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 controls.  So this event handler will certainly contain:
<pre>
    ShowDetails();
</pre>
That's all we have to do in response to a row change.
 
=== Handling Multiple Events ===
This is the most difficult issue for OpenPetra programmers in respect of the grid.  It is easy to say that the simple lines of code are all we need inside each pof these two methods, but 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 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!
 
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 two Invalid Data message boxes popping up one after the other!
 
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 -1.  This 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 events.  If the proposed row is one 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 is a very long time but much shorter than any keyboard repeat time.
 
 
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.
 
 
=== Handling a Sorted Grid ===
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, and we do get notified that the FocusedRow has changed.  In this case we do not need to show the details because the details have not changed - only the position of the data row in the grid.
 
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 FocusRowLeaving and FocusedRowChanged events 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 before the New button is pressed is the same row as where the new row ends up.  In 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.
 
=== Handling Validation ===
Row data validation has introduced new complexities into the event handling mix.  The 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 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'''.  With careful programming we can capture all the correct events and respond in a completely standard way.  Please understand that we never in the generated code 'turn off' any events, nor do we ever 'cancel' any (see above), but we do 'ignore' genuine duplicates, as explained above.
 
Here is the complete code for FocusRowLeaving:
<pre>
    /// FocusedRowLeaving can be called multiple times (e.g. 3 or 4) for just one FocusedRowChanged event.
    /// The key is not to cancel the extra events, but to ensure that we only ValidateAllData once.
    /// We ignore any event that is leaving to go to row # -1
    /// 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)
        {
            double elapsed = (DateTime.UtcNow - FDtPrevLeaving).TotalMilliseconds;
            bool bIsDuplicate = (e.Row == FPrevLeavingFrom && e.ProposedRow == FPrevLeavingTo && elapsed < 2.0);
            if (!bIsDuplicate)
            {
                //Console.WriteLine("{0}: FocusRowLeaving: from {1} to {2}", DateTime.Now.Millisecond, e.Row, e.ProposedRow);
                if (!ValidateAllData(true, true))
                {
                    //Console.WriteLine("{0}:    --- Cancelled", DateTime.Now.Millisecond);
                    e.Cancel = true;
                }
            }
            FPrevLeavingFrom = e.Row;
            FPrevLeavingTo = e.ProposedRow;
            FDtPrevLeaving = DateTime.UtcNow;
        }
    }
</pre>
 
Here is the complete code for FocusedRowChanged:
<pre>
    /// <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.
    /// 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>
    private int FPrevRowChangedRow = -1;
    private void FocusedRowChanged(System.Object sender, SourceGrid.RowEventArgs e)
    {
        // The FocusedRowChanged event simply calls ShowDetails for the new 'current' row implied by e.Row
        // We do get a duplicate event if the user tabs round all the controls multiple times
        // 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.
        if (!grdDetails.Sorting && e.Row != FPrevRowChangedRow)
        {
            //Console.WriteLine("{0}:  FRC ShowDetails for {1}", DateTime.Now.Millisecond, e.Row);
            ShowDetails();
        }
        FPrevRowChangedRow = e.Row;
    }
</pre>


== Common Code Tasks ==
== Common Code Tasks ==

Revision as of 18:03, 25 October 2012

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.

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.

First we will explain the two bug fixes, then we will examine how to code for the events that we use. 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

Unfortunately the grid contains two 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.

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. 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);
	}

Handling Grid Events in OpenPetra Code

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 change. Once FocusedRowChanged happens the code has to respond to the fact that the grid selection has changed.

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 code. However, 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 guidelines.

Inside the FocusRowLeaving Event

The only action that we might take inside this event handler is to cancel the RowLeaving because the current data entered fails validation. So this event handler contains the line:

    if (!ValidateAllData(true, true))
    {
        e.Cancel = true;
    }

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.

It is worth stating here that it is important to be very aware of all the code that runs during the FocusRowLeaving event - it is quite easy to do something in validation that will cause another FocusRowLeaving event to occur, which, if not handled correctly will lead to a stack overflow.

Inside the FocusedRowChanged Event

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 controls. So this event handler will certainly contain:

    ShowDetails();

That's all we have to do in response to a row change.

Handling Multiple Events

This is the most difficult issue for OpenPetra programmers in respect of the grid. It is easy to say that the simple lines of code are all we need inside each pof these two methods, but 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 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!

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 two Invalid Data message boxes popping up one after the other!

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 -1. This 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 events. If the proposed row is one 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 is a very long time but much shorter than any keyboard repeat time.


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.


Handling a Sorted Grid

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, and we do get notified that the FocusedRow has changed. In this case we do not need to show the details because the details have not changed - only the position of the data row in the grid.

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 FocusRowLeaving and FocusedRowChanged events 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 before the New button is pressed is the same row as where the new row ends up. In 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.

Handling Validation

Row data validation has introduced new complexities into the event handling mix. The 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 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. With careful programming we can capture all the correct events and respond in a completely standard way. Please understand that we never in the generated code 'turn off' any events, nor do we ever 'cancel' any (see above), but we do 'ignore' genuine duplicates, as explained above.

Here is the complete code for FocusRowLeaving:

    /// FocusedRowLeaving can be called multiple times (e.g. 3 or 4) for just one FocusedRowChanged event.
    /// The key is not to cancel the extra events, but to ensure that we only ValidateAllData once.
    /// We ignore any event that is leaving to go to row # -1
    /// 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 < 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)
        {
            double elapsed = (DateTime.UtcNow - FDtPrevLeaving).TotalMilliseconds;
            bool bIsDuplicate = (e.Row == FPrevLeavingFrom && e.ProposedRow == FPrevLeavingTo && elapsed < 2.0);
            if (!bIsDuplicate)
            {
                //Console.WriteLine("{0}: FocusRowLeaving: from {1} to {2}", DateTime.Now.Millisecond, e.Row, e.ProposedRow);
                if (!ValidateAllData(true, true))
                {
                    //Console.WriteLine("{0}:    --- Cancelled", DateTime.Now.Millisecond);
                    e.Cancel = true;
                }
            }
            FPrevLeavingFrom = e.Row;
            FPrevLeavingTo = e.ProposedRow;
            FDtPrevLeaving = DateTime.UtcNow;
        }
    }

Here is the complete code for FocusedRowChanged:

    /// <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.
    /// 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>
    private int FPrevRowChangedRow = -1;
    private void FocusedRowChanged(System.Object sender, SourceGrid.RowEventArgs e)
    {
        // The FocusedRowChanged event simply calls ShowDetails for the new 'current' row implied by e.Row
        // We do get a duplicate event if the user tabs round all the controls multiple times
        // 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.
        if (!grdDetails.Sorting && e.Row != FPrevRowChangedRow)
        {
            //Console.WriteLine("{0}:   FRC ShowDetails for {1}", DateTime.Now.Millisecond, e.Row);
            ShowDetails();
        }
        FPrevRowChangedRow = e.Row;
    }

Common Code Tasks

The above event ensures the correlation between the FCurrentRow variable and the current row, especially during sorting etc., and it also ensures, on a delete, that the record at the same index position (as determined by sort order) or 1 above is always chosen.

Selecting a Row

Discovering the Current Row

Adding Rows

Where the form templates contain code for adding records to the grid, changes have had to be made to control event firing. Here is an example:


Deleting Rows

The code for deleting rows is now in the templates:

The autogenerated Delete{#DETAILTABLE} procedure can call up to 3 optional manual code methods that control the deletion process (see code below for example with description of arguments etc.):

/// <summary>
/// Performs checks to determine whether a deletion of the current
///  row is permissable
/// </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(ref PInternationalPostalTypeRow ARowToDelete, ref string ADeletionQuestion)
{
	/*Code to execute before the delete can take place*/
	ADeletionQuestion = String.Format(Catalog.GetString("Are you sure you want to delete Postal Type Code: '{0}'?"),
					  ARowToDelete.InternatPostalTypeCode);
	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(ref PInternationalPostalTypeRow ARowToDelete, out string ACompletionMessage)
{
	bool deletionSuccessful = false;
	
	try
	{
		//Must set the message parameters before the delete is performed if requiring any of the row values
		ACompletionMessage = String.Format(Catalog.GetString("Postal Type Code: '{0}' deleted successfully."),
						   ARowToDelete.InternatPostalTypeCode);
		ARowToDelete.Delete();
		deletionSuccessful = true;
	}
	catch (Exception ex)
	{
		ACompletionMessage = ex.Message;
		MessageBox.Show(ex.Message,
				"Deletion Error",
				MessageBoxButtons.OK,
				MessageBoxIcon.Error);
	}
	
	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(ref PInternationalPostalTypeRow ARowToDelete, bool AAllowDeletion, bool ADeletionPerformed, string ACompletionMessage)
{
	/*Code to execute after the delete has occurred*/
       	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
	}
}

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.

Adding and Deleting Rows Using the Keyboard

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.

Here is a typical yaml file section for the two buttons:

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

And then in the manual code, you call the generated code to create/delete a record:

private void NewRecord(Object sender, EventArgs e)
{
	CreateNewPInternationalPostalType();
}

private void DeleteRecord(Object sender, EventArgs e)
{
	DeletePInternationalPostalType();
}

Grid Testing

Testing needs to be setup where we can test the grid on all its basic functions in forms derived from each of the templates.

Currently, all code generated from the templates compiles and a number of basic maintain screens have been tested for correct behaviour, but a smaller stand-alone application will also be needed to test the behaviour in isolation.