Search code examples
c#wpfdatagridheaderwrappanel

How to 'wrap' the columns (not the text/content) of a WPF DataGrid?


I'm trying to implement a solution where I can 'wrap' a WPF DataGrid - by that I mean that the entire columns and rows wrap, not their text or content (see image below).

I have a set of data comprised of columns and rows (with column headers) that I want to wrap when constricting the window's width constraints, rather than instead using a horizontal scroll bar which would mean data is presented off-screen.

enter image description here

I had a look at using a WrapPanel as the ItemsPanelTemplate of my DataGrid, however I was not able to build on this further to achieve what I wanted.

    <DataGrid.ItemsPanel>
            <ItemsPanelTemplate>
                <WrapPanel Orientation="Horizontal" />
            </ItemsPanelTemplate>
        </DataGrid.ItemsPanel>

If there is an answer that achieves what I want using another control, i.e. a ListView or GridView without compromises, I would be happy with that.

My current solution is to manually modify my ItemsSource and break that up, and then create multiple DataGrids of a pre-determined size, which is not very flexible.


Solution

  • Testing DataGrid.ItemsPanel and WrapPanel

    The DataGrid only displays rows of data. So, if we set WrapPanel.Orientation to "Horizontal":

    <DataGrid x:Name="dg" ItemsSource="{Binding _humans}">
        <DataGrid.ItemsPanel>
            <ItemsPanelTemplate>
                <WrapPanel Orientation="Horizontal"/>
            </ItemsPanelTemplate>
        </DataGrid.ItemsPanel>
    </DataGrid>
    

    Each row will be positioned side by side horizontally: DataGrid and WrapPanel

    ListView and GridView

    If we want to display Property columns separately, we should use a ListView for each property. The GridView will be used for displaying the header. In the XAML document we set a Name for the WrapPanel.

    <Grid>
        <ScrollViewer VerticalScrollBarVisibility="Auto">
            <WrapPanel Name="wraper" Orientation="Horizontal">
            </WrapPanel>
        </ScrollViewer>
    </Grid>
    

    The code behind is implemented as:

    public partial class MainWindow : Window
        {
            private IList<Human> _humans;
            public MainWindow()
            {
                InitializeComponent();
            }
    
            private void Window_Loaded(object sender, RoutedEventArgs e)
            {
                CreateHumans();
                wraper.Children.Add(CreateListViewFor("FirstName"));
                wraper.Children.Add(CreateListViewFor("LastName"));
                wraper.Children.Add(CreateListViewFor("Age"));
                wraper.Children.Add(CreateListViewFor("Bday", "Birthday"));
                wraper.Children.Add(CreateListViewFor("Salary"));
                wraper.Children.Add(CreateListViewFor("Id", "Identification"));
            }
    
            private void CreateHumans()
            {
                _humans = new List<Human>();
                for (int i = 10; i < 20; i++)
                {
                    var human = new Human();
                    human.FirstName = "Zacharias";
                    human.LastName = "Barnham";
                    human.Bday = DateTime.Parse("1.3.1990");
                    human.Id = "ID-1234-zxc";
                    human.Salary = 2_000_000;
                    _humans.Add(human);
                }
            }
    
            private ListView CreateListViewFor(string propertyName, string header)
            {
                var lv = new ListView();
                var gv = new GridView();
                lv.ItemsSource = _humans;
                lv.View = gv;
                lv.SelectionChanged += UpdateSelectionForAllListViews;
                gv.Columns.Add(new GridViewColumn() { Header = header, DisplayMemberBinding = new Binding(propertyName), Width = 100 });
                return lv;
            }
    
            private ListView CreateListViewFor(string propertyName)
            {
                return CreateListViewFor(propertyName, propertyName);
            }
    
            private void UpdateSelectionForAllListViews(object sender, SelectionChangedEventArgs e)
            {
                int index = (sender as ListView).SelectedIndex;
                foreach (var child in wraper.Children)
                {
                    (child as ListView).SelectedIndex = index;
                }
            }
        }
    

    We call our CreateListViewFor() method to provide ListView objects to the WrapPanel. We create a callback for the Selection Changed event to update the selection for each list. The styling is up to you.

    The next gif shows the final result: Final result