c# – WPF How to display an observable collection in a grid with Headers?

I’m trying to achieve the following result:

In the current image, I achieved this layout by creating a CustomUserControl like this:

<UserControl x:Class="FifteenFiftyThreeUi.UserControls.ReadOnlyIndicatorWithUnits"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="clr-namespace:FifteenFiftyThreeUi.UserControls"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             x:Name="root"
             d:DesignHeight="40"
             d:DesignWidth="220"
             mc:Ignorable="d">
    <Grid Background="Bisque">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="5*" />
            <ColumnDefinition Width="30*" />
            <ColumnDefinition Width="35*" />
            <ColumnDefinition Width="20*" />
            <ColumnDefinition Width="5*" />
        </Grid.ColumnDefinitions>
        <Label x:Name="indicatorLable"
               Grid.Column="1"
               VerticalAlignment="Center"
               Content="{Binding IndicatorLable, ElementName=root, FallbackValue=Altitude}" />
        <Label x:Name="indicatorValue"
               Grid.Column="2"
               HorizontalAlignment="Right"
               VerticalAlignment="Center"
               Content="{Binding IndicatorValue, ElementName=root, FallbackValue=95.75}" />
        <Label x:Name="indicatorUnit"
               Grid.Column="3"
               HorizontalAlignment="Center"
               VerticalAlignment="Center"
               Content="{Binding IndicatorUnits, ElementName=root, FallbackValue=[m/s]}" />
    </Grid>
</UserControl>

And then manually inserting those control in a Grid like so:

        <!--#region Navigation-->

        <Label Grid.Row="1"
               Grid.Column="1"
               VerticalAlignment="Center"
               Content="Navigation"
               FontSize="16"
               FontWeight="Bold" />

        <usercontrols:ReadOnlyIndicatorWithUnits Grid.Row="2"
                                                 Grid.Column="1"
                                                 IndicatorLable="Latitude"
                                                 IndicatorUnits="[SC]"
                                                 IndicatorValue="{Binding LatitudeValue, FallbackValue=No Value}" />

        <usercontrols:ReadOnlyIndicatorWithUnits Grid.Row="2"
                                                 Grid.Column="2"
                                                 IndicatorLable="Longitude"
                                                 IndicatorUnits="[SC]"
                                                 IndicatorValue="0.8" />

        <usercontrols:ReadOnlyIndicatorWithUnits Grid.Row="2"
                                                 Grid.Column="3"
                                                 IndicatorLable="Altitude"
                                                 IndicatorUnits="[m]"
                                                 IndicatorValue="987" />

        <usercontrols:ReadOnlyIndicatorWithUnits Grid.Row="3"
                                                 Grid.Column="1"
                                                 IndicatorLable="Roll"
                                                 IndicatorUnits="[SC]"
                                                 IndicatorValue="75.8" />

        <usercontrols:ReadOnlyIndicatorWithUnits Grid.Row="3"
                                                 Grid.Column="2"
                                                 IndicatorLable="Pitch"
                                                 IndicatorUnits="[SC]"
                                                 IndicatorValue="31.4" />

        <usercontrols:ReadOnlyIndicatorWithUnits Grid.Row="3"
                                                 Grid.Column="3"
                                                 IndicatorLable="Az.Geo"
                                                 IndicatorUnits="[SC]"
                                                 IndicatorValue="987" />

        <!--#endregion-->

However what I would like to do is to have an ObservableCollection in my viewModel and bind it to something like a ListView that would generate this layout.

For example a single indicator ViewModel will look like this (I removed INotifyPropertyChanged implementation in an effort of being concise though maybe that boat is sailed):

public class ReadOnlyIndicatorWithUnitsViewModel : ViewModelBase
    {
        #region Fields

        private string? _indicatorLabel = null;
        private string? _indicatorValue = null;
        private string? _indicatorDisplayUnits = null;
        private string? _indicatorGroup = null;

        #endregion Fields  

        public ReadOnlyIndicatorWithUnitsViewModel(string? indicatorLable, string? indicatorValue, string? indicatorDisplayUnits, string? indicatorGroup)
        {
            _indicatorLabel = indicatorLable;
            _indicatorValue = indicatorValue;
            _indicatorDisplayUnits = indicatorDisplayUnits;
            _indicatorGroup = indicatorGroup;
        }
     
}

