Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Make a Master/Detail DGV using CSLA DynamicRootList - Part III

0.00/5 (No votes)
13 Mar 2009 1  
Handling the DataGridView: sorting, auto save for detail DGV, etc.

Synopsis

This project shows how to have a master/detail DataGridView using CSLA DynamicRootList (or EditableRootListBase) as the master list object. If you use DynamicRootList for the master list, auto save is a standard feature. This project also shows how to implement auto save on the detail list. As a bonus, you get both lists sorted.

In part I, we explained the problem, discussed some background, analyzed the use cases and overviewed the database and BO design. In part II, we saw the details of CslaGen code generation, the secrets of binding parents to children and also the changes that need to be done on the generated code.

9. Handling the DataGridView

Let's make it clear that without the help of DataGridView FAQ I don't think I could have finished this project. This FAQ was pointed out by Mark Rideout who was the DataGridView Program Manager. On his MSDN blog you can find a lot of useful information and samples about the DGV.

9.1. Sticky Mode: Stick or Top?

Fig. 13 - Stick elements

Fig. 13 - Stick elements

In the UI, there is a combo box where you choose the sticky mode: Stick or Top. Top means that whenever you change to another brand, the current model row will be reset to the left most column of the top row. Stick means that whenever you change to another brand, the current model row and column will remain the same, provided the new brand has enough models.

The Stick property is handled only in masterDGV_RowEnter event handler. Before loading the new brand, in Stick mode the detail's DGV current column number is stored. After displaying the models of the new brand, CurrentCell is set to the stored column. In Top mode, CurrentCell is set to the first visible column of the first row. Note that DGV internal code takes care of keeping the current row value or decreasing it, if it's larger than total number of rows.

Sticky mode support should be expanded to detailDGV_RowsRemoved as right now it always resets the column to the first visible column. By the time this event handler is executed, the current cell is null. So it's impossible to copy the current column value to the new selected cell.

9.2. Plain Master/Detail, Sorting and Private Fields

The plain master/detail DataGridView doesn't need a lot of code. You specify a DataSource for the master that takes care of fetching the data. For the detail DataSource, you specify the master binding source and specify the appropriate DataMember.

private void MasterDetail_Load(object sender, EventArgs e)
{
    // Bind the DataGridView controls to the BindingSource
    masterDGV.DataSource = masterBindingSource;
    detailDGV.DataSource = detailBindingSource;

    // Get the data for the master DataSource from BrandColl
    masterBindingSource.DataSource = BrandColl.GetBrandColl();

    // Bind the detail DataSource to the master DataSource
    // using the DataMember "ModelColl"
    detailBindingSource.DataSource = masterBindingSource;
    detailBindingSource.DataMember = "ModelColl";

    // Hide some columns on masterDGV
    masterDGV.Columns[0].Visible = false;
    masterDGV.Columns[2].Visible = false; // RowVersion must be hidden

    // Hide some columns on detailDGV
    detailDGV.Columns[0].Visible = false;
    detailDGV.Columns[3].Visible = false; // RowVersion must be hidden
}

If you just need to sort the master DGV, all you need to do is replace this line:

masterBindingSource.DataSource = BrandColl.GetBrandColl();

with these:

var sortedList = new SortedBindingList<Brand>(BrandColl.GetBrandColl());
sortedList.ApplySort("BrandName", ListSortDirection.Ascending);
masterBindingSource.DataSource = sortedList;

The full code is included in CslaERLB1.zip for you to have a look.

The real problem starts when you also want to sort the detail DGV. I couldn't find a way to do it using data binding and had to do it manually. Of course, there are a lot of other small problems... I guess the DataGridView was really optimized for use with data tables, not business objects.

Fig. 14 - Private fields

Fig. 14 - Private fields

The private fields above are part of the solution: _thisMaster is of BrandColl type while _currentMasterItem is of Brand type. One of the biggest problems I found was to keep _currentMasterItem in sync with the current row while we move the cursor around in the master DataGridView.

9.3. Binding Parts

Fig. 15 - Binding parts

Fig. 15 - Binding parts

Everything that handles binding sources is on DisplaySortedMaster and DisplaySortedDetail. These methods are quite similar:

  • Switch off as much event handling as they can
  • Unbind the binding source
  • Get the data and sort it
  • Assign the sorted data to the binding source and reset the bindings
  • Switch on the normal event handling

