Search code examples
c#wpfdatagrid

C# WPF DataGrid change row format (e.g. background) on value PropertyChange



My goal is to change the row / cell style of a DataGrid, when the underlying value property changed. It is indepented of the value, the DataGrid should only show that it has changed. I have a list of Tournament.cs objects which fires property change events. I added a method which listens to these changes in the MainWindow.cs and should find that object in the DataGrid and change the format of the related row / cell. Unfortunally it doesn't work for me. The method is called and I am able to find the `DataGridRow` object of the Tournament, but changing the format of it e.g. the background has no effect. How can I make this work? Find snippets of my code below.

This is the DataGrid. Each row represents a tournament object and each cell a property, which can be updated externally. Goal is to highlight the updated value.

enter image description here

UPDATE

While creating a minimal example I found, that the problem is the automatic sorting which immediately removes the formatting again. I guess there is no way around having a map etc. to save the updated state and binding somehow. In the example app below you can trigger that behaviour by sorting a column.

MainWindow.cs:

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Media.Animation;
using TurnierChecker.Basic;
using WPFTestProject.Model;

namespace WPFTestProject
{
    /// <summary>
    /// Interaktionslogik für MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {

        public MainWindow()
        {
            InitializeComponent();

            // Initial loading of data.
            this.TournamentList = new SortableBindingList<Tournament>();
            this.TournamentList.ListChanged += this.TournamentListChanged;

            // tie View with ViewModel
            this.TournamentGrid.DataContext = this.TournamentList;
        }

        public SortableBindingList<Tournament> TournamentList { get; private set; }



        /// <summary>
        /// Property change method. Should add a animation / format to the related cell to highlight the change.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="ListChangedEventArgs"/> instance containing the event data.</param>
        private void TournamentListChanged(object sender, ListChangedEventArgs e)
        {

            // Property of tournament change.
            if (e.ListChangedType == ListChangedType.ItemChanged)
            {
                Tournament tmnt = (sender as SortableBindingList<Tournament>)?.ElementAt(e.NewIndex);

                DataGridRow row = this.TournamentGrid.ItemContainerGenerator.ContainerFromItem(tmnt) as DataGridRow;

                switch (e.PropertyDescriptor?.Name)
                {
                    case nameof(Tournament.Date):
                        if (row != null)
                        {
                            DataGridCell cell = this.GetCell(this.TournamentGrid, row, 0);

                            if (cell != null)
                            {
                                cell.Background = Brushes.Orange;
                            }
                        }
                        break;
                    case nameof(Tournament.Series):
                        if (row != null)
                        {
                            DataGridCell cell = this.GetCell(this.TournamentGrid, row, 1);

                            if (cell != null)
                            {
                                cell.Background = Brushes.Orange;

                            }
                        }

                        break;

                    // Default property change.
                    default:
                        break;
                }
            }
        }

        /// <summary>
        /// Gets the visual child.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="parent">The parent.</param>
        /// <returns>The child.</returns>
        private T GetVisualChild<T>(Visual parent)
            where T : Visual
        {
            T child = default(T);
            int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
            for (int i = 0; i < numVisuals; i++)
            {
                Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
                child = v as T;
                if (child == null)
                {
                    child = this.GetVisualChild<T>(v);
                }

                if (child != null)
                {
                    break;
                }
            }

            return child;
        }

        /// <summary>
        /// Gets the cell.
        /// </summary>
        /// <param name="grid">The grid.</param>
        /// <param name="row">The row.</param>
        /// <param name="column">The column.</param>
        /// <returns>The cell.</returns>
        private DataGridCell GetCell(DataGrid grid, DataGridRow row, int column)
        {
            if (row != null)
            {
                DataGridCellsPresenter presenter = this.GetVisualChild<DataGridCellsPresenter>(row);

                if (presenter == null)
                {
                    grid.ScrollIntoView(row, grid.Columns[column]);
                    presenter = this.GetVisualChild<DataGridCellsPresenter>(row);
                }

                DataGridCell cell = (DataGridCell)presenter?.ItemContainerGenerator?.ContainerFromIndex(column);
                return cell;
            }

            return null;
        }

        /// <summary>
        /// Handles the Click event of the AddTournamentButton control. Adds a tournament.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="RoutedEventArgs"/> instance containing the event data.</param>
        private void AddTournamentButton_Click(object sender, RoutedEventArgs e)
        {
            Tournament tmnt = new Tournament();
            tmnt.Date = DateTime.Now;
            tmnt.Series = "Sample Changing Series Name Nr. " + new Random().Next(1, 10);
            this.TournamentList.Add(tmnt);
        }

