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
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)
{
masterDGV.DataSource = masterBindingSource;
detailDGV.DataSource = detailBindingSource;
masterBindingSource.DataSource = BrandColl.GetBrandColl();
detailBindingSource.DataSource = masterBindingSource;
detailBindingSource.DataMember = "ModelColl";
masterDGV.Columns[0].Visible = false;
masterDGV.Columns[2].Visible = false;
detailDGV.Columns[0].Visible = false;
detailDGV.Columns[3].Visible = false;
}
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
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
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
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)
{
e.Cancel = true;
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)
{
CreateMasterItem();
masterNavDelete.Enabled = false;
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
{
if (masterDGV.Rows[e.RowIndex].DataBoundItem != null)
{
_currentMasterItem = (Brand) masterDGV.Rows[e.RowIndex].DataBoundItem;
DisplaySortedDetail();
if (detailDGV.Rows.Count > 0)
{
if (Stick)
{
if (detailDGV.CurrentRow != null)
{
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)
{
CreateMasterItem();
masterDGV.CurrentCell = masterDGV.Rows[0].Cells[1];
masterDGV.CurrentCell.Selected = true;
masterNavDelete.Enabled = false;
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)
{
masterDGV.CurrentCell = masterDGV.Rows[e.RowIndex - 1].Cells[1];
masterDGV.CurrentCell.Selected = true;
}
9.5. Detail DGV Event Handling
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