Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++/CLI

Multiple DataGridViews on a Form

4.67/5 (5 votes)
11 Sep 2011CPOL14 min read 51.7K   2.6K  
This is a working example of three DataGridViews on a windows form where the values on the second and third grid change during execution written using C++/CLI.

User John Role Suser

Figure 1. User JOHN; Role SUSER;

User Tom Role Auser

Figure 2. User TOM; Role AUSER;

User Tom Role Reports

Figure 3. User TOM; Role REPORTS;

Objective

The purpose of this article is to provide a working example in C++/CLI of a form with multiple DataGridViews where the contents of the second and subsequent grids are influenced by a value selected in the previous grid.

Background

db_developer here on CodeProject had one of the few examples online but I found it difficult to follow. This is my effort aimed at the C++/CLI audience.

Example Origin

The example is drawn from the user management area of a ticketing system I am working on.

The idea is that a username has a person associated with it on the primary grid, then on the second grid this user can be given any number of roles, while the third grid shows the system features associated with each role.

The system has tables with all roles, all users, all features, features-for-roles and roles-for-users. In this example stubs will use hard coded sample data to simulate reading the data from these tables so that you are not required to have a particular database in place to follow the example.

The User Interface

The UI is designed using a new Windows Form Application project in Visual Studio. The controls on the form are the three DataGridViews called gridUser, gridUserRoles and gridFeatures pulled onto the form from the Toolbox using the IDE, five labels and the Exit Button. The gridUser DataGridView has four columns, two text fields, dgUsername and dgPassword, and the subsequent pair are both of type DataGridViewComboBoxColumn, dgNumber and dgPerson. The gridUserRoles DataGridView has only one column, a DataGridViewComboBoxColumn called dgRoles. The gridFeatures DataGridView also has only one column, a text field called dgFeatureName. Three of the five labels are significant - they are lblPersonID, lblPersonName and lblUsername. I will be using these to show the impact of changing the users. The example uses five minor assemblies to supply data - this is explained further throughout the text. All are compiled into the same solution and assembly references have to be added for them as seen here:

The Assembly References

The Assembly References

Significant Declarations

This is the list of specific declarations that I have added to form1.h to carry out the task. For the most part, these are specific to my implementation. The only ones I would consider as mandatory are the DataTables that will be bound to each grid.

C++
DataTable ^dtPeople;
DataSet ^dsPeople;
BindingSource ^bsPeople;
DataTable ^dtViewUserData;
List<CUserMaster^>^ UserList;

DataTable ^dtRoles;
DataSet ^dsRoles;
BindingSource ^bsRoles;
DataTable ^dtViewUserRoles;
List<CUserRole^>^UserRoleList;

DataTable ^dtRoleFeatures;
DataTable ^dtViewRoleFeatures;
List<CRoleFeatures^>^ RoleFeatureList;

So each of the three grids will be bound to a datatable dtView....

  • ^dtPeople is an internal table of person numbers and their names managed in the example by the DB_PersonManager module. The reason it and the two ..People variables that follow are present is that I insist that my users are selected from a list of persons already on the system. They are completely optional in the context of a multi grid solution.
  • ^dsPeople is the container for ^dtPeople.
  • ^bsPeople is the binding source that will feed into the Combo column on the grid and manage the list of persons
  • ^dtViewUserData is bound to gridUser. It is essential to my multi grid model.
  • ^UserList is populated by the DB_UserManager module. This determines what will be seen in gridUser. In this example, it supplies three hardcoded rows of data, but the module could just as easily be reading a relational database or XML.
  • ^dtRoles is an internal table of roles that may be assigned to a user. Your second and subsequent grids do not have to follow this approach.
  • ^dsRoles is the container for ^dsRoles.
  • ^bsRoles is here because the roles will be presented in a combo column.
  • ^dtViewUserRoles is bound to gridUserRoles. This is another declaration that is essential to my approach.
  • ^UserRoleList is populated by the DB_UserRoleMgr module. It is called used in the refresh process every time the user changes on gridUser to determine what will be seen in gridUserRoles. In this example, it is only supplying hardcodes, but as above it could be fetching from storage.
  • ^dtRoleFeatures is the internal table of features allowed for each role. This is part of my access control mechanism for systems I work on - note there is no containing dataset this time. The features on a role cannot be changed in this module so there is no combo. Again, this is a design choice rather than a restriction.
  • ^dtViewRoleFeatures is bound to gridFeatures. This is the third declaration that is essential to my approach.
  • ^RoleFeatureList is populated by the DB_RoleFeatureMgr module. It is called used in the refresh process every time the role changes on gridUserRoles to determine what will be seen in gridFeatures. As ever, hardcodes are supplied for illustrative purposes only. The real world is free to draw data from wherever is the policy of the day.