        /// <summary>
        /// Handles the Click event of the ChangeButton control. Changes a random tournament data. 
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="RoutedEventArgs"/> instance containing the event data.</param>
        private void ChangeButton_Click(object sender, RoutedEventArgs e)
        {

            int cnt = this.TournamentList.Count();

            if (cnt < 1) { return; }


            switch (new Random().Next(0,2))
            {
                case 0:
                    this.TournamentList[new Random().Next(0, cnt - 1)].Date = DateTime.Now;
                    break;
                case 1:
                    this.TournamentList[new Random().Next(0, cnt - 1)].Series = "Sample Changing Series Name Nr. " + new Random().Next(1, 10);
                    break;
            }
           

        }

        /// <summary>
        /// Handles the Click event of the ApplyButton control. Should remove the updated highlight of a cell.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="RoutedEventArgs"/> instance containing the event data.</param>
        private void ApplyButton_Click(object sender, RoutedEventArgs e)
        {
            // How can I remove the applied format / animation from the DataGrid-Cells.
            var rows = GetDataGridRows(this.TournamentGrid);

            foreach (DataGridRow row in rows)
            {
                for (int i = 0; i < this.TournamentGrid.Columns.Count;i++)
                {
                    DataGridCell cell = this.GetCell(this.TournamentGrid, row, i);
                    cell.Background = Brushes.Transparent;
                }

            }
        }

        private IEnumerable<DataGridRow> GetDataGridRows(DataGrid grid)
        {
            var itemsSource = grid.ItemsSource as IEnumerable;
            if (null == itemsSource) yield return null;
            foreach (var item in itemsSource)
            {
                var row = grid.ItemContainerGenerator.ContainerFromItem(item) as DataGridRow;
                if (null != row) yield return row;
            }
        }
    }
}

MainWindow.xaml

<Window x:Class="WPFTestProject.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:WPFTestProject"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <StackPanel Grid.Row="0" FlowDirection="LeftToRight" Orientation="Horizontal">
            <Button x:Name="AddTournamentButton" Grid.Row="0" Content="Add" Width="100" Margin="10,10,10,10" Click="AddTournamentButton_Click" />
            <Button x:Name="ChangeButton" Grid.Row="0" Content="Change" Width="100" Margin="10,10,10,10" Click="ChangeButton_Click" />
            <Button x:Name="ApplyButton" Grid.Row="0" Content="Apply" Width="100" Margin="10,10,10,10" Click="ApplyButton_Click" />
        </StackPanel>
        
        <DataGrid x:Name="TournamentGrid" ItemsSource="{Binding}" Grid.Row="1" AlternationCount="2" IsReadOnly="True" HeadersVisibility="Column" AutoGenerateColumns="False" VerticalScrollBarVisibility="Auto" RowHeight="28" SelectionUnit="FullRow">
            <DataGrid.Columns>
                <!--Tournament Date-->
                <DataGridTextColumn Binding="{Binding Date, StringFormat=\{0: ddd dd.MM.yy HH:mm U\\hr\}, ConverterCulture=de-DE}" Header="Datum" Width="160" MinWidth="170" SortDirection="Ascending"/>
                <!--Tournament Series-->
                <DataGridTextColumn Binding="{Binding Series}" Header="Serie"  Width="410" MinWidth="395"/>
            </DataGrid.Columns>
        </DataGrid>
        
    </Grid>
    
</Window>

Model: Tournament.cs

namespace WPFTestProject.Model
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Runtime.CompilerServices;

    /// <summary>
    /// The tournament model.
    /// </summary>
    public class Tournament : INotifyPropertyChanged
    {
        /// <summary>
        /// The date and time of the tournament.
        /// </summary>
        private DateTime? date;

        /// <summary>
        /// The series  of the tournament.
        /// </summary>
        private string series;


        /// <summary>
        /// Initializes a new instance of the <see cref="Tournament"/> class.
        /// </summary>
        public Tournament()
        {
        }

        /// <summary>
        /// Tritt ein, wenn sich ein Eigenschaftswert ändert.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Gets or sets the date and time of the tournament.
        /// </summary>
        /// <value>
        /// The date and time.
        /// </value>
        public DateTime? Date
        {
            get => this.date;
            set => this.SetField(ref this.date, value);
        }

        /// <summary>
        /// Gets or sets the series of the tournament.
        /// </summary>
        /// <value>
        /// The series.
        /// </value>
        public string Series
        {
            get => this.series;
            set => this.SetField(ref this.series, value);
        }

        /// <summary>
        /// Called when [property changed].
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        private void OnPropertyChanged(string propertyName) => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

        /// <summary>
        /// Sets the field.
        /// </summary>
        /// <typeparam name="T">The property type.</typeparam>
        /// <param name="field">The field.</param>
        /// <param name="value">The value.</param>
        /// <param name="propertyName">Name of the property.</param>
        /// <returns>Returns true if the property was changed.</returns>
        private bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
        {
            if (EqualityComparer<T>.Default.Equals(field, value))
            {
                return false;
            }

            field = value;
            this.OnPropertyChanged(propertyName);
            return true;
        }
    }
}

Basic: SortableBindingList.cs

namespace WPFTestProject.Model
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;

    /// <summary>
    ///  Sortable binding list.
    /// </summary>
    /// <typeparam name="T">The innertype.</typeparam>
    /// <seealso cref="System.ComponentModel.BindingList&lt;T&gt;" />
    public class SortableBindingList<T> : BindingList<T>
    {
        private readonly Dictionary<Type, PropertyComparer<T>> comparers;
        private bool isSorted;
        private ListSortDirection listSortDirection;
        private PropertyDescriptor propertyDescriptor;

        /// <summary>
        /// Initializes a new instance of the <see cref="SortableBindingList{T}"/> class.
        /// </summary>
        public SortableBindingList()
            : base(new List<T>())
        {
            this.comparers = new Dictionary<Type, PropertyComparer<T>>();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="SortableBindingList{T}"/> class.
        /// </summary>
        /// <param name="enumeration">The enumeration.</param>
        public SortableBindingList(IEnumerable<T> enumeration)
            : base(new List<T>(enumeration))
        {
            this.comparers = new Dictionary<Type, PropertyComparer<T>>();
        }

        /// <summary>
        /// Gets a value indicating whether the list supports sorting.
        /// </summary>
        protected override bool SupportsSortingCore
        {
            get { return true; }
        }

        /// <summary>
        /// Gets a value indicating whether the list is sorted.
        /// </summary>
        protected override bool IsSortedCore
        {
            get { return this.isSorted; }
        }

        /// <summary>
        /// Gets the property descriptor that is used for sorting the list if sorting is implemented in a derived class; otherwise, returns <see langword="null" />.
        /// </summary>
        protected override PropertyDescriptor SortPropertyCore
        {
            get { return this.propertyDescriptor; }
        }

        /// <summary>
        /// Gets the direction the list is sorted.
        /// </summary>
        protected override ListSortDirection SortDirectionCore
        {
            get { return this.listSortDirection; }
        }

        /// <summary>
        /// Gets a value indicating whether the list supports searching.
        /// </summary>
        protected override bool SupportsSearchingCore
        {
            get { return true; }
        }

        /// <summary>
        /// Applies the sort core.
        /// </summary>
        /// <param name="property">The property.</param>
        /// <param name="direction">The direction.</param>
        protected override void ApplySortCore(PropertyDescriptor property, ListSortDirection direction)
        {
            var itemsList = (List<T>)this.Items;
            if (this.comparers == null)
            {
                return;
            }

            Type propertyType = property.PropertyType;
            if (!this.comparers.TryGetValue(propertyType, out PropertyComparer<T> comparer))
            {
                comparer = new PropertyComparer<T>(property, direction);
                this.comparers.Add(propertyType, comparer);
            }

            comparer.SetPropertyAndDirection(property, direction);
            itemsList.Sort(comparer);

            this.propertyDescriptor = property;
            this.listSortDirection = direction;
            this.isSorted = true;

            this.OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
        }

        /// <summary>
        /// Removes any sort applied with <see cref="M:System.ComponentModel.BindingList`1.ApplySortCore(System.ComponentModel.PropertyDescriptor,System.ComponentModel.ListSortDirection)" />
        /// if sorting is implemented in a derived class; otherwise, raises <see cref="T:System.NotSupportedException" />.
        /// </summary>
        protected override void RemoveSortCore()
        {
            this.isSorted = false;
            this.propertyDescriptor = base.SortPropertyCore;
            this.listSortDirection = base.SortDirectionCore;

            this.OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
        }

        /// <summary>
        /// Finds the core.
        /// </summary>
        /// <param name="property">The property.</param>
        /// <param name="key">The key.</param>
        /// <returns>The core value.</returns>
        protected override int FindCore(PropertyDescriptor property, object key)
        {
            int count = this.Count;
            for (int i = 0; i < count; ++i)
            {
                T element = this[i];
                if (property?.GetValue(element)?.Equals(key) ?? false)
                {
                    return i;
                }
            }

            return -1;
        }
    }
}

