Я сократил свой код до минимального размера тестового примера, но он все еще довольно велик; Надеюсь, это довольно просто.
Foo имеет ObservableCollection типов Bar и Baz. Baz хранит ObservableCollection ссылок на объекты Bar в Foo.
В главном окне есть ListBox всех объектов Baz в Foo, которые проходят через конвертер, чтобы сделать их простой строкой. SelectedItem устанавливается как DependencyProperty окна для удобства использования. Позже в окне отображается список всех объектов Bar в Foo, которые можно добавить / удалить с помощью этого свойства DependencyProperty (SelectedBaz). Для целей отладки добавлен еще один ListBox, который показывает объекты Bar SelectedBaz.
Что происходит: SelectedBaz обновляется, Baz в ObservableCollection, удерживаемом Foo, обновляется, событие CollectionChanged для коллекции Baz Foo запускается, но ListBox с конвертером никогда не обновляется.
Я безуспешно пытался разбрызгать немного «Mode = TwoWay» (удалено, так как они не имели никакого эффекта). Я пробовал использовать SelectedValue vs. SelectedItem (похоже, что SelectedItem - правильный способ сделать это из моих исследований, поэтому я оставил его как таковой). Я попытался вручную запустить обновление цели привязки в Baz ListBox при нажатии кнопки добавления / удаления, но это не повлияло.
Затем я расстроился и попытался взломать его и использовать целое число с SelectedIndex, MultiBinding, MultiValueConverter и т. д. И т. Д. И обнаружил, что у меня такая же проблема; источник обновляется, но не цель в привязке Baz ListBox.
Итак, вот и мы.
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());
}
}
Вы не предоставляете Path
в своем Binding
, поэтому вы привязываетесь ко всему объекту, а WPF не отслеживает уведомления об изменении свойств привязок без пути:
<Label Content = "{Binding Converter = {StaticResource BazToString}}" />
Есть два способа решить эту проблему (рекомендую первый, потому что он более чистый и типичный для WPF):
IMultiValueConverter
и MultiBinding
для обоих свойств Name
и Bars
. Например:Измените конвертер на 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;
}
}
Измените привязку Label
к MultiBinding
с соответствующими привязками свойств:
<Label>
<Label.Content>
<MultiBinding Converter = "{StaticResource BazToString}">
<Binding Path = "Name" />
<Binding Path = "Bars" />
</MultiBinding>
</Label.Content>
</Label>
Baz
для возврата самого объекта и привязки к нему. Вам также необходимо будет вызвать события PropertyChanged
для этого свойства. Например:Добавьте свойство в класс Baz
:
public Baz This
{
get { return this; }
}
Добавляем возникновение события PropertyChanged
:
private void Bars_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
NotifyPropertyChanged("Bars");
NotifyPropertyChanged("This");
}
Измените Binding
из Label
:
<Label Content = "{Binding Path=This, Converter = {StaticResource BazToString}}" />
Это именно то! Спасибо!