Example E4630
Visible to All Users

WPF Data Grid - Create a Custom GridControl that Displays Horizontal Columns

This example demonstrates how to create a GridControl class descendant with horizontally-oriented columns (similar to the WinForms Vertical Grid Control).

image

This functionality is not available out of the box, so we used unbound columns to get/set cell values and customized row appearance (Modify Theme Resources).

If you want to use this custom solution in your real application, first check if you can implement one of the following solutions instead:

  1. Change the structure of your ItemsSource collection to use the regular Data Grid.
  2. If you wish to show properties of a single object, use the Property Grid.

Files to Review

Documentation

Does this example address your development requirements/objectives?

(you will be redirected to DevExpress.com to submit your response)

Example Code

dxExample/VGrid/VerticalGridControl.cs(vb)
C#
using DevExpress.Data; using DevExpress.Xpf.Grid; using System; using System.Collections; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; using System.Reflection; using System.Windows; using System.Windows.Controls; using System.Windows.Data; namespace dxExample.VGrid { public class VerticalGridControl : GridControl { public VerticalRowsCollection Rows { get; set; } public bool AutoPopulateRows { get { return (bool)GetValue(AutoPopulateRowsProperty); } set { SetValue(AutoPopulateRowsProperty, value); } } public new object ItemsSource { get { return (object)GetValue(ItemsSourceProperty); } set { SetValue(ItemsSourceProperty, value); } } public VerticalGridControl() { CustomUnboundColumnData += OnCustomUnboundColumnData; InitializeRowsCollection(); } void InitializeRowsCollection() { Rows = new VerticalRowsCollection(); Rows.CollectionChanged += OnRowsCollectionChanged; } void UpdateGridColumns() { ICollection itemsSource = ItemsSource as ICollection; if (itemsSource == null) { Columns.Clear(); return; } int columnsCount = itemsSource.Count; if (Columns.Count == columnsCount) return; Columns.BeginUpdate(); int delta = columnsCount - Columns.Count; if (columnsCount > Columns.Count) { for (int i = Columns.Count; i < columnsCount; i++) { Columns.Add(new GridColumn() { FieldName = i.ToString(), UnboundType = UnboundColumnType.Object }); } } else { for (int i = Columns.Count - 1; i >= columnsCount; i--) { Columns.RemoveAt(i); } } Columns.EndUpdate(); } void UpdateItemsSourceCollectionChangedSubscription(object oldSource, object newSource) { if (oldSource is INotifyCollectionChanged) { ((INotifyCollectionChanged)oldSource).CollectionChanged -= OnItemsSourceCollectionChanged; } if (newSource is INotifyCollectionChanged) { ((INotifyCollectionChanged)newSource).CollectionChanged += OnItemsSourceCollectionChanged; } } void PopulateRows() { if (!(ItemsSource is IEnumerable)) return; var firstItem = ((IEnumerable)ItemsSource).Cast<object>().FirstOrDefault(); if (firstItem == null) return; var newRows = TypeDescriptor.GetProperties(firstItem) .Cast<PropertyDescriptor>() .Select(p => new VerticalRow() { RowName = p.Name }); foreach (var row in newRows) { Rows.Add(row); } } void OnRowsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { base.ItemsSource = Rows; } static void OnItemsSourceChanged(object sender, DependencyPropertyChangedEventArgs e) { var grid = (VerticalGridControl)sender; grid.UpdateGridColumns(); if (grid.AutoPopulateRows) { grid.PopulateRows(); } grid.UpdateItemsSourceCollectionChangedSubscription(e.OldValue, e.NewValue); } void OnItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if (e.Action == NotifyCollectionChangedAction.Add || e.Action == NotifyCollectionChangedAction.Remove) { UpdateGridColumns(); } } void OnCustomUnboundColumnData(object sender, GridColumnDataEventArgs e) { var itemsSource = ItemsSource as IList; if (itemsSource == null) return; var row = Rows[e.ListSourceRowIndex]; var item = itemsSource[Convert.ToInt32(e.Column.FieldName)]; var targetProperty = TypeDescriptor.GetProperties(item).Cast<PropertyDescriptor>().FirstOrDefault(p => p.Name == row.RowName); if (targetProperty == null) return; if (e.IsGetData) { e.Value = targetProperty.GetValue(item); } if (e.IsSetData) { targetProperty.SetValue(item, e.Value); } } public static readonly DependencyProperty AutoPopulateRowsProperty = DependencyProperty.Register("AutoPopulateRows", typeof(bool), typeof(VerticalGridControl), new PropertyMetadata(false)); public static new readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(object), typeof(VerticalGridControl), new PropertyMetadata(null, OnItemsSourceChanged)); } public class BottomIndicatorRowVisibilityConverter : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (values.Count() < 2) return Visibility.Collapsed; if (!((values[0] is int) && (values[1] is int))) return Visibility.Collapsed; return ((int)values[0]) > ((int)values[1]) ? Visibility.Visible : Visibility.Collapsed; } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } } public class DefaultCellTemplateSelector : DataTemplateSelector { public override DataTemplate SelectTemplate(object item, DependencyObject container) { var row = (VerticalRow)((EditGridCellData)item).RowData.Row; return (row.CellTemplate != null) ? row.CellTemplate : base.SelectTemplate(item, container); } } public class VerticalRow : DependencyObject { public string RowName { get; set; } public DataTemplate CellTemplate { get { return (DataTemplate)GetValue(CellTemplateProperty); } set { SetValue(CellTemplateProperty, value); } } public static readonly DependencyProperty CellTemplateProperty = DependencyProperty.Register("CellTemplate", typeof(DataTemplate), typeof(VerticalRow), new PropertyMetadata(null)); public static VerticalRow FromPropertyInfo(PropertyInfo info) { return new VerticalRow() { RowName = info.Name }; } } public class VerticalRowsCollection : ObservableCollection<VerticalRow> { protected override void InsertItem(int index, VerticalRow item) { int existsIndex = IndexOf(item.RowName); if (existsIndex > -1) { if (Items[existsIndex].CellTemplate != null) return; Items[existsIndex].CellTemplate = item.CellTemplate; return; } base.InsertItem(index, item); } int IndexOf(string rowName) { for (int i = 0; i < Items.Count; i++) { if (Items[i].RowName == rowName) return i; } return -1; } } }
dxExample/VGrid/VerticalGridControlResources.xaml
XAML
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:dxg="http://schemas.devexpress.com/winfx/2008/xaml/grid" xmlns:dxgt="http://schemas.devexpress.com/winfx/2008/xaml/grid/themekeys" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:dxExample.VGrid" xmlns:sys="clr-namespace:System;assembly=mscorlib"> <sys:Double x:Key="{dxgt:TableViewThemeKey ResourceKey=IndicatorWidth, ThemeName=Office2016White}">180</sys:Double> <!-- only for optimized mode --> <ControlTemplate x:Key="{dxgt:RowIndicatorThemeKey ResourceKey=RowIndicatorTemplate, ThemeName=Office2016White}" TargetType="{x:Type dxg:RowIndicator}"> <Grid> <Border x:Name="PART_ContentBorder" Opacity="0" /> <ContentPresenter Content="{Binding}" ContentTemplate="{DynamicResource {dxgt:RowIndicatorThemeKey ResourceKey=RowTemplate, ThemeName=Office2016White}}" /> </Grid> </ControlTemplate> <DataTemplate x:Key="{dxgt:RowIndicatorThemeKey ResourceKey=RowTemplate, ThemeName=Office2016White}"> <Grid> <Border Background="White" BorderBrush="DarkGray" BorderThickness="0,0,1,1"> <dxg:GridColumnHeader /> </Border> <Border x:Name="BottomRowBorder" BorderBrush="Black" BorderThickness="0,0,0,1"> <Border.Visibility> <MultiBinding> <Binding Path="Level"/> <Binding Path="NextRowLevel"/> <MultiBinding.Converter> <local:BottomIndicatorRowVisibilityConverter/> </MultiBinding.Converter> </MultiBinding> </Border.Visibility> </Border> <TextBlock Text="{Binding DataContext.Row.RowName, RelativeSource={RelativeSource TemplatedParent}}" HorizontalAlignment="Center" VerticalAlignment="Center"/> </Grid> </DataTemplate> <Style TargetType="{x:Type dxg:TableView}"> <Style.Setters> <Setter Property="ShowTotalSummary" Value="False"/> <Setter Property="AllowColumnMoving" Value="False"/> <Setter Property="ShowGroupPanel" Value="False"/> <Setter Property="AllowColumnFiltering" Value="False"/> <Setter Property="MultiSelectMode" Value="Cell"/> <Setter Property="CellTemplateSelector" Value="{DynamicResource defaultCellTemplateSelector}"/> <Setter Property="UseLightweightTemplates" Value="All" /> </Style.Setters> </Style> <Style TargetType="{x:Type dxg:GridControl}"> <Style.Setters> <Setter Property="AutoPopulateColumns" Value="False"/> </Style.Setters> </Style> <local:DefaultCellTemplateSelector x:Key="defaultCellTemplateSelector"/> </ResourceDictionary>
dxExample/MainWindow.xaml
XAML
<Window x:Class="dxExample.MainWindow" Title="MainWindow" Height="350" Width="850" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:dxe="http://schemas.devexpress.com/winfx/2008/xaml/editors" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:VGrid="clr-namespace:dxExample.VGrid"> <Window.Resources> <ResourceDictionary> <DataTemplate x:Key="dateCellTemplate"> <dxe:DateEdit Name="PART_Editor"/> </DataTemplate> <DataTemplate x:Key="boolCellTemplate"> <dxe:CheckEdit Name="PART_Editor"/> </DataTemplate> <DataTemplate x:Key="intCellTemplate"> <dxe:TextEdit Name="PART_Editor" MaskType="Numeric" Mask="d"/> </DataTemplate> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="VGrid/VerticalGridControlResources.xaml" /> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Window.Resources> <Grid> <VGrid:VerticalGridControl x:Name="grid" Margin="2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" ItemsSource="{Binding Orders}" AutoPopulateRows="True"> <VGrid:VerticalGridControl.Rows> <VGrid:VerticalRow RowName="Date" CellTemplate="{StaticResource dateCellTemplate}"/> <VGrid:VerticalRow RowName="HasFlag" CellTemplate="{StaticResource boolCellTemplate}"/> <VGrid:VerticalRow RowName="Id" CellTemplate="{StaticResource intCellTemplate}"/> </VGrid:VerticalGridControl.Rows> </VGrid:VerticalGridControl> </Grid> </Window>
dxExample/MainWindow.xaml.cs(vb)
C#
using System.Windows; namespace dxExample { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); grid.ItemsSource = SampleDataRow.CreateRows(10); } } }
dxExample/SampleDataRow.cs(vb)
C#
using System; using System.Collections.Generic; namespace dxExample { class SampleDataRow { public int Id { get; set; } public string Name {get; set;} public DateTime Date { get; set; } public bool HasFlag {get; set;} public static IList<SampleDataRow> CreateRows(int rowCount) { IList<SampleDataRow> result = new List<SampleDataRow>(); for(int i = 0; i < rowCount; i++) { result.Add(new SampleDataRow() { Id = i, Name = String.Format("name {0}", i % 3), Date = DateTime.Now.AddDays(i), HasFlag = (i % 3 == 0), }); } return result; } } }

Disclaimer: The information provided on DevExpress.com and affiliated web properties (including the DevExpress Support Center) is provided "as is" without warranty of any kind. Developer Express Inc disclaims all warranties, either express or implied, including the warranties of merchantability and fitness for a particular purpose. Please refer to the DevExpress.com Website Terms of Use for more information in this regard.

Confidential Information: Developer Express Inc does not wish to receive, will not act to procure, nor will it solicit, confidential or proprietary materials and information from you through the DevExpress Support Center or its web properties. Any and all materials or information divulged during chats, email communications, online discussions, Support Center tickets, or made available to Developer Express Inc in any manner will be deemed NOT to be confidential by Developer Express Inc. Please refer to the DevExpress.com Website Terms of Use for more information in this regard.