Basic: PropertyComparer.cs

namespace WPFTestProject.Model
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Reflection;

    /// <summary>
    /// Property comparer class.
    /// </summary>
    /// <typeparam name="T">The type.</typeparam>
    /// <seealso cref="System.Collections.Generic.IComparer&lt;T&gt;" />
    public class PropertyComparer<T> : IComparer<T>
    {
        private readonly IComparer comparer;
        private PropertyDescriptor propertyDescriptor;
        private int reverse;

        /// <summary>
        /// Initializes a new instance of the <see cref="PropertyComparer{T}"/> class.
        /// </summary>
        /// <param name="property">The property.</param>
        /// <param name="direction">The direction.</param>
        public PropertyComparer(PropertyDescriptor property, ListSortDirection direction)
        {
            this.propertyDescriptor = property;
            Type comparerForPropertyType = typeof(Comparer<>).MakeGenericType(property.PropertyType);
            this.comparer = comparerForPropertyType.InvokeMember("Default", BindingFlags.Static | BindingFlags.GetProperty | BindingFlags.Public, null, null, null) as IComparer;
            this.SetListSortDirection(direction);
        }

        /// <summary>
        /// Compares two objects and returns a value indicating whether one is less than, equal to, or greater than the other.
        /// </summary>
        /// <param name="x">The first object to compare.</param>
        /// <param name="y">The second object to compare.</param>
        /// <returns>
        /// A signed integer that indicates the relative values of <paramref name="x" /> and <paramref name="y" />, as shown in the following table.
        /// <list type="table"><listheader><term> Value</term><description> Meaning</description></listheader><item><term> Less than zero</term><description><paramref name="x" /> is less than <paramref name="y" />.</description></item><item><term> Zero</term><description><paramref name="x" /> equals <paramref name="y" />.</description></item><item><term> Greater than zero</term><description><paramref name="x" /> is greater than <paramref name="y" />.</description></item></list>
        /// </returns>
        public int Compare(T x, T y)
        {
            if (this.comparer == null)
            {
                return this.reverse;
            }
            else
            {
                return this.reverse * this.comparer.Compare(this.propertyDescriptor.GetValue(x), this.propertyDescriptor.GetValue(y));
            }
        }

        /// <summary>
        /// Sets the property and direction.
        /// </summary>
        /// <param name="descriptor">The descriptor.</param>
        /// <param name="direction">The direction.</param>
        public void SetPropertyAndDirection(PropertyDescriptor descriptor, ListSortDirection direction)
        {
            this.SetPropertyDescriptor(descriptor);
            this.SetListSortDirection(direction);
        }

        private void SetPropertyDescriptor(PropertyDescriptor descriptor)
        {
            this.propertyDescriptor = descriptor;
        }

        private void SetListSortDirection(ListSortDirection direction)
        {
            this.reverse = direction == ListSortDirection.Ascending ? 1 : -1;
        }
    }
}

