简体   繁体   中英

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. Baz keeps an ObservableCollection of references to the Bar objects in Foo.

The main window has a ListBox of all the Baz objects in Foo, which go through a converter to make them a plain string. The SelectedItem is set as a DependencyProperty of the window for easy reference. 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). For debugging purposes, another ListBox is added that shows the Bar objects of SelectedBaz.

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.

I've tried sprinkling some 'Mode=TwoWay' throughout without any luck (removed since they had no effect). 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). 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.

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; the source is updated but not the target in the binding of the Baz ListBox.

So, here we are.

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

<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

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:

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

  1. You could use IMultiValueConverter and MultiBinding to both properties Name and Bars . 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):

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>
    <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. You would also need to raise PropertyChanged events for this property. For example:

Add the property to class Baz :

public Baz This
{
    get { return this; }
}

Add raising of PropertyChanged event:

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

Change Binding of the Label :

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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