Search code examples
wpftemplatesdatagridstylesdatagridcell

WPF DataGrid Dynamic width cell highlighting


I have an application that's using the WPF Data Grid. That grid presents a set of test results. If the result of a test is out side the min and max allowed values I want to highlight that cell in red. I currently have it working, but am not quite happy with the highlighting.

Here's what it looks like currently:

enter image description here

Here's the desired look (via some image twiddling):

enter image description here

Notice the highlighting in the first example consumes the entire cell width. I'm hoping for the desired example where it only consumes as much space as the widest result with a little margin on both sides. Keep in mind, a result in any one cell could range between 0 and 1920K from one sample to the next. This is an edge case, but I want the highlighted area to grow and shrink as a result.

Just FYI, these results are updated on a configurable timer that triggers anywhere between 10 ms and 10 seconds depending on the user configuration.

Below is the code that generates the first example (sorry for the large amount of code). The interesting bits are DataGridCellStyle, ResultCellStyle and CellTemplate

The XAML

<Window x:Class="StackOverflow_HighlightCell.MainWindow"
    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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:StackOverflow_HighlightCell"
    mc:Ignorable="d"
    Loaded="Window_Loaded"
    Title="MainWindow" Height="350" Width="525">
<Window.Resources>
    <Style TargetType="DataGridColumnHeader">
        <Setter Property="Background" Value="Transparent" />
        <Setter Property="Foreground" Value="White" />
        <Setter Property="BorderThickness" Value="0" />
        <Setter Property="HorizontalContentAlignment" Value="Center" />
    </Style>

    <ControlTemplate x:Key="CellTemplate" TargetType="{x:Type DataGridCell}">
        <Border Background="{TemplateBinding Background}">
            <ContentPresenter Margin="12,0,0,0" />
        </Border>
    </ControlTemplate>

    <Style x:Key="DataGridCellStyle" TargetType="DataGridCell">
        <Setter Property="Background" Value="#707070" />
        <Setter Property="BorderThickness" Value="0" />
        <Setter Property="HorizontalContentAlignment" Value="Left" />
        <Style.Triggers>
            <Trigger Property="IsSelected" Value="True">
                <Setter Property="Foreground" Value="#CCCCCC" />
            </Trigger>
        </Style.Triggers>
    </Style>

    <Style x:Key="ResultCellStyle" TargetType="DataGridCell" 
           BasedOn="{StaticResource DataGridCellStyle}">
        <Setter Property="Template" Value="{StaticResource CellTemplate}" />
        <Style.Triggers>
            <DataTrigger Binding="{Binding Path=IsResultOutOfBounds, 
                                           StringFormat={}{0:0.00}}" 
                                           Value="True">
                <Setter Property="Background" Value="Red" />
                <Setter Property="Foreground" Value="White" />
            </DataTrigger>
        </Style.Triggers>
    </Style>

    <Style TargetType="DataGrid" BasedOn="{x:Null}">
        <Setter Property="RowBackground" Value="#707070" />
        <Setter Property="AutoGenerateColumns" Value="False" />
        <Setter Property="IsReadOnly" Value="True" />
        <Setter Property="Background" Value="#666666" />
        <Setter Property="GridLinesVisibility" Value="None" />
        <Setter Property="BorderBrush" Value="Transparent" />
        <Setter Property="BorderThickness" Value="0" />
        <Setter Property="CanUserSortColumns" Value="False" />
        <Setter Property="HeadersVisibility" Value="Column" />
        <Setter Property="HorizontalAlignment" Value="Stretch" />
        <Setter Property="Foreground" Value="#CCCCCC" />
        <Setter Property="RowDetailsVisibilityMode" Value="Collapsed" />

        <Setter Property="CellStyle" Value="{StaticResource DataGridCellStyle}" />
    </Style>

    <!-- A left justified DataGridTextColumn -->
    <Style x:Key="ElementLeftJustified">
        <Setter Property="TextBlock.HorizontalAlignment" Value="Left" />
        <Setter Property="TextBlock.Margin" Value="15,0,5,0" />
    </Style>

    <!-- A right justified DataGridTextColumn -->
    <Style x:Key="ElementRightJustified">
        <Setter Property="TextBlock.HorizontalAlignment" Value="Right" />
        <Setter Property="TextBlock.Margin" Value="0,0,5,0" />
    </Style>