The method UnbindBindingSource is an important helper you might know from ProjectTracker. CreateMasterItem is a method that is used to create a new master object (with an empty detail list) and show an empty detailDGV.

9.4. Master DGV Event Handling

Fig. 16 - Master DGV handlers

Fig. 16 - Master DGV handlers

9.4.1. Fail to Commit Data to the Data Source: masterDGV_DataError

According to VS 2008 documentation, the DataError event "Occurs when an external data-parsing or validation operation throws an exception, or when an attempt to commit data to a data source fails." Remark this is a rather broad scope: data parsing, validation or commit. When one of these conditions occurs, you get a not so nice MessageBox advising you to handle the event; you don't need to catch any exception, just to create an event handler. When the user edits the brand name and leaves the cell empty, if you don't handle this event, you will get the referred MessageBox saying there was a System.NullReferenceException. After you click OK the old value is restored. If you handle the event, the end result is the same but the user doesn't see the error MessageBox.

9.4.2. If Data is Valid, You Can Leave the Row: masterDGV_RowValidating

Whenever you try to leave the row, the DGV tries to validate the row and the RowValidating event occurs. The purpose of the event handler is to make sure you don't enter invalid data: no empty brand names and no duplicates. If it's a new row, no checks are made. If you are leaving an unsaved empty row, what happened is that your cell cursor was on the bottom insert line and you didn't insert a new brand. The DGV internal code will take care of it, but you really don't want to do anything with this row. If the current row already existed and has uncommitted changes the event handler will:

  • Check if there is an underlying brand object
  • Trim the brand name
  • Check that the underlying brand object is valid
if (masterDGV.IsCurrentRowDirty)
{
    if (masterDGV.Rows[e.RowIndex].DataBoundItem != null)
    {
        masterDGV[1, e.RowIndex].Value = 
		masterDGV[1, e.RowIndex].Value.ToString().Trim();
        var master = (Brand) masterDGV.Rows[e.RowIndex].DataBoundItem;
        if (!master.IsValid)
        {
            // it's invalid; wait for correction
            e.Cancel = true;

            // disable buttons on master navigator
            masterNavDelete.Enabled = false;
            masterNavMoveFirst.Enabled = false;
            masterNavMovePrevious.Enabled = false;
            masterNavMoveNext.Enabled = false;
            masterNavMoveLast.Enabled = false;
        }
    }
}

The e.Cancel = true; is the part that is responsible for blocking the cursor in the current row until the user makes the object valid or cancels the changes by pressing Escape. Disabling all the buttons on the master navigator is a visual option that intends to make clear to the user that he must correct the problem before doing anything else. By the way, it's useless to disable the buttons on the detail navigator as they will show up anyway.

9.4.3. Different Brand Needs an Updated Model List: masterDGV_RowEnter

The RowEnter event occurs when you change the current row, i.e. when you enter a different row. The main function of the event handler is to update the detail DGV with the corresponding sorted detail list. If it is a new row, you are in the insert row and the handler will:

  • Create a new brand object with an empty model collection and display a completely empty detail DGV
  • Set some buttons (you can't delete a brand that doesn't exist yet, neither you can create models for a non existing brand)
if (masterDGV.Rows[e.RowIndex].IsNewRow)
{
    // this is the insert row (not saved)

    // make a new master object and display a blank (empty) detail collection
    CreateMasterItem();

    // disable delete button
    masterNavDelete.Enabled = false;

    // prevent users from adding detail rows
    detailDGV.AllowUserToAddRows = false;
}

If it is an old existing row:

  • Check if there is an underlying brand object
  • Display the sorted detail list for the current brand
  • If there are any detail rows (models), handle current row and column position according to the Stick state (refer to 9.1. Sticky mode: Stick or Top?)
  • Set some buttons (since the brand exists, you can create models for it)
else
{
    // not a new master row
    if (masterDGV.Rows[e.RowIndex].DataBoundItem != null)
    {
        // get the underlying master object and display the detail collection
        _currentMasterItem = (Brand) masterDGV.Rows[e.RowIndex].DataBoundItem;
        DisplaySortedDetail();

        if (detailDGV.Rows.Count > 0)
        {
            // detail collection isn't empty
            if (Stick)
            {
                if (detailDGV.CurrentRow != null)
                {
                    // set current column to the stored column
                    detailDGV.CurrentCell =
                        detailDGV.Rows[detailDGV.CurrentRow.Index].Cells
						[currentDetailColumn];
                }
            }
        }
        detailDGV.AllowUserToAddRows = true;
    }
}

