【问题标题】:WPF: Bindings break when CollectionChanges - why?WPF:当 CollectionChanges 时绑定中断 - 为什么?
【发布时间】:2021-07-04 05:32:42
【问题描述】:

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

我现在想要实现的是数据网格的总计页脚行。由于它需要显示在底行,我采用的方法是添加一些与数据网格中的列对齐的 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);
        }
    }
}

【问题讨论】:

    标签: c# wpf xaml data-binding


    【解决方案1】:

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

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

    【讨论】:

    • 我发现问题的根源与 ObservableItemsCollection 重置有关。显然这打破了gridview,所以一个不同的实现解决了这个问题。所以我认为如果没有这种改变,这个解决方案一旦实施就不会奏效。也就是说,我已经放弃了按名称绑定的想法,因为 VisualTree 中会有多个具有相同名称的元素,因为它位于重复的 ItemsControl 中。我不认为它会起作用,我仍然不知道它是怎么做的,但它确实如此。所以点给你一个更清洁的方式来做到这一点! ty
    【解决方案2】:

    在您的 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>
    

    结果:

    【讨论】:

    • 这不是一个坏主意,但只要调整任何列的大小,它就会中断。
    猜你喜欢
    • 1970-01-01
    • 2013-11-19
    • 2010-11-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-29
    • 2015-08-21
    相关资源
    最近更新 更多