And then in the main view model do something like this:

 public class NavigationViewModel : ViewModelBase
    {
        private MessageStore? _messageStore;

        private Dictionary<string, ReadOnlyIndicatorWithUnitsViewModel>? _fields;

        public ObservableCollection<ReadOnlyIndicatorWithUnitsViewModel> DataCollection { get; set; }

        public NavigationViewModel(MessageStore messageStore)
        {
            _messageStore = messageStore;
            _fields = new Dictionary<string, ReadOnlyIndicatorWithUnitsViewModel>();

            _messageStore.NavMessageChanged += OnNavMessageChanged;
//hardcoded values for testing
            _fields = new()
            {
                {"Latitude", new ReadOnlyIndicatorWithUnitsViewModel("Latitude","N/A","[SC]","Coordinates")},
                {"Longitude", new ReadOnlyIndicatorWithUnitsViewModel("Longitude","N/A","[SC]","Coordinates")},
                {"Altitude", new ReadOnlyIndicatorWithUnitsViewModel("Altitude","N/A","[SC]","Coordinates")},
                {"Roll", new ReadOnlyIndicatorWithUnitsViewModel("Roll","N/A","[SC]","Coordinates")},
                {"Pitch", new ReadOnlyIndicatorWithUnitsViewModel("Pitch","N/A","[SC]","Coordinates")},
                {"Az.Geo", new ReadOnlyIndicatorWithUnitsViewModel("Az.Geo","N/A","[SC]","Coordinates")},

                {"Accel X", new ReadOnlyIndicatorWithUnitsViewModel("Acc X","N/A","[m/s^2]","Acceleration")},
                {"Accel Y", new ReadOnlyIndicatorWithUnitsViewModel("Acc Y","N/A","[m/s^2]","Acceleration")},
                {"Accel Z", new ReadOnlyIndicatorWithUnitsViewModel("Acc Z","N/A","[m/s^2]","Acceleration")},
            };

            DataCollection = new ObservableCollection<ReadOnlyIndicatorWithUnitsViewModel>(_fields.Values);
        }

    }

NavigationView.xaml:

<ListView Grid.Row="1"
                  Grid.Column="1"
                  ItemsSource="{Binding DataCollection}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <Grid Background="BlanchedAlmond">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="5" />
                            <ColumnDefinition Width="2*" />
                            <ColumnDefinition Width="3*" />
                            <ColumnDefinition Width="1*" />
                            <ColumnDefinition Width="5" />
                        </Grid.ColumnDefinitions>
                        <Label Grid.Column="1"
                               HorizontalAlignment="Left"
                               VerticalAlignment="Center"
                               Content="{Binding IndicatorLable}" />
                        <Label Grid.Column="2"
                               HorizontalAlignment="Right"
                               VerticalAlignment="Center"
                               Content="{Binding IndicatorValue}" />
                        <Label Grid.Column="3"
                               HorizontalAlignment="Right"
                               VerticalAlignment="Center"
                               Content="{Binding IndicatorDisplayUnits}" />
                    </Grid>
                </DataTemplate>
            </ListView.ItemTemplate>            
        </ListView>

Which gives me the following result:

enter image description here

I’ve asked a similar question on Reddit and it was suggested that I use a ListView instead of custom UserControls. Also from searching the internet I came across a ListView with a GirdView configuration but it’s more for displaying a datatable and not laying out items on a grid.

I also came across ItemControls in my search though I’m not entirely sure how to use it to create a similar layout.

Maybe my whole approach is wrong and there is a better/more correct way to achieve similar results. I’ll apricate any assistance.

EDIT:

After some helpful input from @ASh who pointed me to this question: Grouping data into itemscontrol(s) with template

This helped me to move forward, but alas I’m still not there. If anyone has further ideas on how to improve this it will be really helpful. Maybe it’s possible to place a custom row template in the data grid.

I managed to achieve this layout:
enter image description here

By doing this in the code:

//Added a CollectionViewSource and exposed it as a property
private CollectionViewSource _viewSource;
public object ViewSource { get => _viewSource.View; }