</Window.Resources>

<Grid Background="#FF666666">
    <Border Margin="20" >
        <DataGrid x:Name="_testSummaryGrid"
              ItemsSource="{Binding TestResults}">

            <DataGrid.Columns>
                <DataGridTextColumn 
                    MinWidth="75" Header="Test"
                    Binding="{Binding TestName}" 
                    ElementStyle="{StaticResource ElementLeftJustified}" />

                <DataGridTextColumn 
                    MinWidth="75" Header="Min" 
                    Binding="{Binding Min, StringFormat={}{0:0.00}}" 
                    ElementStyle="{StaticResource ElementRightJustified}" />


                <DataGridTextColumn 
                    MinWidth="75" Header="Result" 
                    Binding="{Binding Result, StringFormat={}{0:0.00}}" 
                    ElementStyle="{StaticResource ElementRightJustified}" 
                    CellStyle="{StaticResource ResultCellStyle}" />

                <DataGridTextColumn 
                    MinWidth="75" Header="Max" 
                    Binding="{Binding Max, StringFormat={}{0:0.00}}" 
                    ElementStyle="{StaticResource ElementRightJustified}" />
            </DataGrid.Columns>
        </DataGrid>
    </Border>

</Grid>
</Window>

The View Model

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using System.Collections.ObjectModel;

namespace StackOverflow_HighlightCell
{
    public class ResultsViewModel : ViewModelBase
    {
        public ResultsViewModel()
        {
            TestResults = new ObservableCollection<TestResult>
            {
            { new TestResult { TestGroup = "Circle",TestName = "Radius",
                               Min = 100, Max = 153, Result = 150} },
            { new TestResult { TestGroup = "Circle", TestName = "Min Radius",
                               Min = 0, Max = 90, Result = 97.59 } },
            // And so on ...
            };
        }

        public ObservableCollection<TestResult> TestResults { get; set; }

    }

    public class TestResult : ViewModelBase
    {
        public string TestGroup { get; set; }
        public string TestName { get; set; }
        public double Result { get; set; }
        public double Min { get; set; }
        public double Max { get; set; }
        public bool IsResultOutOfBounds { get { return !(Result >= Min && Result <= Max); } }

    }

    public class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private void FirePropertyChanged(string property)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
        }
    }
}

Solution

  • I think this should do what you want:

    <ControlTemplate x:Key="ResultCellTemplate" TargetType="{x:Type DataGridCell}">
        <Border Background="{TemplateBinding Background}">
            <Grid 
                Margin="12,0,0,0"
                HorizontalAlignment="Right"
                >
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" SharedSizeGroup="Result" />
                </Grid.ColumnDefinitions>
                <Border Grid.Column="0" x:Name="ContentPresenterBorder">
                    <ContentPresenter 
                        />
                </Border>
            </Grid>
        </Border>
    
        <ControlTemplate.Triggers>
            <!-- 
            That stringformat you had will have been ignored because the target
            type isn't string. 
            -->
            <DataTrigger Binding="{Binding IsResultOutOfBounds}" Value="True">
                <Setter TargetName="ContentPresenterBorder" Property="Background" Value="Red" />
                <Setter TargetName="ContentPresenterBorder" Property="TextElement.Foreground" Value="White" />
            </DataTrigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>
    
    <Style x:Key="ResultCellStyle" TargetType="DataGridCell" 
        BasedOn="{StaticResource DataGridCellStyle}">
        <Setter Property="Template" Value="{StaticResource ResultCellTemplate}" />
    </Style>
    

    ...but don't forget to set Grid.IsSharedSizeScope="True" on the DataGrid. That works with SharedSizeGroup="Result" on the ColumnDefinition to ensure that any grid column named "Result" anywhere within the DataGrid will be dynamically sized to the same width.

    <DataGrid 
        x:Name="_testSummaryGrid"
        ItemsSource="{Binding TestResults}"
        Grid.IsSharedSizeScope="True"
        >
    

    Excellent example by the way. Would've been better trimmed down to just two DataGrid columns, but I pasted it in, pressed F5, and it worked. And the important part wasn't hard to find.