Skip to main content

Working with Master-Detail Relationships in Code

  • 23 minutes to read

Bind to a Database with Master-Detail

The following example demonstrates how to bind 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.1\\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:

Master-Detail Visualization - WinForms Data Grid

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 DataController.AllowIEnumerableDetails option before you bind the Data Grid to a data source to handle these properties as collections.

The following example demonstrates how to bind 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:

Bind to Master-Detail Data Objects - WinForms Data Grid

Load Details on Demand (Handle Events)

Primary Events

Handle the following events to load detail data:

Event Name

Description

MasterRowGetRelationCount

Fires for every master row and allows you to specify the number of details. Use the e.RowHandle parameter to identify a master row. Set the e.RelationCount parameter to the number of detail tables (views).

The Data Grid may fire the MasterRowGetRelationCount event with the e.RowHandle equal to GridControl.InvalidRowHandle. This is a service event that allows you to display or hide all master row expand/collapse buttons. Set the e.RelationCount to any positive number to display expand/collapse buttons.

gridView.MasterRowGetRelationCount += (s, e) => {
    e.RelationCount = 1;
};

MasterRowEmpty

Fires for each master with details. The MasterRowEmpty event can fire multiple times to the same master row. It depends on how many related details the master row has. The AllowExpandEmptyDetails option must be disabled.

Use the e.RelationIndex parameter to get the processed detail. Set the e.IsEmpty parameter to true if the detail is empty. The Data Grid does not display empty detail views.

gridView.MasterRowGetRelationCount += (s, e) => {
    e.RelationCount = 2;
};

gridView.MasterRowEmpty += (s, e) => {
    e.IsEmpty = false;
};

MasterRowGetRelationName

Handle this event to specify the relation name for each detail. Use the e.RelationName parameter to specify the relation name.

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.

gridView1.MasterRowGetRelationName += (s, e) => {
    switch (e.RelationIndex) {
        case 0:
            e.RelationName = "Order Detail";
            break;
        case 1:
            e.RelationName = "Customer Details";
            break;
    }
};

MasterRowGetChildList

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 e.ChildList property to supply data to clone views.

gridView.MasterRowGetChildList += (s, e) => {
    e.ChildList = e.RelationIndex == 0 ? SampleData1.GetData().ToList() : SampleData2.GetData().ToList();
};

Run Demo: Master-Detail Mode with Events

Optional Events

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:

Display Master-Detail Data with Events - WinForms Data Grid

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:

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 demonstrates how to set 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:

Multi-Level Master-Detail Visualization - WinForms Data Grid

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 demonstrates how to customize the 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:

Access Clone Views - WinForms Data Grid

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:

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

Knowledge Base Articles

Cheat Sheets and Best Practices

Read the following quick-reference guide for general information and examples:

Master-Detail Mode - DevExpress WinForms Cheat Sheet

See Also