Putting the Functionality in Place

This section will take you through how the declarations above are applied to the UI as I have outlined it to deliver a form with multiple DataGridViews.

Initialization

The LaunchForm function carries out the critical initialization after it has built the form but before it loads any data.

First up, we have the code to allocate the memory for each of our People structures:

C++
dtPeople = gcnew DataTable("dtPeople");
dtPeople->Columns->Add("PersonID", Int32::typeid);
dtPeople->Columns->Add("PersonName", String::typeid);
bsPeople = gcnew BindingSource();
dsPeople = gcnew DataSet();
dsPeople->Tables->Add(dtPeople);

Next, we define and add the columns to the datatable that is bound to the primary datagridview:

C++
dtViewUserData = gcnew DataTable("ViewUserData");
dtViewUserData->Columns->Add("Username", String::typeid);
dtViewUserData->Columns->Add("Password", String::typeid);
dtViewUserData->Columns->Add("Number", Int32::typeid);
dtViewUserData->Columns->Add("PersonID", Int32::typeid);

Now that we are ready for our data, we call some functions to load people, roles and features.

Next we define the lists for the users and the user roles:

C++
UserList = gcnew List<CUserMaster^>();
UserRoleList = gcnew List<CUserRole^>();

Finally we call a function to load the Users, then complete the list setup. This will load the primary grid and set in motion a chain of events that will load the other two grids. The functions to load people, roles and features are not responsible for making the multi grid solution work - all they do is provide information that will be presented in the grids, so we will defer looking at them until after we have seen the grids being created.

Load_Users

Load_Users is the function that gets everything under way by going out into the world and fetching the data for the first DataGridView on the form. It has four significant parts, the first of which makes sure the user list is clear:

C++
UserList->Clear();

Then the data table and the grid are reset:

C++
dtViewUserData->Clear();
gridUser->DataSource = nullptr;
gridUser->Rows->Clear();

Happy that everything is properly initialized, we call the Fetch routine to populate our user list:

C++
UserList = CComs_UM::Fetch_User_Local(nullptr);

Finally the user list is mapped to the data table that is bound to the first grid:

C++
DataRow ^dr;

for each(CUserMaster^ candidate in UserList)
{
	dr = dtViewUserData->NewRow();
	dr["Username"]= candidate->p_Username;
	dr["Password"]= candidate->p_Password;
	dr["Number"] = candidate->p_Person_ID;
	dr["PersonID"]= candidate->p_Person_ID;
	dtViewUserData->Rows->Add(dr);
}

List_SetUp

Load_Users is only part of the process of getting started, List_SetUp completes the population of the first DataGridView with:

C++
gridUser->AutoGenerateColumns = false;

gridUser->DataSource = dtViewUserData;

gridUser->Columns[dgUsername->Index]->DataPropertyName = "Username";
gridUser->Columns[dgPassword->Index]->DataPropertyName = "Password";
gridUser->Columns[dgNumber->Index]->DataPropertyName = "Number";
gridUser->Columns[dgPerson->Index]->DataPropertyName = "PersonID";

m_LoadCompleted = true;
gridUser->AutoResizeColumns();

Now we have data in our primary data grid. The other two require the firing of the row entered event on this primary grid. That event is fired for every row while gridUser->DataSource is pulling data from dtViewUserData but we only require it for when the grid is fully loaded - everything else is just adding load to our machine, database and network, so that is where the m_LoadCompleted flag which you see set to true at the end of this function plays a key part.

Populating the Second and Subsequent Grid at Run Time

Process_RowEntered

Process_RowEntered is the function to handle the RowEnter event on the gridUser DataGridView. Its purpose is to perform specific tasks - in this case read the current row, as soon as a user enters it. I mostly use it to record the current row contents for comparison against their final state when I am done with the row so that I can keep the number of fields to be written to the database as low as possible. Essentially I want to avoid "Update TableA set X=1 where X=1". However in this multi grid scenario, it performs a key additional task, it decides when to re-load the second grid at run time. This check is carried out after checking the contents of the username cell on the current row, and where it has a value and load completed is true then we go and get the data for the second grid using Load_UserRoles and RoleList_Setup.

