SourceGrid specification and testing

From OpenPetra Wiki
Jump to navigation Jump to search

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


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

Event - FocusRowLeaving

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.:

    private bool FInitialFocusEventCompleted = false;
    private bool FNewFocusEvent = false;
    private bool FRepeatLeaveEventDetected = false;
    private int FDetailGridRowsCountPrevious = 0;
    private int FDetailGridRowsCountCurrent = 0;
    private int FDetailGridRowsChangedState = 0;

    private void FocusPreparation(bool AIsLeaveEvent)
    {
        if (FRepeatLeaveEventDetected)
        {
            return;
        }

        FDetailGridRowsCountCurrent = grdDetails.Rows.Count;

        //first run only
        if (!FInitialFocusEventCompleted)
        {
            FInitialFocusEventCompleted = true;
            FDetailGridRowsCountPrevious = FDetailGridRowsCountCurrent;
        }

        //Specify if it is a row change, add or delete
        if (FDetailGridRowsCountPrevious == FDetailGridRowsCountCurrent)
        {
            FDetailGridRowsChangedState = 0;
        }
        else if (FDetailGridRowsCountPrevious > FDetailGridRowsCountCurrent)
        {
            FDetailGridRowsCountPrevious = FDetailGridRowsCountCurrent;
            FDetailGridRowsChangedState = -1;
        }
        else if (FDetailGridRowsCountPrevious < FDetailGridRowsCountCurrent)
        {
            FDetailGridRowsCountPrevious = FDetailGridRowsCountCurrent;
            FDetailGridRowsChangedState = 1;
        }

    }

    private void InvokeFocusedRowChanged(int AGridRowNumber)
    {
        SourceGrid.RowEventArgs rowArgs  = new SourceGrid.RowEventArgs(AGridRowNumber);
        FocusedRowChanged(grdDetails, rowArgs);
    }

    private void FocusRowLeaving(object sender, SourceGrid.RowCancelEventArgs e)
    {
        //Ignore this event if currently sorting
        if (grdDetails.Sorting)
        {
            FNewFocusEvent = false;
            return;
        }

        if (FNewFocusEvent == false)
        {
            FNewFocusEvent = true;
        }

        FocusPreparation(true);

        if (!FRepeatLeaveEventDetected)
        {
            FRepeatLeaveEventDetected = true;

            if (FDetailGridRowsChangedState == -1 || FDetailGridRowsCountCurrent == 2)  //do not run validation if cancelling current row
                                                                    // OR only 1 row present so no rowleaving event possible
            {
                e.Cancel = true;
            }

            Console.WriteLine("FocusRowLeaving");

            if (!ValidateAllData(true, true))
            {
                e.Cancel = true;
            }
        }
        else
        {
            // Reset flag
            FRepeatLeaveEventDetected = false;
            e.Cancel = true;
        }
    }

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.

    private void FocusedRowChanged(System.Object sender, SourceGrid.RowEventArgs e)
    {
        FNewRecordUnsavedInFocus = false;

        FRepeatLeaveEventDetected = false;

        if (!grdDetails.Sorting)
        {
            //Sometimes, FocusedRowChanged get called without FocusRowLeaving
            //  so need to handle that
            if (!FNewFocusEvent)
            {
                //This implies start of a new event chain without a previous FocusRowLeaving
                FocusPreparation(false);
            }

            //Only allow, row change, add or delete, not repeat events from grid changing focus
            if(e.Row != FCurrentRow && FDetailGridRowsChangedState == 0)
            {
                // Transfer data from Controls into the DataTable
                if (FPreviouslySelectedDetailRow != null)
                {
                    GetDetailsFromControls(FPreviouslySelectedDetailRow);
                }

                // Display the details of the currently selected Row
                FPreviouslySelectedDetailRow = GetSelectedDetailRow();
                ShowDetails(FPreviouslySelectedDetailRow);
                pnlDetails.Enabled = true;
            }
            else if (FDetailGridRowsChangedState == 1) //Addition
            {

            }
            else if (FDetailGridRowsChangedState == -1) //Deletion
            {
                if (FDetailGridRowsCountCurrent > 1) //Implies at least one record still left
                {
                    int nextRowToSelect = e.Row;
                    //If last row deleted, subtract row index to select by 1
                    if (nextRowToSelect == FDetailGridRowsCountCurrent)
                    {
                        nextRowToSelect--;
                    }
                    // Select and display the details of the currently selected Row without causing an event
                    grdDetails.SelectRowInGrid(nextRowToSelect, TSgrdDataGrid.TInvokeGridFocusEventEnum.NoFocusEvent);
                    FPreviouslySelectedDetailRow = GetSelectedDetailRow();
                    ShowDetails(FPreviouslySelectedDetailRow);
                    pnlDetails.Enabled = true;
                }
                else
                {
                    e.Row = 0;
                    FPreviouslySelectedDetailRow = null;
                    pnlDetails.Enabled = false;
                }
            }
        }

        FCurrentRow = e.Row;

        //Event chain tidy-up
        FDetailGridRowsChangedState = 0;
        FNewFocusEvent = false;
    }

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.

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:

    private bool FNewRecordUnsavedInFocus = false;
	
	/// automatically generated, create a new record of {#DETAILTABLE} and display on the edit screen
    /// we create the table locally, no dataset
    public bool CreateNew{#DETAILTABLE}()
    {
        if(ValidateAllData(true, true))
        {    
            {#DETAILTABLE}Row NewRow = FMainDS.{#DETAILTABLE}.NewRowTyped();
            {#INITNEWROWMANUAL}
            FMainDS.{#DETAILTABLE}.Rows.Add(NewRow);
            
            FPetraUtilsObject.SetChangedFlag();

			grdDetails.DataSource = null;
            grdDetails.DataSource = new DevAge.ComponentModel.BoundDataView(FMainDS.{#DETAILTABLE}.DefaultView);
            
			SelectDetailRowByDataTableIndex(FMainDS.{#DETAILTABLE}.Rows.Count - 1);
            InvokeFocusedRowChanged(grdDetails.SelectedRowIndex());

            //Must be set after the FocusRowChanged event is called as it sets this flag to false
            FNewRecordUnsavedInFocus = true;

            FPreviouslySelectedDetailRow = GetSelectedDetailRow();
            ShowDetails(FPreviouslySelectedDetailRow);
			
            Control[] pnl = this.Controls.Find("pnlDetails", true);
            if (pnl.Length > 0)
            {
	            //Look for Key & Description fields
	            bool keyFieldFound = false;
	            foreach (Control detailsCtrl in pnl[0].Controls)
	            {
	                if (!keyFieldFound && (detailsCtrl is TextBox || detailsCtrl is ComboBox))
	                {
	                    keyFieldFound = true;
	                    detailsCtrl.Focus();
	                }
	
	                if (detailsCtrl is TextBox && detailsCtrl.Name.Contains("Descr") && detailsCtrl.Text == string.Empty)
	                {
	                    detailsCtrl.Text = "PLEASE ENTER DESCRIPTION";
	                    break;
	                }
	            }

		    GetDetailsFromControls(FPreviouslySelectedDetailRow, true);
            }
			
            return true;
        }
        else
        {
            return false;
        }
    }

You will notice that the grid's DataSource needed to be set to Null before being reset. This ensured removal of focus from a cell on the previous row. The code also invokes the FocusedRowChanged event as well as sending focus to the new row irrespective of sorting and then gives focus to the key value field and populates the description field if it exists.

Also in another template method: SelectDetailRowByDataTableIndex(), the last line is changed that calls a new signature method in the wrapper that selects the specified row but without firing any events. The enumerator allows you to specify the event you would like to fire.

grdDetails.SelectRowInGrid(RowNumberGrid, TSgrdDataGrid.TInvokeGridFocusEventEnum.NoFocusEvent);

To sync the grid with the details controls for the added row, a call to GetDetailsFromControls() is made with the new optional second argument set to true: GetDetailsFromControls(FPreviouslySelectedDetailRow, true);

Here is an example of the expanded GetDetailsFromControls() method in the templates:

private void GetDetailsFromControls({#DETAILTABLETYPE}Row ARow, bool AIsNewRow = false)
{
    if (ARow != null && !grdDetails.Sorting)
    {
        if (AIsNewRow)
        {
            {#SAVEDETAILS}
        }
        else
        {
            ARow.BeginEdit();
            {#SAVEDETAILS}
            ARow.EndEdit();
        }
    }
}

Deleting Rows

The code for deleting rows is now in the templates:

   private void Delete{#DETAILTABLE}()
    {
		bool allowDeletion = true;
		bool deletionPerformed = false;
		string deletionQuestion = Catalog.GetString("Are you sure you want to delete the current row?");
		string completionMessage = string.Empty;
		
		if (FPreviouslySelectedDetailRow == null)
		{
			return;
		}

		int rowIndexToDelete = grdDetails.SelectedRowIndex();
		{#DETAILTABLETYPE}Row rowToDelete = GetSelectedDetailRow();
		
		{#PREDELETEMANUAL}
		
		if(allowDeletion)
		{
        	if ((MessageBox.Show(deletionQuestion,
					 Catalog.GetString("Confirm Delete"),
                     MessageBoxButtons.YesNo,
                     MessageBoxIcon.Question) == System.Windows.Forms.DialogResult.Yes))
			{
{#IFDEF DELETEROWMANUAL}
			    {#DELETEROWMANUAL}
{#ENDIF DELETEROWMANUAL}
{#IFNDEF DELETEROWMANUAL}				
			    rowToDelete.Delete();
			    deletionPerformed = true;
{#ENDIFN DELETEROWMANUAL}				
			
			    FPetraUtilsObject.SetChangedFlag();
			    //Select and call the event that doesn't occur automatically
			    InvokeFocusedRowChanged(rowIndexToDelete);
			}
		}

{#IFDEF POSTDELETEMANUAL}
		{#POSTDELETEMANUAL}
{#ENDIF POSTDELETEMANUAL}
{#IFNDEF POSTDELETEMANUAL}
		if(deletionPerformed && completionMessage.Length > 0)
		{
			MessageBox.Show(completionMessage,
					 Catalog.GetString("Deletion Completed"));
		}
{#ENDIFN POSTDELETEMANUAL}

    }

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.

Saving Rows

The main issue with saving was ensuring that the sorting was correct after changing the key field. To do this the following code was added to the save event in the template:

//The sorting will be affected when a new row is saved, so need to reselect row
if (FNewRecordUnsavedInFocus)
{
	SelectDetailRowByDataTableIndex(FMainDS.{#DETAILTABLE}.Rows.Count - 1);
	InvokeFocusedRowChanged(grdDetails.SelectedRowIndex());
}

Even though the FocusRowChanged event should not be needed, it is used to display the record (which may have moved due to sorting) and keep the FCurrent variable up to date.

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.