9.4.4. Current Cell is Gone - Select Another: masterDGV_RowsRemoved

The RowsRemoved event occurs when you delete a row. The event handler just tries to keep the cell cursor on the first visible column of the most obvious row. It's not possible to keep the cursor on the same column it was before as by the time this event handler is executed, the current cell is null. If there are no rows at all:

  • Create a new brand object with an empty model collection and display a completely empty detail DGV
  • On the master DGV, position the cell cursor on the top left cell and select that cell
  • Set some buttons (no brands to delete; note you need to force an update of the button status)
if (masterBindingSource.Count == 0)
{
    // master collection is empty

    // make a new master object and display a blank (empty) detail collection
    CreateMasterItem();

    // move cursor to top left cell
    masterDGV.CurrentCell = masterDGV.Rows[0].Cells[1];
    masterDGV.CurrentCell.Selected = true;

    // disable delete button
    masterNavDelete.Enabled = false;

    // force the update of the button status
    masterNav.Validate();
}

There are some rows in the master DGV. If the bottom row was deleted, position the cell cursor just above the insert row, on the bottom left cell and select that cell.

else if (e.RowIndex == masterDGV.RowCount - 1)
{
    // we are on the last data row (not the insert row)

    // previous last row deleted ; select the new last row
    masterDGV.CurrentCell = masterDGV.Rows[e.RowIndex - 1].Cells[1];
    masterDGV.CurrentCell.Selected = true;
}

9.5. Detail DGV Event Handling

Fig. 17 - Detail DGV handlers

Fig. 17 - Detail DGV handlers

As part of the world fight against boredom, we will avoid redundant code, showing only the relevant code that wasn't present on the similar event handler for the master DGV.

9.5.1. The List of Models Might Need to be Updated: detailDGV_UpdateModelListHelper

As referred to in the Synopsis, auto save is standard but only for master objects. Auto save means rows are saved as soon as you move to another row. For detail objects your code must implement this behavior using event handlers. This must be done when you leave a row and when you remove a row. What does this helper method do?

  • Checks if the master object needs to be saved
  • Saves the master object
  • Reloads the detail list
if (_currentMasterItem.IsSavable)
{
    _thisMaster.SaveItem(_currentMasterItem);
    masterDGV_RowEnter(sender,
        new DataGridViewCellEventArgs(masterDGV.CurrentCell.ColumnIndex,
        masterDGV.CurrentRow.Index));
}

The operation of reloading the detail list uses masterDGV_RowEnter the event handler that gets called when you move to a different master row. This event handler checks a lot of conditions and re-using it avoids writing repeated code.

9.5.2. Fail to Commit Data to the Data Source: detailDGV_DataError

The function of this event was explained previously. For the detail rows, you need it also because Price is of type Decimal and there might be conversion problems. Instead of getting a noisy MessageBox the user is stuck in the price cell until his input is a valid decimal value. There are other possible solutions like masked input but they are completely outside the scope of this project. Of course I'd prefer to show the user an error icon so he understands why he can't leave the cell. But that wasn't an option.

9.5.3. If Data is Valid You Can Leave, But Save the Row: detailDGV_RowValidating

This event handler resembles a lot the similar event handler for master objects. The only difference is when the object is valid: in that case detailDGV_UpdateModelListHelper is called so the row gets committed to the database at once.

9.5.4. You Can't Remove a Model That Doesn't Exist Yet: detailDGV_RowEnter

The event handler just disables the remove row button when you are on the insert row.

9.5.5. Current Cell is Gone - Save Changes and Select Another: detailDGV_RowsRemoved

This event handler also resembles a lot the similar event handler for master objects. There are two differences though:

  • First of all the row gets committed to the database at once by calling detailDGV_UpdateModelListHelper
  • As this is the detail collection, when it's empty of course there is no creation of an empty detail collection

The code to position the cell on the left most column just above insert row and select the current cell is there and does the same thing. The only difference is that you have two visible columns while in the brand DGV you only have one visible column.

Other Parts Of This Article

History

  • Document version 1: 12 March 2009
  • Document version 2: 14 March 2009 - bug correction
  • Document version 3: 15 March 2009 - masterDGV_DataError explained

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here