C++
if (gridUser->Rows[e->RowIndex]->Cells[dgUsername->Index]->Value != nullptr)
{
	m_OrigUsername = gridUser->Rows[e->RowIndex]->
		Cells[dgUsername->Index]->Value->ToString();
	if (m_LoadCompleted)
	{
		Load_UserRoles(m_OrigUsername);
		RoleList_Setup();
		m_Role_LoadCompleted = true;
	}
}

Note that we are setting a m_Role_LoadCompleted flag - this will form the signal to go ahead and load the third grid after we have got the data for the second one.

Also present in this function but not required for the multi grid solution is code to initialize the labels, populate them and make sure the Person combo always shows the correct value:

C++
if (e->RowIndex == gridUser->RowCount - 1)
 {
	 lblPersonID->Text= nullptr;
	 lblPersonName->Text	= nullptr;
	 lblUsername->Text	= nullptr;
	 return;
 }
if ((m_LoadCompleted && e->RowIndex < gridUser->RowCount)
	&& (e->RowIndex != gridUser->RowCount - 1))
{
	lblUsername->Text = gridUser->Rows[e->RowIndex]->
			Cells[dgUsername->Name]->Value->ToString();
	array<DataRow^>^ row;
	try
	{
		if (gridUser->Rows[e->RowIndex]->Cells
				[dgPerson->Index]->Value != nullptr
			&&gridUser->Rows[e->RowIndex]->Cells
				[dgPerson->Index]->Value->ToString() != "")
		{
			String^ IndPerson = gridUser->Rows[e->RowIndex]->
				Cells[dgPerson->Index]->Value->ToString();
			row =dtPeople->Select(String::Format
				("PersonID={0}", IndPerson));
			if (row->Length > 0)
			{
				lblPersonID->Text = row[0]->ItemArray[0]->ToString();
				lblPersonName->Text = 
					row[0]->ItemArray[1]->ToString();
			}
		}
	}
	catch (Exception ^e)
	{
			String ^MessageString = 
			    " Error reading internal people table: " + e->Message;
			MessageBox::Show(MessageString);
	}
}

Load_UserRoles

Load_UserRoles uses an identical approach to Load_Users in performing its data retrieval tasks. The only difference is in the data structure / variable names referenced. But significantly it takes the username as a parameter and uses this to restrict the fetched dataset.

RoleList_Setup

RoleList_Setup does for gridUserRoles what List_SetUp did for gridUser except that the completed flag is set outside it, but that makes no difference to the logic. As was the case with gridUser, this process fires the RowEnter event for gridUserRoles.

Process_Roles_RowEntered

Process_Roles_RowEntered is the function to handle the RowEnter event on the gridUserRoles DataGridView. Its purpose is also to perform specific tasks - e.g. read the current row, as soon as a user enters it. In this multi grid scenario, it performs the key additional task of deciding when to re-load the third grid at run time. This check is carried out after checking the contents of the role cell on the current row, and where it has a value and load completed is true then we go and get the data for the third grid using Load_RoleFeatures only.

Load_RoleFeatures

Load_RoleFeatures takes the Role ID as a parameter. It is a little different from the other two loads, for two reasons, firstly I chose to preload all features, and secondly it incorporates the functionality of the ..._Setup functions. Neither of these differences has any bearing on the multi grid functionality. This is what happens here.

The first action is to declare a DataRow array:

C++
array<DataRow^>^ FeatureRows;

Next, do the standard resets:

C++
dtViewRoleFeatures->Clear();
gridFeatures->DataSource = nullptr;
gridFeatures->Rows->Clear();

This time we are selecting the required features from an in-memory table:

C++
FeatureRows =dtRoleFeatures->Select(String::Format("Role_ID='{0}'", arg_Role_ID));

Then the selected features are mapped to the datatable that is bound to the third grid:

C++
DataRow ^dr;

for each(DataRow ^row in FeatureRows)
{
	dr = dtViewRoleFeatures->NewRow();
	dr["Feature_Name"]= row[2];
	dtViewRoleFeatures->Rows->Add(dr);
}

