WPF:CollectionChanges 时绑定中断 - 为什么?

WPF: Bindings break when CollectionChanges - why?

首先,对于这篇文章的篇幅,我深表歉意post - 我知道它很长,但我认为对于这篇文章,多点细节总比少点好。

我现在想要实现的是数据网格的总计页脚行。由于它需要显示在底行,我采用的方法是添加一些与数据网格中的列对齐的 TextBlock。

我的应用程序在 ItemsControl 中有多个数据网格,所以我还没有找到一种只设置绑定的好方法。使用 RelativeSource 似乎不是一个选项,因为没有办法(据我所知)将它指向后代元素,然后搜索特定的子元素。所以我写了一些 hackery 代码来做我想做的事。

好的,现在问题来了.. 应用程序启动时一切看起来都很好,但是一旦网格中的任何项目发生变化,宽度绑定似乎就会完全中断。我写了一个小测试应用程序来展示我的意思。这是一些屏幕截图:

现在,如果我单击按钮更改元素,页脚文本块的宽度绑定会中断:

对于此行为的原因,我完全感到困惑。我是 WPF 的新手,只是在构建我的第一个应用程序时摸索着自己的方式。因此,如果这是一种愚蠢的做事方式,请告诉我。这是我的代码。

MainWindow.xaml:

<Window x:Class="WpfTestApp.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:WpfTestApp"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel Orientation="Vertical">
        <ItemsControl x:Name="BarsItemsControl" ItemsSource="{Binding Bars}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Vertical">
                        <TextBlock Text="{Binding Description}" />
                        <DataGrid x:Name="FooGrid" 
                              ItemsSource="{Binding Foos}" 
                              IsSynchronizedWithCurrentItem="False" 
                              AutoGenerateColumns="False" 
                              SelectionUnit="Cell" 
                              SelectionMode="Extended" 
                              CanUserReorderColumns="False"
                              CanUserAddRows="True"
                              HeadersVisibility="Column">
                            
                            <DataGrid.Columns>
                                <DataGridTextColumn Header="Col 1" Width="*" Binding="{Binding Value1}" />
                                <DataGridTextColumn Header="Col 2" Width="*" Binding="{Binding Value2}" />
                                <DataGridTextColumn Header="Col 3" Width="*" Binding="{Binding Value3}" />
                            </DataGrid.Columns>
                        </DataGrid>
                        <StackPanel x:Name="TotalsRow" Orientation="Horizontal">
                            <TextBlock Text="{Binding Totals[0]}" />
                            <TextBlock Text="{Binding Totals[1]}" />
                            <TextBlock Text="{Binding Totals[2]}" />
                        </StackPanel>
                    </StackPanel>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
        <Button Content="Change something" Click="Button_Click" />
    </StackPanel>
</Window>

MainWindow.xaml.cs

using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;


namespace WpfTestApp
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public IList<Bar> Bars { get; }

        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = this;
            this.Bars = new ObservableItemsCollection<Bar>();
            var foos = new ObservableItemsCollection<Foo>();
            for (int i = 0; i < 5; i++)
            {
                foos.Add(new Foo()
                {
                    Value1 = 14.23,
                    Value2 = 53.23,
                    Value3 = 35.23
                });
            }

            var foos2 = new ObservableItemsCollection<Foo>();
            for (int i = 0; i < 5; i++)
            {
                foos2.Add(new Foo()
                {
                    Value1 = 14.23,
                    Value2 = 53.23,
                    Value3 = 35.23
                });
            }

            this.Bars.Add(new Bar(foos) 
            { 
                Description = "Bar 1",
            });

            this.Bars.Add(new Bar(foos2)
            {
                Description = "Bar 2",
            });

            this.Loaded += MainWindow_Loaded;
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            // Bind widths of the TotalsRow textblocks (footers) to the width of the 
            // datagrid column they're associated with
            var elements = new List<FrameworkElement>();
            this.GetChildElementsByName(this.BarsItemsControl, "FooGrid", ref elements);
            foreach (var element in elements)
            {
                var dataGrid = element as DataGrid;
                if (dataGrid != null)
                {
                    var totalsRowList = new List<FrameworkElement>();
                    this.GetChildElementsByName(VisualTreeHelper.GetParent(dataGrid), "TotalsRow", ref totalsRowList);
                    if (totalsRowList.Count > 0)
                    {
                        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(totalsRowList[0]); i++)
                        {
                            var textBlock = VisualTreeHelper.GetChild(totalsRowList[0], i) as TextBlock;
                            Binding widthBinding = new Binding();
                            widthBinding.Source = dataGrid.Columns[i];
                            widthBinding.Path = new PropertyPath("ActualWidth");
                            widthBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
                            BindingOperations.SetBinding(textBlock, TextBlock.WidthProperty, widthBinding);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Populate a list of elements in the visual tree with the given name under the given parent
        /// </summary>
        public void GetChildElementsByName(DependencyObject parent, string name, ref List<FrameworkElement> elements)
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
            {
                var child = VisualTreeHelper.GetChild(parent, i);
                var element = child as FrameworkElement;
                if (element != null && element.Name == name)
                {
                    elements.Add(element);
                }
                GetChildElementsByName(child, name, ref elements);
            }
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            this.Bars[0].Foos[3].Value1 = 10;
        }
    }
}

Foo.cs

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace WpfTestApp
{
    public class Foo : INotifyPropertyChanged
    {
        private double value1;
        public double Value1 {
            get { return value1; }
            set { value1 = value; OnPropertyChanged(); }
        }
        private double value2;
        public double Value2
        {
            get { return value2; }
            set { value2 = value; OnPropertyChanged(); }
        }
        private double value3;
        public double Value3
        {
            get { return value3; }
            set { value3 = value; OnPropertyChanged(); }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged([CallerMemberName] string name = null)
        {
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
    }
}

Bar.cs

using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;


namespace WpfTestApp
{
    public class Bar : INotifyPropertyChanged
    {
        public Bar(ObservableItemsCollection<Foo> foos)
        {
            this.Foos = foos;
            this.Totals = new double[3] { 14, 14, 14};
            this.Foos.CollectionChanged += Foos_CollectionChanged;
        }

        private void Foos_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            //var fooList = this.Categories.Cast<CategoryViewModel>();
            this.Totals[0] = this.Foos.Sum(f => f.Value1);
            this.Totals[1] = this.Foos.Sum(f => f.Value2);
            this.Totals[2] = this.Foos.Sum(f => f.Value3);
            OnPropertyChanged(nameof(Totals));
        }

        public string Description { get; set; }
        public ObservableItemsCollection<Foo> Foos { get; }

        public double[] Totals { get; }

        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged([CallerMemberName] string name = null)
        {
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
    }
}

ObservableItemsCollection.cs

using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;


namespace WpfTestApp
{
    public class ObservableItemsCollection<T> : ObservableCollection<T>
        where T : INotifyPropertyChanged
    {
        private void Handle(object sender, PropertyChangedEventArgs args)
        {
            base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset, null));
        }

        protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            if (e.NewItems != null)
            {
                foreach (object t in e.NewItems)
                {
                    ((T)t).PropertyChanged += Handle;
                }
            }
            if (e.OldItems != null)
            {
                foreach (object t in e.OldItems)
                {
                    ((T)t).PropertyChanged -= Handle;
                }
            }
            base.OnCollectionChanged(e);
        }
    }
}

将 xaml 来源中的最后一个堆栈面板更改为网格:

   <Grid x:Name="TotalsRow">
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*"/>
      <ColumnDefinition Width="*"/>
      <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
       <TextBlock Text="{Binding Totals[0]}" Grid.Column="0"/>
       <TextBlock Text="{Binding Totals[1]}" Grid.Column="1"/>
       <TextBlock Text="{Binding Totals[2]}" Grid.Column="2"/>
  </Grid>

结果:

只需绑定到列的实际宽度:

<Window x:Class="WpfTestApp.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:WpfTestApp"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800">
<StackPanel Orientation="Vertical">
    <ItemsControl x:Name="BarsItemsControl" ItemsSource="{Binding Bars}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Vertical">
                    <TextBlock Text="{Binding Description}" />
                    <DataGrid x:Name="FooGrid" 
                          ItemsSource="{Binding Foos}" 
                          IsSynchronizedWithCurrentItem="False" 
                          AutoGenerateColumns="False" 
                          SelectionUnit="Cell" 
                          SelectionMode="Extended" 
                          CanUserReorderColumns="False"
                          CanUserAddRows="True"
                          HeadersVisibility="Column">

                        <DataGrid.Columns>
                            <DataGridTextColumn x:Name="col1" Header="Col 1" Width="*" Binding="{Binding Value1}" />
                            <DataGridTextColumn x:Name="col2" Header="Col 2" Width="*" Binding="{Binding Value2}" />
                            <DataGridTextColumn x:Name="col3" Header="Col 3" Width="*" Binding="{Binding Value3}" />
                        </DataGrid.Columns>
                    </DataGrid>
                    <StackPanel  x:Name="TotalsRow" Orientation="Horizontal">
                        <TextBlock Width="{Binding ElementName=col1, Path=ActualWidth}" Text="{Binding Totals[0]}" />
                        <TextBlock Width="{Binding ElementName=col2, Path=ActualWidth}" Text="{Binding Totals[1]}" />
                        <TextBlock Width="{Binding ElementName=col3, Path=ActualWidth}" Text="{Binding Totals[2]}" />
                    </StackPanel>
                </StackPanel>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
    <Button Content="Change something" Click="Button_Click" />
</StackPanel>