Figure 1. User Entry Form with DataGrid Control � Records Expanded
Introduction
The purpose of this article is to
provide an example of using a DataGrid
control in a Windows Form programmed in
Managed C++ (MC++). I wanted the DataSource
for the DataGrid
to be 2 nested
ArrayLists
of objects. The accompanying sample Project was my first attempt at
using a DataGrid
control. Most of the articles and examples that I found online
were written in C# or VB .Net, so I felt compelled to help out other C++
programmers with some of the lessons I learned while familiarizing myself with
the DataGrid
control.
The user interface had to permit
adding new Positions
to the parent ArrayList
and related ScaleFactors
using
other data entry controls. It also had to allow deletion of a selected child
ScaleFactor
or deletion of a selected parent Position from the
DataGrid
control. Finally, the UI had to permit the removal of all ScaleFactors
in a
given Position or the removal of all Positions.
The DataGrid
control is probably the
most powerful class derived from the System.Windows.Forms.Control
class. A
DataGrid
object in a WinForm is normally bound to a data source of related data
tables. Only one table is displayed in a DataGrid
at any time. If a
parent-child relationship is established between data tables and if navigation
is enabled for the DataGrid
, the user can navigate between the related tables by
clicking an expander in the row header of a record that generates a Web-like
link to the child table. See Figure 1. When the link is clicked, the records in the child
table related to the single parent record are displayed. See Figure 2. While the
DataGrid
is
displaying records from the child table, a back button is available in the
DataGrid�s header allowing the user to navigate back to the parent table.
Figure 2. DataGrid Control with 2 ScaleFactor Objects Entered
How It Works
The WinForm shown in Figure 1 shows
the final form for the sample Project after some data has been entered by the
user. All of the TextBoxes are set with initial values at Design Time to
provide examples of the type of data that are expected. The ScaleFactor
Group
becomes enabled when there is at least one parent Position object in the
ArrayList
that is the DataSource
for the DataGrid
. All of the initial values
are cleared after each new record is added. ScaleFactors
are added to a given
Position by selecting a Position record and then entering data in the TextBoxes
of the ScaleFactor
GroupBox. Whichever record that the record indicator is in
is the current record that will receive the ScaleFactors
entered.
To remove a selected Position
object, you click on the RowHeader
(the left-most gray column) of a chosen
record and then click the Remove Selected button. When you remove a Position
object, you also remove all of the related child ScaleFactor
objects. To remove
a selected ScaleFactor
object, you first click on the expander (the + button in
the RowHeader
) of a chosen Position record and then click on the Web-like link
to display the list of related child ScaleFactors
. Then, you select a
ScaleFactor
RowHeader
and click the Remove Selected in the
ScaleFactor
GroupBox.
To clear all of the ScaleFactors
related to a given Position object, you select a Position row and click the
ScaleFactor
GroupBox Clear All button. To clear all Position records, you click
the main form�s Clear All button, which wipes the entire DataGrid
clean and even
removes the data binding to the underlying ArrayList
of Positions.
I refresh the DataGrid
in almost
every method of the Project unless it isn�t necessary, such as when clearing all
of the Positions. This may seem excessive, but it ensures that the presentation
of the DataGrid
is kept in a consistent state after each manipulation.
The user can edit any data member of
a Position or ScaleFactor
by clicking in a cell and editing the value and then
clicking on another record or typing CTRL + Enter. In fact, there is a whole
list of keyboard shortcuts in the Visual Studio .Net help, appropriately named
Keyboard Shortcuts for the Windows Forms DataGrid
Control.
To bind a DataGrid
control to an array of
objects, the object class must contain public properties. The parent Position
ArrayList
contains a data
member that is itself an ArrayList
of ScaleFactor
objects as shown in the
following code snippet. This establishes
the parent-child relationship between the Data Members of the DataGrid�s DataSource
.
public: __gc class Position {
private:
float x;
float y;
float z;
myArrayList * scaleFactors;
public:
Position( void ) {
scaleFactors = new myArrayList;
};
__property float get_X( void ) { return x; };
__property void set_X( float input ) { x = input; };
__property float get_Y( void ) { return y; };
__property void set_Y( float input ) { y = input; };
__property float get_Z( void ) { return z; };
__property void set_Z( float input ) { z = input; };
__property myArrayList * get_ScaleFactors( void ) { return scaleFactors; };
__property void set_ScaleFactors( myArrayList * input )
{ scaleFactors = input; };
};
Binding to the ArrayLists
Parent and child objects are removed through the DataGrid
using a CurrencyManager
object, which updates the records displayed in the
DataGrid. At the same time, the objects are removed from the parent or a given
parent�s child ArrayList.
The DataGrid
serves to display the current state of the
nested ArrayLists, allows the user to modify the field values in any record, and
permits the deletion of a selected parent or child ArrayList
Item. The public
properties of the Position and ScaleFactor
classes are mapped to the columns of
data in the DataGrid
.
The System.Collections.ArrayList
Class implements the IList
interface using an array whose size is dynamically increased as required. Several Classes in the
Visual Studio .Net help discuss the
use of an ArrayList
as the bound data source of a DataGrid
and, in most cases,
the Help warns that ". . . the ArrayList
must have items in it when it is bound.
An empty ArrayList
will result in an empty grid." The DataSource
and
DataMember
of the DataGrid
are set using the SetDataBinding
method at run-time, as
Microsoft recommends and as shown in the next code snippet. The binding takes place
with every Position added to the
parent ArrayList
, called positionList
. This way of binding is necessary because
there is no DataAdapter
associated with an ArrayList
and the DataGrid�s
DataSource
must be updated with each new ArrayList
Item added to the data
source.
Position * newParentObj = new Position;
newParentObj->X = Single::Parse( tboX->Text );
newParentObj->Y = Single::Parse( tboY->Text );
newParentObj->Z = Single::Parse( tboZ->Text );
int addIndex = positionList->Add( newParentObj );
dataGrid2->SetDataBinding( positionList, S"" );
Through the DataGridTableStyle
you can control the appearance of
the data displayed in the DataGrid
. All DataGridTableStyles
and DataGridColumnStyles
have to be created programmatically because no DataSource
exists at design time. Also,
when creating DataGridColumnStyles
and DataGridTableStyles
, the Visual Studio .Net help
cautions to:
"Always create DataGridColumnStyle
objects and add them to the
GridColumnStylesCollection
before adding DataGridTableStyle
objects to the
GridTableStylesCollection
. When you add an empty DataGridTableStyle
to the collection,
DataGridColumnStyle
objects are automatically generated for you. Consequently, an exception
will be thrown if you try to add [your] new DataGridColumnStyle
objects with duplicate
MappingName
values to the GridColumnStylesCollection
."
The Visual Studio .Net help also specifies that when binding a DataGrid
control to an
ArrayList
,
you should set the MappingName
of the DataGridTableStyle
to "ArrayList
" (the type name). The
MappingName
for each DataGridTableStyle
must also be unique. By creating a child class identical
to the ArrayList
with a different name, both the PositionObjectsTable
and the
ScaleFactorsTable
can
be mapped to ArrayLists
as shown in the next code snippet.
public: __gc class myArrayList : public ArrayList { };
.
.
.
PositionsStyle->MappingName = S"ArrayList";
.
.
.
ScaleFactorsStyle->MappingName = S"myArrayList";
The DataGrid
is refreshed in almost every method that adds, removes, or clears a record in the
DataSource
. The CurrencyManager
of the parent ArrayList
also invokes the Refresh method.
Removing a Record
Determining which row in the DataGrid
a user selected and then determining whether the selected
row was a record from the parent or child table were the most difficult logical elements of the
Project to formulate. The DataGrid
Class Overview in the Visual Studio .Net help provides the answer
to the first of these difficulties:
"To determine which part of the control the user clicked, use the HitTest
method in the
MouseDown
event [of the DataGrid
control]. The HitTest
method returns a DataGrid.HitTestInfo
object,
which contains the row and column of a clicked area."
To determine whether a parent or child record was selected, we must know which list in the
DataSource
that the DataGrid
control is displaying. The
DataMember
property gets this list for us.
A slight complication arises from the fact that when the DataSource
is an
ArrayList
, the top-most
DataMember
is an empty string. The next code snippet shows how I determine which table is being displayed
and then sets the Position property of the parent or child Currency Manager to the selected record,
which makes that record the current or active record.
if( parentCurrencyMgr != 0 ) {
if( dataGrid2->DataMember->ToString()->Equals(String::Empty) ) {
parentCurrencyMgr->Position = hti->Row;
}
if( dataGrid2->DataMember->ToString()->Equals(S"ScaleFactors") ) {
Position * currentParentObj =
dynamic_cast(parentCurrencyMgr->Current);
if( childCurrencyMgr != 0 ) childCurrencyMgr = 0;
childCurrencyMgr = dynamic_cast<CurrencyManager *>
(dataGrid2->BindingContext->get_Item( currentParentObj->
ScaleFactors, S"" ));
if( 0 <= hti->Row && hti->Row < childCurrencyMgr->Count )
childCurrencyMgr->Position = hti->Row;
Summary
This prototype MC++ Project was my first attempt at using the DataGrid
control, perhaps the most
powerful control in the Control class. Having experienced the frustrations and rewards of working with
this control, I want to move on to using typed DataSets as the DataGrid�s DataSource
. Questions or polite
comments can be e-mailed to me.