public NavigationViewModel(MessageStore messageStore)
        {
            _fields = new Dictionary<string, ReadOnlyIndicatorWithUnitsViewModel>();

            _fields = new()
            {
                {"Latitude", new ReadOnlyIndicatorWithUnitsViewModel("Latitude","N/A","[SC]","Coordinates")},
                {"Longitude", new ReadOnlyIndicatorWithUnitsViewModel("Longitude","N/A","[SC]","Coordinates")},
                {"Altitude", new ReadOnlyIndicatorWithUnitsViewModel("Altitude","N/A","[SC]","Coordinates")},
                {"Roll", new ReadOnlyIndicatorWithUnitsViewModel("Roll","N/A","[SC]","Coordinates")},
                {"Pitch", new ReadOnlyIndicatorWithUnitsViewModel("Pitch","N/A","[SC]","Coordinates")},
                {"Az.Geo", new ReadOnlyIndicatorWithUnitsViewModel("Az.Geo","N/A","[SC]","Coordinates")},

                {"Roll Rate", new ReadOnlyIndicatorWithUnitsViewModel("Roll","N/A","[rad/s]","Rate")},
                {"Pitch Rate", new ReadOnlyIndicatorWithUnitsViewModel("Pitch","N/A","[rad/s]","Rate")},
                {"Yaw Rate", new ReadOnlyIndicatorWithUnitsViewModel("Yaw","N/A","[rad/s]","Rate")},

                {"Accel X", new ReadOnlyIndicatorWithUnitsViewModel("Acc X","N/A","[m/s^2]","Acceleration")},
                {"Accel Y", new ReadOnlyIndicatorWithUnitsViewModel("Acc Y","N/A","[m/s^2]","Acceleration")},
                {"Accel Z", new ReadOnlyIndicatorWithUnitsViewModel("Acc Z","N/A","[m/s^2]","Acceleration")},

                {"Velocity North", new ReadOnlyIndicatorWithUnitsViewModel("North","N/A","[m/s]","Velocity")},
                {"Velocity East", new ReadOnlyIndicatorWithUnitsViewModel("East","N/A","[m/s]","Velocity")},
                {"Velocity Down", new ReadOnlyIndicatorWithUnitsViewModel("Down","N/A","[m/s]","Velocity")},
            };

            //instantiated CollectionViewSource with the items above
            _viewSource = new CollectionViewSource
            {
                Source = _fields.Values
            };            

            if (_viewSource != null && _viewSource.View.CanGroup == true)
            {
                _viewSource.GroupDescriptions.Clear();
                // IndicatorGroup is the name of the property in the `ReadOnlyIndicatorWithUnitsViewModel` 
                // class that holds the group information of the class
                _viewSource.GroupDescriptions.Add(new PropertyGroupDescription("IndicatorGroup"));
            }
        }

And in the XAML:

<DataGrid Grid.Row="1"
                  Grid.Column="1"
                  AutoGenerateColumns="True"
                   ItemsSource="{Binding ViewSource}">  <!--The property in the ViewModel above -->
            <DataGrid.GroupStyle>
                <GroupStyle>
                    <GroupStyle.ContainerStyle>
                        <Style TargetType="{x:Type GroupItem}">
                            <Setter Property="Margin"
                                    Value="0,0,0,5" />
                            <Setter Property="Template">
                                <Setter.Value>
                                    <ControlTemplate TargetType="{x:Type GroupItem}">
                                        <Expander IsExpanded="True"
                                                  Background="#FF112255"
                                                  BorderBrush="#FF002255"
                                                  Foreground="#FFEEEEEE"
                                                  BorderThickness="1,1,1,5">
                                            <Expander.Header>
                                                <DockPanel>
                                                    <TextBlock FontWeight="Bold"
                                                               Text="{Binding Path=Name}" 
                                                               Margin="5,0,0,0"
                                                               Width="100" /> <!--'Name' property that the textblock binds to referes to the group name. It is a hardcoded property -->
                                                    <TextBlock FontWeight="Bold"
                                                               Text="{Binding Path=ItemCount}" /> <!--Save with ItemCount. It refers to the item count in a certain group -->
                                                </DockPanel>
                                            </Expander.Header>
                                            <Expander.Content>
                                                <ItemsPresenter />
                                            </Expander.Content>
                                        </Expander>
                                    </ControlTemplate>
                                </Setter.Value>
                            </Setter>
                        </Style>
                    </GroupStyle.ContainerStyle>
                </GroupStyle>
            </DataGrid.GroupStyle>
            <DataGrid.RowStyle>
                <Style TargetType="DataGridRow">
                    <Setter Property="Foreground"
                            Value="Black" />
                    <Setter Property="Background"
                            Value="White" />
                </Style>
            </DataGrid.RowStyle>
        </DataGrid>

Leave a Comment