Working with Master-Detail Relationships in Code
- 19 minutes to read
Data Binding
- Binding to a Regular Database at Runtime
- Binding to Objects with Collection Properties
- Load Details Dynamically by Handling Events
- Load Details Dynamically by Implementing the IRelationList/IRelationListEx Interfaces
Public API
- Acess Views
- Create Multi-Level Detail Relationships in Code
- Access Rows
- Expand and Collapse Rows
- Example 1: Process All Rows
- Example 2: Recursively Expand a Master Row
- Example 3: Expand All Master Rows
Binding to a Regular Database at Runtime
The code below illustrates how to bind a Data Grid to a sample MS Access “NorthWind” database with a master GridView and a detail CardView. You can download and review the complete sample project here.
Tip
This sample data source has a single detail level only. Go to the Create Multi-Level Detail Relationships in Code section for an example on binding to more complex sources that have multiple detail levels.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Data.OleDb;
using DevExpress.XtraGrid.Views.Card;
using DevExpress.XtraEditors.Repository;
namespace Grid_Runtime_MasterDetail_Mode {
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e) {
//Define a connection to the database
OleDbConnection connection = new OleDbConnection(
"Provider=Microsoft.Jet.OLEDB.4.0;Data Source = ..\\..\\Data\\nwind.mdb");
//Create data adapters for retrieving data from the tables
OleDbDataAdapter AdapterCategories = new OleDbDataAdapter(
"SELECT CategoryID, CategoryName, Picture FROM Categories", connection);
OleDbDataAdapter AdapterProducts = new OleDbDataAdapter(
"SELECT CategoryID, ProductID, ProductName, UnitPrice FROM Products", connection);
DataSet dataSet11 = new DataSet();
//Create DataTable objects for representing database's tables
AdapterCategories.Fill(dataSet11, "Categories");
AdapterProducts.Fill(dataSet11, "Products");
//Set up a master-detail relationship between the DataTables
DataColumn keyColumn = dataSet11.Tables["Categories"].Columns["CategoryID"];
DataColumn foreignKeyColumn = dataSet11.Tables["Products"].Columns["CategoryID"];
dataSet11.Relations.Add("CategoriesProducts", keyColumn, foreignKeyColumn);
//Bind the grid control to the data source
gridControl1.DataSource = dataSet11.Tables["Categories"];
gridControl1.ForceInitialize();
//Assign a CardView to the relationship
CardView cardView1 = new CardView(gridControl1);
gridControl1.LevelTree.Nodes.Add("CategoriesProducts", cardView1);
//Specify text to be displayed within detail tabs.
cardView1.ViewCaption = "Category Products";
//Hide the CategoryID column for the master View
gridView1.Columns["CategoryID"].VisibleIndex = -1;
//Present data in the Picture column as Images
RepositoryItemPictureEdit riPictureEdit = gridControl1.RepositoryItems.Add("PictureEdit") as RepositoryItemPictureEdit;
gridView1.Columns["Picture"].ColumnEdit = riPictureEdit;
//Stretch images within cells.
riPictureEdit.SizeMode = DevExpress.XtraEditors.Controls.PictureSizeMode.Stretch;
gridView1.Columns["Picture"].OptionsColumn.FixedWidth = true;
//Change Picture column's width
gridView1.Columns["Picture"].Width = 180;
//Change row height in the master View
gridView1.RowHeight = 50;
//Create columns for the detail pattern View
cardView1.PopulateColumns(dataSet11.Tables["Products"]);
//Hide the CategoryID column for the detail View
cardView1.Columns["CategoryID"].VisibleIndex = -1;
//Format UnitPrice column values as currency
cardView1.Columns["UnitPrice"].DisplayFormat.FormatType = DevExpress.Utils.FormatType.Numeric;
cardView1.Columns["UnitPrice"].DisplayFormat.FormatString = "c2";
}
}
}
Binding to Objects with Collection Properties
If the Data Grid is bound to a data source where records provide property (or properties) of the IList type (e.g., ArrayList or List<T>), these collection properties are treated as detail Views unless the GridOptionsDetail.EnableMasterViewMode is explicitly disabled.
Properties of the IList<T>, ICollection<T> and IEnumerable types are not recognized as collection properties by default. To treat them as collection properties, enable the BaseView.DataController.AllowIEnumerableDetails option prior to binding the Data Grid to a data source.
In this example, the Data Grid is bound to an ArrayList object “NestedRecords”. Data source entities are objects of the “NestedRecord” class, which provides two properties: a string “Name” and an ArrayList “ChildList”. Such a data source is automatically recognized by the Data Grid as a master-detail source.
gridControl1.DataSource = new NestedRecords();
//...
public class NestedRecords : ArrayList {
public NestedRecords() {
Add(new NestedRecord("Customers", new ChildRecordsCustomers()));
Add(new NestedRecord("Products", new ChildRecordsProducts()));
Add(new NestedRecord("Shippers", new ChildRecordsShippers()));
}
public virtual new NestedRecord this[int index] {
get { return (NestedRecord)(base[index]); }
}
}
//Products
public class ChildRecordsProducts : ArrayList
{
public ChildRecordsProducts()
{
for (int i = 0; i < 10; i++)
Add(new Product() { Price = i, Name = "Product" + i });
}
}
public class Product
{
public string Name { get; set; }
public double Price { get; set; }
}
//Customers
//. . .
//Shippers
//. . .
//Nested Records
public class NestedRecord {
private string fName;
ArrayList fChildList = null;
public NestedRecord(string name, ArrayList childList) {
this.fName = name;
this.fChildList = childList;
}
public string Name {
get { return fName; }
set { fName = value; }
}
public ArrayList ChildList {
get { return fChildList; }
set { fChildList = value; }
}
}
Load Details Dynamically by Handling Events
To load details dynamically, rather from a bound source, handle the following events.
Primary Events
GridView.MasterRowGetRelationCount
Fires repeatedly for every row and allows you to specify several details for this row. To identify a row for which the event is raised, read the event’s RowHandle parameter. Then, set the RelationCount parameter accordingly to the desired number of detail tables. Occasionally, the Data Grid fires this event with a RowHandle equal to the GridControl.InvalidRowHandle constant. This is a service event that allows you to display or hide all master row expand/collapse buttons. Set the RelationCount to any positive number to show these buttons.
gridView.MasterRowGetRelationCount += (s, e) => { // set to any positive value to display expand/collapse buttons for master rows if (e.RowHandle == DevExpress.XtraGrid.GridControl.InvalidRowHandle) e.RelationCount = 100; // even master rows have two details, odd master rows have none else if (e.RowHandle % 2 != 0) e.RelationCount = 2; };
-
Fires for each row that has been marked by the GridView.MasterRowGetRelationCount event as a row that has details. For each of these non-empty rows, the GridView.MasterRowEmpty event fires as many times as the MasterRowGetRelationCount event’s RelationCount parameter is. To retrieve the number of the currently processed detail, read the RelationIndex parameter, then set the IsEmpty parameter to a required boolean value. Empty details are not shown.
// all master rows have 5 detail gridView.MasterRowGetRelationCount += (s, e) => { e.RelationCount = 5; }; // details with even indexes are not empty, details with odd indexes are hidden gridView.MasterRowEmpty += (s, e) => { if (e.RelationIndex%2!=0) { e.IsEmpty = true; } else e.IsEmpty = false; };
GridView.MasterRowGetRelationName
This parameter allows you to set the RelationName parameter for each detail. This name is used to retrieve the required pattern View from the GridControl.LevelTree tree. At the same time, the RelationName specifies a default caption for the detail tab.
GridView.MasterRowGetChildList
When the required pattern View is obtained, the Data Grid creates its clone View to display detail data. Set the event’s ChildList parameter to provide data to these clones.
gridView.MasterRowGetChildList += (s, e) => { // details with even indexes receive data from one sample source if (e.RelationIndex%2 != 0) e.ChildList = SampleData1.GetData().ToList(); // details with odd indexes receive data from another one else e.ChildList = SampleData2.GetData().ToList(); };
Optional Events
GridView.MasterRowGetRelationDisplayCaption
Handle this event if you want detail tabs to have different captions than detail names, set on the GridView.MasterRowGetRelationName event.
GridView.MasterRowGetLevelDefaultView
This event allows you to explicitly provide a pattern View for a specific detail. Generally, this is required only when the View does not belong to the GridControl.LevelTree tree. Otherwise, pattern Views are provided indirectly on the GridView.MasterRowGetRelationName event.
-
Handle this event to prevent specific master rows from expanding.
Demo: Master-detail mode using events
Load Details Dynamically by Implementing the IRelationList/IRelationListEx Interfaces
As an alternative to handling View events, you can also implement the IRelationList/IRelationListEx interface for your runtime created data or a .NET data structure (e.g., a System.Data.DataView object). The IRelationList interface provides the following members.
-
Returns the number of master-detail relationships for master rows. If set to zero, the master-detail mode is disabled.
IRelationList.IsMasterRowEmpty
Specifies whether or not a specific detail has data.
-
Specifies a relation name, with which the required pattern View is retrieved from the GridControl.LevelTree tree. This same name is displayed by a detail tab at runtime.
-
Must return data for a specific clone View.
The IRelationListEx interface extends the IRelationList with two additional methods. These allow you to provide different numbers of master-detail relationships for master rows and customize captions for relationships.
IRelationListEx.GetRelationCount
Returns the number of relationships for this specific master row.
IRelationListEx.GetRelationDisplayName
Returns the relationship caption, displayed in a detail tab.
The code below demonstrates the IRelationList interface implemented for the NWTables class, which serves as a data source in the Master-Detail (unbound) demo.
public class NWTables : ArrayList, DevExpress.Data.IRelationList {
private DataSet dataSet;
public NWTables(DataSet ds) {
Add(new NWTable(Properties.Resources.SuppliersTable, "Suppliers"));
Add(new NWTable(Properties.Resources.CategoriesTable, "Categories"));
Add(new NWTable(Properties.Resources.EmployeesTable, "Employees"));
Add(new NWTable(Properties.Resources.ShippersTable, "Shippers"));
Add(new NWTable(Properties.Resources.CustomersTable, "Customers"));
Add(new NWTable(Properties.Resources.OrdersTable, "Orders500"));
dataSet = ds;
}
//IRelationList
string IRelationList.GetRelationName(int index, int relationIndex) {
if(index >= 0 && index < this.Count)
return this[index].RelationName;
return "";
}
int IRelationList.RelationCount {
get { return dataSet.Tables.Count > 0 ? 1 : 0; }
}
IList IRelationList.GetDetailList(int index, int relationIndex) {
if(dataSet.Tables.Count > 0)
return dataSet.Tables[((NWTable)this[index]).TableName()].DefaultView;
return null;
}
bool IRelationList.IsMasterRowEmpty(int index, int relationIndex) {
return false;
}
//IRelationList
public virtual new NWTable this[int index] {
get {return (NWTable)(base[index]);}
}
}
Demo: Master-Detail (unbound)
Access Views
Retrieves the top-level master View.
Returns a detail clone View for a specific master row. The example below illustrates how to retrieve the focused cell value for a clone View.
using DevExpress.XtraGrid.Views.Grid;
gridView1.MasterRowExpanded += GridView1_MasterRowExpanded;
gridView1.OptionsBehavior.Editable = gridView2.OptionsBehavior.Editable = false;
private void GridView1_MasterRowExpanded(object sender, DevExpress.XtraGrid.Views.Grid.CustomMasterRowEventArgs e) {
GridView master = sender as GridView;
GridView detail = master.GetDetailView(e.RowHandle, e.RelationIndex) as GridView;
detail.Click += new EventHandler(detail_Click);
}
void detail_Click(object sender, EventArgs e) {
GridView gridView = sender as GridView;
var value = gridView.GetRowCellValue(gridView.FocusedRowHandle, gridView.FocusedColumn);
MessageBox.Show("Cell value: " + value.ToString(), "Message");
}
For clone Views, this property returns a master View. For pattern and main Views, returns null (Nothing in Visual Basic).
Returns a currently visible detail clone for a specific master row.
Provides access to master and clone Views that are currently visible. Detail clones are dynamically added to or removed from this collection as details expand or collapse.
Provides access to a hierarchical structure that associates pattern Views with master-detail relationships. Hierarchy nodes are represented by the GridLevelNode class objects. The following sample illustrates how to replace a View for a master-detail “Orders” relationship with a new banded View.
using DevExpress.XtraGrid;
using DevExpress.XtraGrid.Views.Base;
using DevExpress.XtraGrid.Views.Grid;
using DevExpress.XtraGrid.Views.BandedGrid;
// Collapse all the details opened for the master rows in the main view.
(gridControl1.MainView as GridView).CollapseAllDetails();
// Get the node at the first nesting level that stores a view for the "Orders" relation.
GridLevelNode node = gridControl1.LevelTree.Nodes["Orders"];
if(node == null) return;
// The old view which represents the "Orders" relation.
BaseView oldView = node.LevelTemplate;
// Dispose of this view.
oldView.Dispose();
// Create a new view.
BandedGridView bandedView = new BandedGridView(gridControl1);
// Associate this view with the "Orders" relation.
node.LevelTemplate = bandedView;
// Customize the new view.
GridBand band = bandedView.Bands.Add("Orders");
BandedGridColumn column = (BandedGridColumn)bandedView.Columns.Add("ID");
column.OwnerBand = band;
column.Visible = true;
column = (BandedGridColumn)bandedView.Columns.AddField("ProductID");
column.OwnerBand = band;
column.Visible = true;
Create Multi-Level Detail Relationships in Code
To add multi-level details, you need to create a template View for each level. Also, create a GridLevelNode object for each existing child View. The topmost detail View’s node goes into the GridControl.LevelTree collection, further nodes must be added into the GridLevelNode.Nodes collections of their parent nodes.
GridControl gridControl = new GridControl();
GridView view1 = new DevExpress.XtraGrid.Views.Grid.GridView();
GridView view2 = new DevExpress.XtraGrid.Views.Grid.GridView();
GridView view3 = new DevExpress.XtraGrid.Views.Grid.GridView();
//Root master-detail relation
DevExpress.XtraGrid.GridLevelNode myGridLevelNode1 = new DevExpress.XtraGrid.GridLevelNode();
myGridLevelNode1.LevelTemplate = view2;
myGridLevelNode1.RelationName = "Relation1";
//Secondary master-detail relation
DevExpress.XtraGrid.GridLevelNode myGridLevelNode2 = new DevExpress.XtraGrid.GridLevelNode();
myGridLevelNode2.LevelTemplate = view3;
myGridLevelNode2.RelationName = "Relation2";
//add Node2 to Node1's collection of child nodes
myGridLevelNode1.Nodes.AddRange(new DevExpress.XtraGrid.GridLevelNode[] {
myGridLevelNode2});
//add Node1 to the grid's node collection
gridControl.LevelTree.Nodes.AddRange(new DevExpress.XtraGrid.GridLevelNode[] {
myGridLevelNode1});
gridControl.Dock = System.Windows.Forms.DockStyle.Right;
gridControl.MainView = view1;
gridControl.Name = "gridControl1";
gridControl.ViewCollection.AddRange(new DevExpress.XtraGrid.Views.Base.BaseView[] { view1, view2, view3 });
this.Controls.Add(gridControl);
Access Rows
BaseView.SourceRow, BaseView.SourceRowHandle
Returns a row (or its handle) that owns this clone View.
Expand and Collapse Rows
GridView.ExpandMasterRow, GridView.CollapseMasterRow
Expands or collapses the specific master row.
GridView.GetMasterRowExpanded, GridView.SetMasterRowExpanded
Call these methods to identify whether or not a particular master row is collapsed and expand it manually.
GridView.GetMasterRowExpandedEx, GridView.SetMasterRowExpandedEx
Call these methods to identify whether or not a specific clone of a master row is visible, and if not, show it.
Collapses all master rows for a specific View.
Example 1: Process All Rows
The following example iterates through rows of all master and clone Views. To access clones, master rows must be expanded. After all rows have been processed, their original expand/collapse state is restored.
using DevExpress.XtraGrid.Views.Grid;
using DevExpress.XtraGrid.Views.Base;
public void NavigateDetails(ColumnView inspectedView) {
if(inspectedView == null) return;
// Prevent excessive visual updates.
inspectedView.BeginUpdate();
try {
GridView gridView = inspectedView as GridView;
// Get the number of data rows in the View.
int dataRowCount;
if(gridView == null)
dataRowCount = inspectedView.RowCount;
else
dataRowCount = gridView.DataRowCount;
// Traverse View's rows.
for(int rowHandle = 0; rowHandle < dataRowCount; rowHandle++) {
// Place your code here to process the current row.
// ...
if(gridView != null) {
// Get the number of master-detail relationships for the current row.
int relationCount = gridView.GetRelationCount(rowHandle);
// Iterate through master-detail relationships.
for(int relationIndex = 0; relationIndex < relationCount; relationIndex++) {
// Store expansion status of the corresponding detail View.
bool wasExpanded = gridView.GetMasterRowExpandedEx(rowHandle, relationIndex);
// Expand the detail View.
if(!wasExpanded)
gridView.SetMasterRowExpandedEx(rowHandle, relationIndex, true);
// Navigate the detail View.
NavigateDetails((ColumnView)gridView.GetDetailView(rowHandle, relationIndex));
// Restore the row's expansion status.
gridView.SetMasterRowExpandedEx(rowHandle, relationIndex, wasExpanded);
}
}
}
}
finally {
// Enable visual updates.
inspectedView.EndUpdate();
}
}
Example 2: Recursively Expand a Master Row
This method expands a row and scans for its detail Views. If there are any, they are expanded as well. The process repeats until all underlying details are expanded.
using DevExpress.XtraGrid.Views.Grid;
// expand all nested details for the focused row
private void barButtonItem1_ItemClick(object sender, DevExpress.XtraBars.ItemClickEventArgs e) {
RecursiveExpand(gridView1, gridView1.FocusedRowHandle);
}
// custom method for expanding nested details
public void RecursiveExpand(GridView masterView, int masterRowHandle) {
try {
var relationCount = masterView.GetRelationCount(masterRowHandle);
for (var index = relationCount - 1; index >= 0; index--) {
masterView.ExpandMasterRow(masterRowHandle, index);
var childView = masterView.GetDetailView(masterRowHandle, index) as GridView;
if (childView != null) {
var childRowCount = childView.DataRowCount;
for (var handle = 0; handle < childRowCount; handle++)
RecursiveExpand(childView, handle);
}
}
}
catch (Exception ex) { }
finally { }
}
Example 3: Expand All Master Rows
The Data Grid does not provide a method to expand all master rows for a single View. You can, however, implement such a method yourself.
using DevExpress.XtraGrid.Views.Grid;
//...
public void ExpandAllRows(GridView View) {
View.BeginUpdate();
try {
int dataRowCount = View.DataRowCount;
for(int rHandle = 0; rHandle < dataRowCount; rHandle ++)
View.SetMasterRowExpanded(rHandle, true);
}
finally {
View.EndUpdate();
}
}