简体   繁体   English

WPF绑定未更新,可能由于值转换器或依赖项属性

[英]WPF binding not updating, possibly because of value converter or dependency property

I've trimmed my code down to as small of a test case as possible, but it's still rather large; 我已将代码缩减为尽可能少的测试用例,但仍然很大。 I hope it's pretty straightforward. 我希望它非常简单。

Foo has an ObservableCollection of both Bar and Baz types. Foo具有Bar和Baz类型的ObservableCollection。 Baz keeps an ObservableCollection of references to the Bar objects in Foo. Baz在Foo中保留对Bar对象的引用的ObservableCollection。

The main window has a ListBox of all the Baz objects in Foo, which go through a converter to make them a plain string. 主窗口有一个Foo中所有Baz对象的ListBox,这些对象通过转换器使其成为纯字符串。 The SelectedItem is set as a DependencyProperty of the window for easy reference. SelectedItem设置为窗口的DependencyProperty,以方便参考。 Later on in the window, a list of all the Bar objects in Foo are listed and can be added/removed through this DependencyProperty (SelectedBaz). 在窗口的后面,列出了Foo中所有Bar对象的列表,可以通过此DependencyProperty(SelectedBaz)添加/删除。 For debugging purposes, another ListBox is added that shows the Bar objects of SelectedBaz. 出于调试目的,添加了另一个ListBox,其中显示了SelectedBaz的Bar对象。

What is happening is SelectedBaz is updated, the Baz in the ObservableCollection held by Foo is updated, the CollectionChanged event for the Baz collection of Foo is fired, but the ListBox with the converter is never updated. 发生的情况是,SelectedBaz被更新,Foo持有的ObservableCollection中的Baz被更新,Foo的Baz集合的CollectionChanged事件被触发,但是带有转换器的ListBox从未更新。

I've tried sprinkling some 'Mode=TwoWay' throughout without any luck (removed since they had no effect). 我尝试在整个过程中撒一些'Mode = TwoWay',但没有任何运气(由于它们无效,因此删除了)。 I've tried using SelectedValue vs. SelectedItem (it seems that SelectedItem is the proper way to do this from my research, so I left it as such). 我已经尝试过使用SelectedValue与SelectedItem(从我的研究看来,SelectedItem是执行此操作的正确方法,因此我将其保留了下来)。 I tried manually triggering an update of the binding target in the Baz ListBox in the add/remove button clicks, but that had no effect. 我尝试在添加/删除按钮单击中手动触发Baz ListBox中的绑定目标的更新,但这没有任何效果。

I then got frustrated and tried to hack it and use an integer with SelectedIndex, MultiBinding, MultiValueConverter, etc. etc. and I found that I had the same problem; 然后,我感到沮丧,并尝试破解它,并使用带有SelectedIndex,MultiBinding,MultiValueConverter等的整数,等等。我发现我也遇到了同样的问题。 the source is updated but not the target in the binding of the Baz ListBox. Baz ListBox绑定中的源已更新,但目标未更新。

So, here we are. 所以,我们到了。

Foo.cs Foo.cs

using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Windows.Data;

namespace WpfApp1
{
    public class Foo : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public Foo()
        {
            bars.CollectionChanged += Bars_CollectionChanged;
            bazes.CollectionChanged += Bazes_CollectionChanged;

            bars.Add(new Bar("Bar 1"));
            bars.Add(new Bar("Bar 2"));
            bars.Add(new Bar("Bar 3"));

            bazes.Add(new Baz("Baz 1")
            {
                Bars = { bars[0] }
            });

            bazes.Add(new Baz("Baz 2")
            {
                Bars = { bars[1] }
            });

            bazes.Add(new Baz("Baz 3")
            {
                Bars = { bars[0], bars[1], bars[2] }
            });
        }

        public ObservableCollection<Bar> Bars
        {
            get
            {
                return bars;
            }
        }

        public ObservableCollection<Baz> Bazes
        {
            get
            {
                return bazes;
            }
        }

        private void Bars_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            NotifyPropertyChanged("Bars");
        }

        private void Bazes_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            NotifyPropertyChanged("Bazes");
        }

        private void NotifyPropertyChanged([CallerMemberName] string caller = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(caller));
        }

        private ObservableCollection<Bar> bars = new ObservableCollection<Bar>();

        private ObservableCollection<Baz> bazes = new ObservableCollection<Baz>();
    }

    public class Bar : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public Bar(string name)
        {
            this.name = name;
        }

        public string Name
        {
            get
            {
                return name;
            }

            set
            {
                if (name != value)
                {
                    name = value;
                    NotifyPropertyChanged();
                }
            }
        }

        private void NotifyPropertyChanged([CallerMemberName] string caller = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(caller));
        }

        private string name = "";
    }

    public class Baz : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public Baz(string name)
        {
            this.name = name;
            bars.CollectionChanged += Bars_CollectionChanged;
        }

        public ObservableCollection<Bar> Bars
        {
            get
            {
                return bars;
            }
        }

        public string Name
        {
            get
            {
                return name;
            }

            set
            {
                if (name != value)
                {
                    name = value;
                    NotifyPropertyChanged();
                }
            }
        }

        private void Bars_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            NotifyPropertyChanged("Bars");
        }

        private void NotifyPropertyChanged([CallerMemberName] string caller = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(caller));
        }

        private ObservableCollection<Bar> bars = new ObservableCollection<Bar>();

        private string name = "";
    }

    public class BazToString : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            Baz b = value as Baz;
            string s = "Baz is " + b.Name + " ";

            foreach (Bar bar in b.Bars)
            {
                s += "with a Bar " + bar.Name + " ";
            }

            return s;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

