Highlighting cells in WPF DataGrid when the bound value changes

datagrid, mvvm, storyboard, wpf

Solution

Since `TargetUpdated` is truly only UI update based event. It doesn't matter how update is happening. While sorting all the `DataGridCells` remain at their places only data is changed in them according to sorting result hence `TargetUpdated`is raised. hence we have to be dependent on data layer of WPF app. To achieve this I've reset the Binding of `DataGridCell` based on a variable that kind of trace if update is happening at data layer.

XAML:

<Window.Resources>
    <Style x:Key="ChangedCellStyle" TargetType="DataGridCell">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="DataGridCell">
                    <ControlTemplate.Triggers>
                        <EventTrigger RoutedEvent="Binding.TargetUpdated">
                            <BeginStoryboard>
                                <Storyboard>
                                    <ColorAnimation Duration="00:00:04" Storyboard.TargetName="myTxt"
                                        Storyboard.TargetProperty="(DataGridCell.Background).(SolidColorBrush.Color)" 
                                        From="Red" To="Transparent" />
                                </Storyboard>
                            </BeginStoryboard>
                        </EventTrigger>                           
                    </ControlTemplate.Triggers>

                    <TextBox HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Transparent"
                             Name="myTxt" >
                        <TextBox.Style>
                            <Style TargetType="TextBox">
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},Path=DataContext.SourceUpdating}" Value="True">
                                        <Setter Property="Text" Value="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},Path=Content.Text,NotifyOnSourceUpdated=True,NotifyOnTargetUpdated=True}" />
                                    </DataTrigger>
                                    <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},Path=DataContext.SourceUpdating}" Value="False">
                                        <Setter Property="Text" Value="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},Path=Content.Text}" />                                            
                                    </DataTrigger>                                       
                                </Style.Triggers>                                    
                            </Style>
                        </TextBox.Style>
                    </TextBox>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</Window.Resources>

<StackPanel Orientation="Vertical">
    <DataGrid ItemsSource="{Binding list}" CellStyle="{StaticResource ChangedCellStyle}" AutoGenerateColumns="False"
              Name="myGrid"  >
        <DataGrid.Columns>
            <DataGridTextColumn Header="Name" Binding="{Binding Name}" />
            <DataGridTextColumn Header="ID" Binding="{Binding Id}" />
        </DataGrid.Columns>
    </DataGrid>
    <Button Content="Change Values" Click="Button_Click" />
</StackPanel>

Code Behind(DataContext object of Window):

 public MainWindow()
    {
        list = new ObservableCollection<MyClass>();
        list.Add(new MyClass() { Id = 1, Name = "aa" });
        list.Add(new MyClass() { Id = 2, Name = "bb" });
        list.Add(new MyClass() { Id = 3, Name = "cc" });
        list.Add(new MyClass() { Id = 4, Name = "dd" });
        list.Add(new MyClass() { Id = 5, Name = "ee" });
        list.Add(new MyClass() { Id = 6, Name = "ff" });   
        InitializeComponent();
    }

    private ObservableCollection<MyClass> _list;
    public ObservableCollection<MyClass> list
    {
        get{ return _list; }
        set{   
            _list = value;
            updateProperty("list");
        }
    }
   
    Random r = new Random(0);
    private void Button_Click(object sender, RoutedEventArgs e)
    {

        int id = (int)r.Next(6);
        list[id].Id += 1;
        int name = (int)r.Next(6);
        list[name].Name = "update " + r.Next(20000);
    }

Model Class: `SourceUpdating` property is set to true(which set the binding to notify `TargetUpdate` via a `DataTrigger`) when any notification is in progress for `MyClass` in `updateProperty()` method and after update is notified to `UI`, `SourceUpdating` is set to false(which then reset the binding to not notify `TargetUpdate` via a `DataTrigger`).

public class MyClass : INotifyPropertyChanged
{
    private string name;
    public string Name
    {
        get { return name; }
        set { 
            name = value;updateProperty("Name");
        }
    }

    private int id;
    public int Id
    {
        get { return id; }
        set 
        { 
            id = value;updateProperty("Id");
        }
    }

    //the vaiable must set to ture when update in this calss is ion progress
    private bool sourceUpdating;
    public bool SourceUpdating
    {
        get { return sourceUpdating; }
        set 
        { 
            sourceUpdating = value;updateProperty("SourceUpdating");
        }
    }        

    public event PropertyChangedEventHandler PropertyChanged;
    public void updateProperty(string name)
    {
        if (name == "SourceUpdating")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(name));
            }
        }
        else
        {
            SourceUpdating = true;               
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(name));
            }               
           SourceUpdating = false;                
        }
    }

}

Outputs:

Two simultaneous Updates/ Button is clicked once :

Many simultaneous Updates/ Button is clicked many times :

SO after update, when sorting or filtering is happening the bindings know that it doesn't have to invoke the `TargetUpdated` event. Only when the update of source collection is in progress the binding is reset to invoke the `TargetUpdated` event. Also initial coloring problem is also get handled by this.

However as the logic still has some sort comings as for editor `TextBox` the logic is based on with more complexity of data types and UI logic the code will become more complex also for initial binding reset whole row is animated as `TargetUpdated` is raised for all cells of a row.

Problem

I have a DataGrid that has its data refreshed by a background process every 15 seconds. If any of the data changes, I want to run an animation that highlights the cell with the changed value in yellow and then fade back to white. I sort-of have it working by doing the following: I created a style with event trigger on Binding.TargetUpdated ``` <Style x:Key="ChangedCellStyle" TargetType="DataGridCell"> <Style.Triggers> <EventTrigger RoutedEvent="Binding.TargetUpdated"> <BeginStoryboard> <Storyboard> <ColorAnimation Duration="00:00:15" Storyboard.TargetProperty= "(DataGridCell.Background).(SolidColorBrush.Color)" From="Yellow" To="Transparent" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Style.Triggers> </Style> ``` And then applied it to the columns I wanted to highlight if a value changes ``` <DataGridTextColumn Header="Status" Binding="{Binding Path=Status, NotifyOnTargetUpdated=True}" CellStyle="{StaticResource ChangedCellStyle}" /> ``` If the value for the status field in the database changes, the cell highlights in yellow just like I want. But, there are a few problems. First, when the data grid is initially loaded, the entire column is highlighted in yellow. This makes sense, because all of the values are being loaded for the first time so you would expect TargetUpdated to fire. I'm sure there is some way I can stop this, but it's a relatively minor point. The real problem is the entire column is highlighted in yellow if the grid is sorted or filtered in any way. I guess I don't understand why a sort would cause TargetUpdated to fire since the data didn't change, just the way it is displayed. So my question is (1) how can I stop this behavior on initial load and sort/filter, and (2) am I on the right track and is this even a good way to do this? I should mention this is MVVM.

Original source