【问题标题】:Highlighting cells in WPF DataGrid when the bound value changes绑定值更改时突出显示 WPF DataGrid 中的单元格
【发布时间】:2012-06-06 21:43:37
【问题描述】:

我有一个 DataGrid,它的数据由后台进程每 15 秒刷新一次。如果任何数据发生变化,我想运行一个动画,以黄色突出显示具有更改值的单元格,然后淡化回白色。我通过执行以下操作使其工作:

我在 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>

然后将其应用于我想在值更改时突出显示的列

<DataGridTextColumn Header="Status" 
    Binding="{Binding Path=Status, NotifyOnTargetUpdated=True}" 
    CellStyle="{StaticResource ChangedCellStyle}" />

如果数据库中状态字段的值发生变化,单元格会像我想要的那样以黄色突出显示。但是,有一些问题。

首先,当最初加载数据网格时,整个列以黄色突出显示。这是有道理的,因为所有值都是第一次加载,因此您会期望 TargetUpdated 触发。我确信有一些方法可以阻止这种情况,但这是一个相对次要的问题。

真正的问题是,如果以任何方式对网格进行排序或过滤,整个列都会以黄色突出显示。我想我不明白为什么排序会导致 TargetUpdated 触发,因为数据没有改变,只是它的显示方式。

所以我的问题是(1)如何在初始加载和排序/过滤时停止这种行为,以及(2)我是否走在正确的轨道上,这甚至是一个好方法吗?我应该提到这是 MVVM。

【问题讨论】:

  • 为您建议的解决方案安排替代解决方案... 1) 您希望列表很大吗? (在这种情况下意味着大 >= 100 项);以及 2) 您是否希望列表中的项目数量经常变化?
  • 这本质上是一个帮助台队列应用程序,它列出交易错误,并允许人们获得特定错误的所有权并将其标记为已解决。这些值不应该经常更改,我希望它们在生产中每天出现的错误少于 100 个。
  • 好的,我发现当它们的内容发生变化时,显示不同的单元格平滑地改变其背景的想法,例如,当任务的状态发生变化或任务的受让人很有趣......,但我对此进行了一些研究,但我找不到仅通过编写 Xaml 就可以做到这一点的方法。我要做的是在内存中编写域对象的集合,每次从服务器检索列表时,实现实用方法,将刚刚检索到的数据与数据网格中已经存在的数据进行比较并执行风格改变了。
  • 您可以尝试将其设为模板列并直接在底层控件上附加绑定(而不是使用列绑定机制)。这就是我能想到的为什么它会触发排序或过滤器。另一种选择可能是检查 ViewModel 上的通知事件,以查看它是否在排序/过滤期间被触发,以及哪些属性可能触发它。
  • @PaulAbbott 你检查过答案了吗?你可以接受还是我需要研究其他方式(因为我发现它是 WPF 最有趣的问题之一)?

标签: wpf mvvm datagrid storyboard


【解决方案1】:

因为TargetUpdated 是真正唯一的基于 UI 更新的事件。更新如何发生并不重要。在对所有 DataGridCells 进行排序时,它们仍保留在它们的位置,只有数据会根据排序结果在其中更改,因此会引发 TargetUpdated。因此我们必须依赖于 WPF 应用程序的数据层。为了实现这一点,我已经重置了 DataGridCell 的绑定,基于一个变量,如果更新发生在数据层。

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>

后面的代码(Window 的 DataContext 对象):

 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);
    }

模型类: 当任何通知正在进行时,SourceUpdating 属性设置为 true(将绑定设置为通过 DataTrigger 通知 TargetUpdate)对于MyClassupdateProperty() 方法中的updateProperty() 方法和更新后通知UISourceUpdating 设置为false(然后将绑定重置为不通过TargetUpdate 通知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;                
        }
    }

}

输出:

两个同时更新/按钮被点击一次:

许多同时更新/按钮被多次点击:

所以更新后,当排序或过滤发生时,绑定知道它不必调用TargetUpdated 事件。仅当源集合的更新正在进行时 绑定被重置以调用TargetUpdated 事件。初始着色问题也由此得到处理。

但是,由于编辑器TextBox 的逻辑仍然存在某种形式,该逻辑基于更复杂的数据类型和 UI 逻辑,代码也将变得更复杂,初始绑定重置整行动画为@987654344为一行的所有单元格引发@。

【讨论】:

    【解决方案2】:

    我对第 (1) 点的想法是在代码中处理这个问题。一种方法是处理 DataGridTextColumn 的 TargetUpdated 事件,并对旧值与新值进行额外检查,并仅在值不同时应用样式,也许另一种方法是创建和删除绑定以编程方式基于代码中的不同事件(如初始加载、刷新等)。

    【讨论】:

      【解决方案3】:

      我建议对视图模型中的每个道具使用 OnPropertyChanged 并更新相关的 UIElement(启动动画或其他),这样您的问题就会得到解决(加载、排序、过滤等),并且用户还可以看到哪个单元格发生了变化!

      【讨论】: