Working with Master-Detail Relationships in Code

  • 20 minutes to read

Data Binding

Public API

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;
    };
    
  • GridView.MasterRowEmpty

    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.

    
    gridView1.MasterRowGetRelationName += (s, e) => {
        switch (e.RelationIndex) {
            case 0:
                e.RelationName = "Order Detail";
                break;
            case 1:
                e.RelationName = "Customer Details";
                break;
            case 2:
                e.RelationName = "Employee Details";
                break;
        }
    };
    
  • 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

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.

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.

The code below demonstrates the IRelationList interface implemented for the NWTables class, which serves as a Data Grid source.


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]);}
    }
}

Access Views

GridControl.MainView

Retrieves the top-level master View.

GridControl.ViewRegistered

Data Grid dynamically creates and discards detail clone Views. When the control creates a detail clone and places it inside the GridControl.Views collection, the ViewRegistered event occurs. Handle it to retrieve this new clone View.

using DevExpress.XtraGrid.Columns;
using DevExpress.XtraGrid.Views.Grid;

private void gridControl1_ViewRegistered(object sender, DevExpress.XtraGrid.ViewOperationEventArgs e) {
    GridView view = (GridView)e.View;
    foreach (GridColumn col in view.Columns) {
        if (col.FieldName == "Discount")
            col.Caption = "Percent";
    }
}

GridView.GetDetailView

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");
}

BaseView.ParentView

For clone Views, this property returns a master View. For pattern and main Views, returns null (Nothing in Visual Basic).

GridView.GetVisibleDetailView

Returns a currently visible detail clone for a specific master row.

GridControl.Views

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.

GridControl.LevelTree

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.

GridView.CollapseAllDetails

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();
    }
}
See Also