Working with Master-Detail Relationships in Code
- 23 minutes to read
Bind to a Database with Master-Detail
The following example binds the DevExpress WinForms Data Grid to the nwind.mdb database and display a master-detail relationship. This database ships as part of DevExpress WinForms Demos and is located at C:\Users\Public\Documents\DevExpress Demos 2x.x\Components\Data.
using System.Data;
using System.Data.OleDb;
using DevExpress.Utils;
using DevExpress.XtraEditors.Repository;
using DevExpress.XtraGrid.Views.Card;
namespace DXDataGridMasterDetailApp {
public partial class Form1 : DevExpress.XtraEditors.XtraForm {
public Form1() {
InitializeComponent();
// Creates a connection to the Nwind database.
OleDbConnection connection = new OleDbConnection(
"Provider=Microsoft.Jet.OLEDB.4.0;Data Source = C:\\Users\\Public\\Documents\\DevExpress Demos 24.2\\Components\\Data\\nwind.mdb");
// Creates the data adapters to retrieve data from the Categories and Products data 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 dataSetNwind = new DataSet();
// Creates DataTable objects that correspond to database tables.
adapterCategories.Fill(dataSetNwind, "Categories");
adapterProducts.Fill(dataSetNwind, "Products");
// Sets up a master-detail relationship between data tables.
DataColumn keyColumn = dataSetNwind.Tables["Categories"].Columns["CategoryID"];
DataColumn foreignKeyColumn = dataSetNwind.Tables["Products"].Columns["CategoryID"];
dataSetNwind.Relations.Add("CategoriesProducts", keyColumn, foreignKeyColumn);
// Binds the Data Grid to a data source.
gridControl1.DataSource = dataSetNwind.Tables["Categories"];
// Forces the Data Grid to initialize its settings.
gridControl1.ForceInitialize();
// Creates a pattern view (CardView) to display detail data.
CardView cardViewProducts = new CardView(gridControl1);
gridControl1.LevelTree.Nodes.Add("CategoriesProducts", cardViewProducts);
// Specifies the detail view's caption (the caption of detail tabs).
cardViewProducts.ViewCaption = "Category Products";
// Hides the CategoryID column from the master view.
gridView1.Columns["CategoryID"].VisibleIndex = -1;
/* Creates a Picture Edit repository item to display images in the Picture column,
* adds it to the Data Grid's RepositoryItems collection, and sets up image settings.
*/
RepositoryItemPictureEdit riPictureEdit = gridControl1.RepositoryItems.Add("PictureEdit") as RepositoryItemPictureEdit;
gridView1.Columns["Picture"].ColumnEdit = riPictureEdit;
riPictureEdit.SizeMode = DevExpress.XtraEditors.Controls.PictureSizeMode.Stretch;
// Specifies the width of the Picture column.
gridView1.Columns["Picture"].Width = 250;
gridView1.Columns["Picture"].OptionsColumn.FixedWidth = true;
// Enables automatic height for master rows.
gridView1.OptionsView.RowAutoHeight = true;
// Creates columns in a detail pattern view (CardView) for all fields in the Products table.
cardViewProducts.PopulateColumns(dataSetNwind.Tables["Products"]);
// Hides the CategoryID column.
cardViewProducts.Columns["CategoryID"].VisibleIndex = -1;
// Formats cell values in the UnitPrice column as currency.
cardViewProducts.Columns["UnitPrice"].DisplayFormat.FormatType = FormatType.Numeric;
cardViewProducts.Columns["UnitPrice"].DisplayFormat.FormatString = "c2";
}
}
}
The screenshot below shows the result:
Bind to Objects with Collection Properties
The Data Grid handles properties of the IList type (for example, ArrayList, List<T>) as detail views if its GridOptionsDetail.EnableMasterViewMode option is enabled.
Note
The Data Grid does not handle properties of the IList<T>
, ICollection<T>
, and IEnumerable
types as collections. Enable the View’s BaseView.DataController.AllowIEnumerableDetails
option before you bind the Data Grid to a data source to handle these properties as collections.
The following example binds the Data Grid to ArrayList
(Orders). Data source items are objects of the Order
type with the Products
collection property. The Data Grid automatically identifies and visualizes a master-detail relation.
using System;
using System.Collections;
using System.ComponentModel.DataAnnotations;
namespace DXDataGridMasterDetailApp {
public partial class Form1 : DevExpress.XtraEditors.XtraForm {
public Form1() {
InitializeComponent();
// Binds the Data Grid to a data source (ArrayList descendant).
gridControl1.DataSource = new Orders();
}
}
public class Orders : ArrayList {
public Orders() {
Add(new Order(new ArrayList() {
new Product(){ ProductName = "Product A-1", Price = 78.99 },
new Product(){ ProductName = "Product A-2", Price = 199.99 },
new Product(){ ProductName = "Product A-3", Price = 18.99 },
}) { OrderName = "Order A", OrderDate = DateTime.Today } );
Add(new Order(new ArrayList() {
new Product(){ ProductName = "Product B-1", Price = 25.99 },
new Product(){ ProductName = "Product B-2", Price = 277.99 },
new Product(){ ProductName = "Product B-3", Price = 10.99 },
}) { OrderName = "Order B", OrderDate = DateTime.Today });
Add(new Order(new ArrayList() {
new Product(){ ProductName = "Product C-1", Price = 5.99 },
new Product(){ ProductName = "Product C-2", Price = 14.99 },
new Product(){ ProductName = "Product C-3", Price = 77.99 },
}) { OrderName = "Order C", OrderDate = DateTime.Today });
}
public virtual new Order this[int index] {
get {
return (Order)base[index];
}
}
}
public class Order {
ArrayList products;
public Order(ArrayList productsList) {
products = productsList;
}
public string OrderName { get; set; }
public DateTime OrderDate { get; set; }
public ArrayList Products {
get { return products; }
set { products = value; }
}
}
public class Product {
public string ProductName { get; set; }
[DisplayFormat(DataFormatString = "c2")]
public double Price { get; set; }
}
}
The image below shows the result:
Load Details on Demand (Handle Events)
Primary Events
Handle the following events to load detail data:
Event Name | Description |
---|---|
Fires for every master row and allows you to specify the number of details. Use the The Data Grid may fire the | |
Fires for each master with details. The Use the | |
Handle this event to specify the relation name for each detail. Use the Important The level name must exactly match the name and case of the master-detail relation with which the view is associated. If a detail view displays data from a collection property, the level name must match the collection name. | |
The Data Grid dynamically creates a clone view based on the settings of its pattern view when a user expands a master row (a clone view is a copy of a pattern view). Use the |
Optional Events
GridView.MasterRowGetRelationDisplayCaption
Detail tabs’ captions match detail names set in the GridView.MasterRowGetRelationName event handler. Handle the
MasterRowGetRelationDisplayCaption
event to assign custom tab captions.GridView.MasterRowGetLevelDefaultView
This event allows you to explicitly set a pattern View for a specific detail. Handle this event when the View does not belong to the GridControl.LevelTree tree. Otherwise, pattern Views are set indirectly in the GridView.MasterRowGetRelationName event handler.
-
Handle this event to prevent users from expanding certain master rows based on a condition.
Example: How to Display Master-Detail Data on Events
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace DXDataGridMasterDetailApp {
public partial class Form1 : DevExpress.XtraEditors.XtraForm {
public Form1() {
InitializeComponent();
gridControl1.DataSource = SampleData.GetData();
gridView1.MasterRowGetRelationCount += (s, e) => {
e.RelationCount = 1;
};
gridView1.MasterRowEmpty += (s, e) => {
e.IsEmpty = false;
};
gridView1.MasterRowGetChildList += (s, e) => {
e.ChildList = SampleData.GetData();
};
gridView1.MasterRowGetRelationName += (s, e) => {
e.RelationName = "Test Relation Name";
};
}
}
public class SampleData {
[Display(Order = -1)]
public int ID { get; set; }
public string Name { get; set; }
public double Length { get; set; }
public bool Mark { get; set; }
public DateTime RecordDate { get; set; }
public static List<SampleData> GetData() {
return new List<SampleData>() {
new SampleData(){ ID = 0, Name = "Bluehead Wrasse", Length = 15.09, Mark = false, RecordDate = DateTime.Now },
new SampleData(){ ID = 1, Name = "Ornate ButterflyFish", Length = 15.09, Mark = true, RecordDate = DateTime.Now },
new SampleData(){ ID = 2, Name = "Senorita", Length = 15.09, Mark = true, RecordDate = DateTime.Now },
new SampleData(){ ID = 3, Name = "Surf Smelt", Length = 15.09, Mark = false, RecordDate = DateTime.Now },
};
}
}
}
The screenshot below shows the result:
Load Details Dynamically by Implementing IRelationList, IRelationListEx
This is an alternative to handling master-detail events. Implement the IRelationList or IRelationListEx interface in a data source or .NET data structure (for example, System.Data.DataView
). The IRelationList interface exposes the following properties and methods:
-
Gets the number of master-detail relations for master rows. The Data Grid disables the master-detail mode if the
RelationCount
property is set to 0. IRelationList.IsMasterRowEmpty
Specifies whether a detail is empty (the detail has no data).
-
Returns the relation name.
-
Returns detail data for a clone view.
The IRelationListEx interface extends the IRelationList with the following methods that allow you to specify the number of master-detail relationships and detail caption for master rows:
Example: How to Implement IRelationList (Master-Detail)
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;
}
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;
}
public virtual new NWTable this[int index] {
get {return (NWTable)(base[index]);}
}
}
Create Multi-Level Relationships
The following example sets up the WinForms Data Grid control to visualize the Categories-Products-Order Details master-detail relationship.
using System;
using System.Windows.Forms;
using DevExpress.XtraEditors;
using DevExpress.XtraGrid;
using DevExpress.XtraGrid.Views.Base;
using DevExpress.XtraGrid.Views.Card;
using DevExpress.XtraGrid.Views.Grid;
namespace DXDataGridMasterDetailApp {
public partial class Form1 : XtraForm {
GridControl gridControl;
public Form1() {
InitializeComponent();
gridControl = new GridControl();
GridView mainView = new GridView(gridControl);
GridView patternView1 = new GridView(gridControl);
CardView patternView2 = new CardView(gridControl);
GridLevelNode detailLevel1 = new GridLevelNode();
detailLevel1.LevelTemplate = patternView1;
detailLevel1.RelationName = "CategoriesProducts";
GridLevelNode detailLevel2 = new GridLevelNode();
detailLevel2.LevelTemplate = patternView2;
detailLevel2.RelationName = "ProductsOrder Details";
detailLevel1.Nodes.Add(detailLevel2);
gridControl.LevelTree.Nodes.Add(detailLevel1);
gridControl.MainView = mainView;
gridControl.Name = "gridControl1";
gridControl.Dock = DockStyle.Fill;
gridControl.ViewCollection.AddRange(new BaseView[] { mainView, patternView1, patternView2 });
this.Controls.Add(gridControl);
}
private void Form1_Load(object sender, EventArgs e) {
this.order_DetailsTableAdapter1.Fill(this.nwindDataSet1.Order_Details);
this.productsTableAdapter1.Fill(this.nwindDataSet1.Products);
this.categoriesTableAdapter.Fill(this.nwindDataSet1.Categories);
// Bind the Data Grid control to a data source.
gridControl.DataSource = categoriesBindingSource;
}
}
}
The screenshot below shows the result:
Access Master and Clone Views
Access Main (Root) View
Use the Data Grid’s MainView property to get the top-level master view. The grid’s FocusedView property gets or sets the View which is currently focused.
Initialize and Access Clone Views
The Data Grid dynamically creates a clone view based on the settings of its pattern view when a user expands a master row (a clone view is a copy of a pattern view). The Data Grid destroys the clone view after the corresponding master row is collapsed.
The Data Grid fires the ViewRegistered event after it creates a clone view. Handle this event to customize the clone view before it is displayed.
gridControl1.ViewRegistered += (s, e) => {
if(e.View.IsDetailView) {
// Customize the clone view.
}
};
Use the master view’s GetDetailView(Int32, Int32) method to get a clone view for a specific master row.
The following example customizes appearance settings of even rows in a clone view based on a condition.
using DevExpress.XtraGrid.Views.Grid;
gridControl1.ViewRegistered += (s, e) => {
if(e.View.IsDetailView) {
(e.View as GridView).OptionsView.EnableAppearanceEvenRow = true;
}
};
gridView1.MasterRowExpanded += (s, e) => {
GridView masterView = s as GridView;
GridView cloneView = masterView.GetDetailView(e.RowHandle, e.RelationIndex) as GridView;
cloneView.Appearance.EvenRow.Options.UseBackColor = true;
cloneView.Appearance.EvenRow.BackColor = cloneView.RowCount > 2 ? Color.LightGreen : Color.Orange;
};
The image below shows the result:
Get Visible Views
Use the Data Grid’s Views property to get a collection of all visible views. This collection includes a master (root) view and clone views displayed for expanded rows. The Data Grid automatically adds/removes clone views to/from this collection as rows expand or collapse.
Get Parent View
Use the view’s BaseView.ParentView property to get its parent view. For clone views, the ParentView
property returns a master view. For the main and pattern views, the ParentView
property returns null (Nothing in Visual Basic).
Get Visible Clone View
Use the master view’s GridView.GetVisibleDetailView method to get the currently visible detail (a clone view) for the specified master row.
Master-Detail Level Tree
Use the Data Grid’s LevelTree property to get a collection of relation levels (GridLevelNode) - a ‘hierarchical’ structure of pattern views in a master-detail relationship.
using DevExpress.XtraGrid;
using DevExpress.XtraGrid.Views.Base;
using DevExpress.XtraGrid.Views.Grid;
// Binds the Data Grid to a data source.
gridControl.DataSource = Category.GetMasterDetailData();
// Creates a pattern view and customizes its appearance settings.
GridView detailPatternView = new GridView { ViewCaption = Category.ProductsLevelName };
detailPatternView.Appearance.Row.BackColor = Color.Coral;
detailPatternView.Appearance.Row.Options.UseBackColor = true;
// Adds the pattern view to the Data Grid's ViewCollection.
gridControl.ViewCollection.Add(detailPatternView);
// Associates the pattern view with the Products relation level.
gridControl.LevelTree.Nodes.Add(Category.ProductsLevelName, detailPatternView);
Get Master Rows
Use a clone view’s SourceRow method to get its master row. The clone view’s SourceRowHandle property allows you to get the handle of its master row.
Get and Set Cell Values in Clone Views
Use the parent (master) view’s GetDetailView method to obtain a clone view (detail) for the specified expanded master row. Use the clone view’s data editing API to get and set cell values.
Expand and Collapse Master Rows
Use the parent view’s GridView.ExpandMasterRow and GridView.CollapseMasterRow methods to expand/collapse the specified master row. The CollapseAllDetails() method collapses all master rows in a view. You can also use the SetMasterRowExpanded method to expand/collapse a master row.
Expand and collapse API also includes the following methods:
- GetMasterRowExpanded(Int32)
- GetMasterRowExpandedEx(Int32, Int32)
- SetMasterRowExpandedEx(Int32, Int32, Boolean)
Handle the MasterRowExpanding and MasterRowCollapsing events to prevent the user from expanding/collapsing certain rows.
The MasterRowExpanded and MasterRowCollapsed events notify that the user has expanded/collapsed a master row.
Example 1: Iterate Over Rows in Main and Clone Views
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 the 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: Expand a Master Row and Nesting Details
This example demonstrates how to expand the focused row and all nested details.
Note
The OptionsDetail.AllowOnlyOneMasterRowExpanded property of detail views must be set to true.
using DevExpress.XtraGrid.Views.Grid;
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 in a View
Note
The master view’s OptionsDetail.AllowOnlyOneMasterRowExpanded property must be set to true.
using DevExpress.XtraGrid.Views.Grid;
public void ExpandAllMasterRows(GridView View) {
View.BeginUpdate();
try {
int dataRowCount = View.DataRowCount;
for(int rHandle = 0; rHandle < dataRowCount; rHandle ++)
View.SetMasterRowExpanded(rHandle, true);
}
finally {
View.EndUpdate();
}
}
Example 4: Expand All Master Rows and Detail Views
The following example expands all master rows and all detail/nested views after the grid has been loaded. The example uses ExpandAllMasterRows
and RecursiveExpand
methods implemented in Example 3 and Example 2 respectively. The ExpandAllMasterRows
is modified to call the RecursiveExpand
method:
Note
The OptionsDetail.AllowOnlyOneMasterRowExpanded property of master and detail views must be set to true.
private void gridControl1_Load(object sender, EventArgs e) {
ExpandAllMasterRows(gridView1);
}
public void ExpandAllMasterRows(GridView View) {
View.BeginUpdate();
try {
int dataRowCount = View.DataRowCount;
for (int rHandle = 0; rHandle < dataRowCount; rHandle++) {
View.SetMasterRowExpanded(rHandle, true);
RecursiveExpand(View, rHandle);
}
}
finally {
View.EndUpdate();
}
}
public void RecursiveExpand(GridView masterView, int masterRowHandle) {
// ...
}
Additional Examples
- How to: Bind GridControl to a DataBase and Implement Master-Detail Mode at Runtime
- How to: Bind GridControl to Database and Implement Master-Detail Mode at Design Time
- How to: Expand and Maximize a Specific Detail
- How to: Hide Expand Buttons for Master Rows with Empty Details
- How to: Drag Card Between Details
- How to: Replace a View that Represents a Specific Master-Detail Relationship
- How to: Hide Disabled Expand/Collapse Buttons for Master Rows without Detail Records
- How to: Display Master-Detail Tables in Separate Grid Controls
- How to: Dynamically Load and Refresh Detail Data from the Database
- Asynchronous detail view loading
- What Can Cause Properties, Methods, and Events of a Detail View to Fail?
Knowledge Base Articles
- How to: Prevent a User from Opening Certain Detail Levels
- How to: Reorder Detail Views (Tabs)
- How to: Edit Records in Detail View When a Value in Master Row was Changed
- How to: Auto-Expand All Sub-Detail Views when Expanding a Root Master Row
- How to: Insert a New Row into a Detail View and Populate Foreign Keys
Cheat Sheets and Best Practices
Read the following quick-reference guide for general information and examples:
Master-Detail Mode - DevExpress WinForms Cheat Sheet