Solution

  • My solution is based on BionicCodes answer: I added a HashSet to my Tournament object which holds the namens of the changed properties. Below is my solution:

    MainWindow.xaml

    <Window x:Class="WPFTestProject.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:conv="clr-namespace:WPFTestProject.Converter"
            mc:Ignorable="d"
            Title="MainWindow" Height="450" Width="800">
    
        <Window.Resources>
            <conv:PropertiesUpdatesSetToBoolConverter x:Key="PropertiesUpdatesSetToBoolConverter" />
            <Style TargetType="{x:Type DataGridCell}">
                <Style.Triggers>
                    <DataTrigger Value="True">
                        <DataTrigger.Binding>
                            <MultiBinding Converter="{StaticResource PropertiesUpdatesSetToBoolConverter}">
                                <Binding Path="Column.Binding.Path.Path" RelativeSource="{RelativeSource Self}" />
                                <Binding Path="PropertiesUpdatedSet" />
                            </MultiBinding>
                        </DataTrigger.Binding>
                        <DataTrigger.EnterActions >
                            <BeginStoryboard Name="BlinkingAnimation">
                                <Storyboard>
                                    <ColorAnimation Duration="00:00:03" RepeatBehavior="Forever" AutoReverse="True" Storyboard.TargetProperty="(DataGridCell.Foreground).(SolidColorBrush.Color)" From="Red" To="Black" />
                                </Storyboard>
                            </BeginStoryboard>
                        </DataTrigger.EnterActions>
                        <DataTrigger.ExitActions>
                            <StopStoryboard BeginStoryboardName="BlinkingAnimation"/>
                        </DataTrigger.ExitActions>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </Window.Resources>
        
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            <StackPanel Grid.Row="0" FlowDirection="LeftToRight" Orientation="Horizontal">
                <Button x:Name="AddTournamentButton" Grid.Row="0" Content="Add" Width="100" Margin="10,10,10,10" Click="AddTournamentButton_Click" />
                <Button x:Name="ChangeButton" Grid.Row="0" Content="Change" Width="100" Margin="10,10,10,10" Click="ChangeButton_Click" />
                <Button x:Name="ApplyButton" Grid.Row="0" Content="Apply" Width="100" Margin="10,10,10,10" Click="ApplyButton_Click" />
            </StackPanel>
            
            <DataGrid x:Name="TournamentGrid" ItemsSource="{Binding}" Grid.Row="1" AlternationCount="2" IsReadOnly="True" HeadersVisibility="Column" AutoGenerateColumns="False" VerticalScrollBarVisibility="Auto" RowHeight="28" SelectionUnit="FullRow">
                <DataGrid.Columns>
                    <!--Tournament Date-->
                    <DataGridTextColumn Binding="{Binding Date, StringFormat=\{0: ddd dd.MM.yy HH:mm U\\hr\}, ConverterCulture=de-DE}" Header="Datum" Width="160" MinWidth="170" SortDirection="Ascending"/>
                    <!--Tournament Series-->
                    <DataGridTextColumn Binding="{Binding Series}" Header="Serie"  Width="410" MinWidth="395" />
                </DataGrid.Columns>
            </DataGrid>
        </Grid>
        
    </Window>
    

    MainWindow.xaml.cs

    using System;
    using System.Linq;
    using System.Windows;
    using WPFTestProject.Model;
    
    namespace WPFTestProject
    {
        /// <summary>
        /// Interaktionslogik für MainWindow.xaml
        /// </summary>
        public partial class MainWindow : Window
        {
    
            public MainWindow()
            {
    
                InitializeComponent();
    
    
                // Initial loading of data.
                this.TournamentList = new SortableBindingList<Tournament>();
    
                // tie View with ViewModel
                this.TournamentGrid.DataContext = this.TournamentList;
            }
    
            public SortableBindingList<Tournament> TournamentList { get; private set; }
    
            /// <summary>
            /// Handles the Click event of the AddTournamentButton control. Adds a tournament.
            /// </summary>
            /// <param name="sender">The source of the event.</param>
            /// <param name="e">The <see cref="RoutedEventArgs"/> instance containing the event data.</param>
            private void AddTournamentButton_Click(object sender, RoutedEventArgs e)
            {
                Tournament tmnt = new Tournament();
                tmnt.Date = DateTime.Now;
                tmnt.Series = "Sample Changing Series Name Nr. " + new Random().Next(1, 10);
                this.TournamentList.Add(tmnt);
            }
    
            /// <summary>
            /// Handles the Click event of the ChangeButton control. Changes a random tournament data. 
            /// </summary>
            /// <param name="sender">The source of the event.</param>
            /// <param name="e">The <see cref="RoutedEventArgs"/> instance containing the event data.</param>
            private void ChangeButton_Click(object sender, RoutedEventArgs e)
            {
    
                int cnt = this.TournamentList.Count();
    
                if (cnt < 1) { return; }
    
    
                switch (new Random().Next(0,2))
                {
                    case 0:
                        this.TournamentList[new Random().Next(0, cnt - 1)].Date = DateTime.Now;
                        break;
                    case 1:
                        this.TournamentList[new Random().Next(0, cnt - 1)].Series = "Sample Changing Series Name Nr. " + new Random().Next(1, 10);
                        break;
                }
               
    
            }
    
            /// <summary>
            /// Handles the Click event of the ApplyButton control. Should remove the updated highlight of a cell.
            /// </summary>
            /// <param name="sender">The source of the event.</param>
            /// <param name="e">The <see cref="RoutedEventArgs"/> instance containing the event data.</param>
            private void ApplyButton_Click(object sender, RoutedEventArgs e)
            {
              foreach (Tournament tmnt in this.TournamentList)
                {
                    tmnt.PropertiesUpdatedSet.Clear();
                    tmnt.OnPropertyChanged(nameof(Tournament.PropertiesUpdatedSet));
                }
            }
        }
    }
    

    Model: Tournament.cs

    namespace WPFTestProject.Model
    {
        using System;
        using System.Collections.Generic;
        using System.Collections.ObjectModel;
        using System.ComponentModel;
        using System.Runtime.CompilerServices;
    
        /// <summary>
        /// The tournament model.
        /// </summary>
        public class Tournament : INotifyPropertyChanged
        {
            /// <summary>
            /// The date and time of the tournament.
            /// </summary>
            private DateTime? date;
    
            /// <summary>
            /// The series  of the tournament.
            /// </summary>
            private string series;
    
    
            /// <summary>
            /// Initializes a new instance of the <see cref="Tournament"/> class.
            /// </summary>
            public Tournament()
            {
                PropertiesUpdatedSet = new HashSet<string>();
            }
    
            /// <summary>
            /// Tritt ein, wenn sich ein Eigenschaftswert ändert.
            /// </summary>
            public event PropertyChangedEventHandler PropertyChanged;
    
            /// <summary>
            /// Gets or sets the date and time of the tournament.
            /// </summary>
            /// <value>
            /// The date and time.
            /// </value>
            public DateTime? Date
            {
                get => this.date;
                set => this.SetField(ref this.date, value);
            }
    
            /// <summary>
            /// Gets or sets the series of the tournament.
            /// </summary>
            /// <value>
            /// The series.
            /// </value>
            public string Series
            {
                get => this.series;
                set => this.SetField(ref this.series, value);
            }
    
            public HashSet<string> PropertiesUpdatedSet {  get; }
    
    
            /// <summary>
            /// Called when [property changed].
            /// </summary>
            /// <param name="propertyName">Name of the property.</param>
            public void OnPropertyChanged(string propertyName) => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    
            /// <summary>
            /// Sets the field.
            /// </summary>
            /// <typeparam name="T">The property type.</typeparam>
            /// <param name="field">The field.</param>
            /// <param name="value">The value.</param>
            /// <param name="propertyName">Name of the property.</param>
            /// <returns>Returns true if the property was changed.</returns>
            private bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
            {
                if (EqualityComparer<T>.Default.Equals(field, value))
                {
                    return false;
                }
    
                field = value;
                this.OnPropertyChanged(propertyName);
    
                if (!PropertiesUpdatedSet.Contains(propertyName))
                {
                    PropertiesUpdatedSet.Add(propertyName);
                    this.OnPropertyChanged(nameof(PropertiesUpdatedSet));
                }
    
    
                return true;
            }
        }
    }
    

    Converter: PropertiesUpdatesSetToBoolConverter.cs

    namespace WPFTestProject.Converter
    {
        using System;
        using System.Collections.Generic;
        using System.Globalization;
        using System.Windows.Data;
    
        /// <summary>
        /// Converts a string value to visibility property.
        /// </summary>
        /// <seealso cref="IMultiValueConverter" />
        public class PropertiesUpdatesSetToBoolConverter : IMultiValueConverter
        {
            /// <summary>
            /// Converts a string value to visibility property.
            /// </summary>
            /// <param name="value">The value produced by the binding source.</param>
            /// <param name="targetType">The type of the binding target property.</param>
            /// <param name="parameter">The converter parameter to use.</param>
            /// <param name="culture">The culture to use in the converter.</param>
            /// <returns>
            /// A converted value. If the method returns <see langword="null" />, the valid null value is used.
            /// </returns>
            public object Convert(object[] value, Type targetType, object parameter, CultureInfo culture)
            {
                if (!(value[1] is HashSet<string> flags))
                {
                    return false;
                }
    
                if (string.IsNullOrEmpty(value[0]?.ToString())) {
                    return false;
                }
    
                bool result = flags.Contains(value[0].ToString());
                return flags.Contains(value[0].ToString());
            }
    
            /// <summary>
            /// Converts a value.
            /// </summary>
            /// <param name="value">The value that is produced by the binding target.</param>
            /// <param name="targetType">The type to convert to.</param>
            /// <param name="parameter">The converter parameter to use.</param>
            /// <param name="culture">The culture to use in the converter.</param>
            /// <returns>
            /// A converted value. If the method returns <see langword="null" />, the valid null value is used.
            /// </returns>
            /// <exception cref="NotImplementedException">Not implemented</exception>
            public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
            {
                throw new NotImplementedException();
            }
        }
    }