MainWindow.xaml MainWindow.xaml

<Window x:Class="WpfApp1.MainWindow"
        x:Name="Main"
        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:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:Foo />
    </Window.DataContext>
    <Window.Resources>
        <local:BazToString x:Key="BazToString" />
    </Window.Resources>
    <Grid>
        <ListBox Width="300" Height="150" Margin="10,10,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" ItemsSource="{Binding Bazes}" SelectedItem="{Binding ElementName=Main, Path=SelectedBaz}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Label Content="{Binding Converter={StaticResource BazToString}}" />
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <ListBox x:Name="ListBoxBarSelector" Width="300" Height="150" Margin="10,170,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" ItemsSource="{Binding Bars}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Label Content="{Binding Name}" />
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <ListBox Width="300" Height="150" Margin="320,170,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" ItemsSource="{Binding ElementName=Main, Path=SelectedBaz.Bars}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Label Content="{Binding Name}" />
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <Button Width="100" Height="30" Margin="10,330,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" Click="ButtonAddBar_Click" Content="Add Bar" />
        <Button Width="100" Height="30" Margin="120,330,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" Click="ButtonDelBar_Click" Content="Delete Bar" />
    </Grid>
</Window>

MainWindow.xaml.cs MainWindow.xaml.cs

using System.Windows;

namespace WpfApp1
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        public Baz SelectedBaz
        {
            get
            {
                return (Baz)GetValue(SelectedBazProperty);
            }

            set
            {
                SetValue(SelectedBazProperty, value);
            }
        }

        private void ButtonAddBar_Click(object sender, RoutedEventArgs e)
        {
            Bar bar = ListBoxBarSelector.SelectedItem as Bar;

            if (bar != null && SelectedBaz != null && !SelectedBaz.Bars.Contains(bar))
            {
                SelectedBaz.Bars.Add(bar);
            }
        }

        private void ButtonDelBar_Click(object sender, RoutedEventArgs e)
        {
            Bar bar = ListBoxBarSelector.SelectedItem as Bar;

            if (bar != null && SelectedBaz != null && SelectedBaz.Bars.Contains(bar))
            {
                SelectedBaz.Bars.Remove(bar);
            }
        }

        private static readonly DependencyProperty SelectedBazProperty =
            DependencyProperty.Register(
                "SelectedBaz",
                typeof(Baz),
                typeof(MainWindow),
                new PropertyMetadata());
    }
}

You are not providing Path in your Binding so you are binding to a whole object and WPF does not track property change notifications of the bindings with no path: 您没有在Binding提供Path ,因此您要绑定到整个对象,并且WPF不会跟踪没有路径的绑定的属性更改通知:

<Label Content="{Binding Converter={StaticResource BazToString}}" />

There are two ways to solve this problem (I recommend the first way, because it is more clean and typical for WPF): 有两种方法可以解决此问题(我建议采用第一种方法,因为它更干净,而且对于WPF而言是典型的):

  1. You could use IMultiValueConverter and MultiBinding to both properties Name and Bars . 您可以将IMultiValueConverterMultiBinding都用于属性NameBars For example: 例如:

Change the converter to IMultiValueConverter (I also recommend coming up with more suitable name for the converter after this change to multi value converter): 将转换器更改为IMultiValueConverter (在更改为多值转换器之后,我还建议为转换器提供更合适的名称):

public class BazToString : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        var bazName = (string)values[0];
        var bars = (IEnumerable<Bar>)values[1];


        string s = "Baz is " + bazName + " ";

        foreach (Bar bar in bars)
        {
            s += "with a Bar " + bar.Name + " ";
        }

        return s;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        return null;
    }
}

Change the binding of the Label to MultiBinding with appropriate property bindings: 使用适当的属性绑定将Label的绑定更改为MultiBinding

<Label>
    <Label.Content>
        <MultiBinding Converter="{StaticResource BazToString}">
            <Binding Path="Name" />
            <Binding Path="Bars" />
        </MultiBinding>
    </Label.Content>
</Label>
  1. More dirty solution would be adding a property to Baz class for returning the object itself and binding to it. 更肮脏的解决方案是在Baz类中添加一个属性,以返回对象本身并绑定到该对象。 You would also need to raise PropertyChanged events for this property. 您还需要为此属性引发PropertyChanged事件。 For example: 例如:

Add the property to class Baz : 将该属性添加到Baz类:

public Baz This
{
    get { return this; }
}

Add raising of PropertyChanged event: 添加引发PropertyChanged事件:

private void Bars_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
    NotifyPropertyChanged("Bars");
    NotifyPropertyChanged("This");
}

Change Binding of the Label : 更改Label Binding

<Label Content="{Binding Path=This, Converter={StaticResource BazToString}}" />

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM