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>
首先,对于这篇文章的篇幅,我深表歉意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>