Finally, we carry out the steps that were part of a setup function on the other grids:

C++
dtViewRoleFeatures->AcceptChanges();
gridFeatures->AutoGenerateColumns = false;
gridFeatures->DataSource = dtViewRoleFeatures;

gridFeatures->Columns[dgFeatureName->Index]->DataPropertyName = "Feature_Name";

That is all that is required to have three or more linked DataGridViews on a form. Since the third grid is display only, I don't have a RowEntered event on it. All that remains now is to look at the other functions that contribute and handle data on these grids.

Other Interesting Functions in this Example

Load_People

Load_People gets ids and names of all persons on the system and puts them in a binding source datastructure that we will tie up to both the dgNumber and dgPerson columns on gridUser.

First, a list is defined to hold the persons:

C++
List<cpersonmaster^ />^ PersonList = gcnew List<cpersonmaster^ />();

Then that list is populated by a "Fetch" routine from DB_PersonManager which in this example supplies just three hardcodes, but could be fetching from any source:

C++
PersonList = CComs_PM::Fetch_PersonMaster();

Then the persons are mapped from the list to the datatable:

C++
DataRow ^row;
for each(CPersonMaster^ candidate in PersonList)
{
	row = dsPeople->Tables["dtPeople"]->NewRow();
	row["PersonID"] = candidate->p_PersonID;
	row["PersonName"] = candidate->p_Surname + ", " + candidate->p_Forename;
	dsPeople->Tables["dtPeople"]->Rows->Add(row);
}

A Binding source is set up where the dataset provides the DataSource and the datatable is the DataMember.

C++
bsPeople->DataSource = dsPeople;
bsPeople->DataMember = "dtPeople";

The data source for the person name column dgPerson on gridUser gets its value from the Binding source, while the DisplayMember is the person name, but the ValueMember is the person ID. This is important, because it is the Person ID that will be coming in from the User table, and we will be using it to query the person table for the name. Scroll back up and look at Process_RowEntered to see this happening.

C++
dgPerson->DataSource = bsPeople;
dgPerson->DisplayMember = "PersonName";
dgPerson->ValueMember = "PersonID";

The grid also shows person ID, which is done in the same manner as the name except that in this case the display and value members both point to the person ID.

C++
dgNumber->DataSource = bsPeople;
dgNumber->DisplayMember = "PersonID";
dgNumber->ValueMember = "PersonID";

Load_Roles

Load_Roles begins by creating its binding sources and datatables including the dtViewUserRoles that will be bound to gridUserRoles:

C++
dtRoles = gcnew DataTable("dtRoles");
dtRoles->Columns->Add("Role_ID", String::typeid);
bsRoles = gcnew BindingSource();
dsRoles = gcnew DataSet();
dsRoles->Tables->Add(dtRoles);

dtViewUserRoles = gcnew DataTable("ViewUserRoles");
dtViewUserRoles->Columns->Add("Role_ID", String::typeid);

From then on, it follows logic very similar to that seen in Load_People with the exception that only the role ID is bound to a grid column.

Load_Features

Load_Features is different from the other load functions, in part because it doesn't have to supply a combo column, but also because of how we handled features above. First we define a datatable for role features:

C++
dtRoleFeatures = gcnew DataTable("dtRoles");
dtRoleFeatures->Columns->Add("Role_ID", String::typeid);
dtRoleFeatures->Columns->Add("Feature_Code", String::typeid);
dtRoleFeatures->Columns->Add("Feature_Name", String::typeid);

Then a datatable that will be bound to gridFeatures.

C++
dtViewRoleFeatures = gcnew DataTable("ViewRoleFeatures");
dtViewRoleFeatures->Columns->Add("Feature_Name", String::typeid);

Followed by a standard list to hold the features:

C++
RoleFeatureList = gcnew List<crolefeatures^ />();

Then the features are fetched:

C++
RoleFeatureList = CComs_RF::Fetch_Features(nullptr);

and mapped to the role features datatable.

C++
DataRow ^dr;

for each(CRoleFeatures^ candidate in RoleFeatureList)
{
	dr = dtRoleFeatures->NewRow();
	dr["Role_ID"]= candidate->p_Role_ID;
	dr["Feature_Code"] = candidate->p_Feature_Code;
	dr["Feature_Name"] = candidate->p_Feature_Name;
	dtRoleFeatures->Rows->Add(dr);
}

Finally, the gridFeatures is bound to the datasource set aside for this purpose. You have already seen this table get populated in Load_RoleFeatures above. Recall that while we called this function (Load_Features) earlier, we deferred looking at it until we saw how the three grids hung together.

C++
gridFeatures->DataSource = dtViewRoleFeatures;

Process_CellValueChanged

Process_CellValueChanged is the function to handle the CellValueChanged event on the gridUser DataGridView. It does not trigger a new call to reload roles. If you are changing an existing entry, then this allows you to change the user details without changing the associated roles. For new users, fill in their details then go to the second grid and add the roles to be associated with that new user.

Process_Roles_CellValueChanged

Process_Roles_CellValueChanged is the function to handle the CellValueChanged event on the gridUserRoles DataGridView. This time when dgRoles has changed and is not null, I reload the features grid.

Process_CellClick

Process_CellClick holds the code to handle a CellClick event from the DataGridView. It is not essential, but without it the combo cells will have to be clicked twice to activate the dropdowns. This is the engine of the function:

C++
gridUser->BeginEdit(true);
(safe_cast<combobox^ />(gridPassenger->EditingControl))->DroppedDown = true;

Process_Roles_CellClick performs a similar function on gridUserRoles.

Process_CellEndEdit

Process_CellEndEdit is the function to handle the CellEndEdit event on the user DataGridView. It will put a little red icon in any cell that needs one if you leave the row without filling in the required cells. It provides this functionality in coordination with the RowValidating event.

The Error Icon

Process_Roles_CellEndEdit performs a similar function on the roles grid.

Process_RowValidating

Process_RowValidating handles the RowValidating event on the DataGridView. It uses an If statement to decide which columns are to be checked for errors. In this case, I am choosing to ban null on every column. It sets up padding for the Icon in the cell.

Process_Roles_RowValidating does the same task for the user roles grid.

Process_DataError

I introduced the DataError event to handle unforeseen display failures on the DataGridView when I was putting together the Enum example. You could live without it, but if you accidentally leave something out, then it may get called into action, and it provides useful information about what went wrong any time it does get invoked because it nicely catches unexpected display issues with the DataGridView. This function is called by the display error events of all three grids.

Walkthrough the Code

This section will give you a very brief but sequential overview over the way in which the example solution is laid out. The solution was created around DataGridViewMultiEx1.cpp and its associated Form1.h. These are compiled to form the EXE. The other modules are used to form the DB_PersonManager, DB_RoleFeatureMgr, DB_RoleManager, DB_UserManager and DB_UserRoleMgr assemblies respectively. All are included in the same solution to simplify matters.

Form1.h

Form1.h begins with namespaces. The following ones are added to the standard set:

C++
using namespace System::Collections::Generic;//Needed for list containers
using namespace DB_PersonManager;
using namespace DB_RoleFeatureMgr;
using namespace DB_RoleManager;
using namespace DB_UserManager;
using namespace DB_UserRoleMgr;

This is followed by the constructor which has a couple of set up lines for the error icon and a call to the LaunchForm function that gets everything up and going. This is succeeded by the code added by the IDE to build the form. Then there are the user defined functions and variables, for the data tables and lists which were introduced at the start of the article. Then there are my Process_... functions which are called from the corresponding grid and form events. Look an the properties on each control to see what events related to it.

DataGridViewMultiEx1.cpp

Contains all the function code, including those of the event handlers who default to the .h but are redirected here via user defined functions. e.g. Process_RowEntered is the workhorse for gridUser_RowEnter. Since we have already visited all the code, there is nothing new to add here except to point out that all the functions for each grid are grouped together.

DB_PersonManager.h

DB_PersonManager.h defines the Person class, the Get properties for it and a communications class to bring back the values. DB_RoleFeatureMgr.h, DB_RoleManager.h, DB_UserManager.h and DB_UserRoleMgr.h perform similar tasks for their tables.

DB_PassengerManager.cpp

DB_PassengerManager.cpp is stripped down to just a fetch function, with some values hard coded onto a list which is returned to the calling module. DB_RoleFeatureMgr.cpp, DB_RoleManager.cpp, DB_UserManager.cpp and DB_UserRoleMgr.cpp perform similar tasks for their tables.

History

  • 2011-09-08 - V1.0 - Initial submission

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)