简体   繁体   中英

Why is this DataTemplate inside a ListView not updating its binding automatically?

I have the following class hierarchy:

namespace WpfBindingProblem
{
    public class Top
    {
        public IList<Mid> MidList { get; } = new ObservableCollection<Mid>();
    }

    public class Mid
    {
        public IList<Bot> BotList { get; } = new ObservableCollection<Bot>();
    }

    public class Bot
    {
    }
}

And I have this XAML window:

<Window x:Class="WpfBindingProblem.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:WpfBindingProblem"
        mc:Ignorable="d"
        Title="MainWindow" Height="217.267" Width="333.686">
    <Window.DataContext>
        <local:Top/>
    </Window.DataContext>
    <Window.Resources>
        <local:TriggersToString x:Key="TriggersToString"/>
    </Window.Resources>
    <Grid>
        <ListView Margin="10" ItemsSource="{Binding MidList}" x:Name="ThatList">
            <ListView.Resources>
                <DataTemplate DataType="{x:Type local:Mid}">
                    <TextBlock Text="{Binding BotList, Converter={StaticResource TriggersToString}}" />
                </DataTemplate>
            </ListView.Resources>
            <ListView.ContextMenu>
                <ContextMenu>
                    <MenuItem Header="Add mid" Click="AddMid"/>
                    <MenuItem Header="Add bot to selected mid" Click="AddBot" />
                </ContextMenu>
            </ListView.ContextMenu>
            <ListView.View>
                <GridView>
                    <GridViewColumn/>
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>

With these handlers:

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

        private void AddMid(object sender, RoutedEventArgs e)
        {
            if(DataContext is Top p)
            {
                p.MidList.Add(new Mid());
            }
        }

        private void AddBot(object sender, RoutedEventArgs e)
        {
            if(ThatList.SelectedItem is Mid c)
            {
                c.BotList.Add(new Bot());
            }
        }
    }
}

And this converter (as a stand-in for any arbitrary converter):

namespace WpfBindingProblem
{
    [ValueConversion(typeof(IList<Bot>), typeof(string))]
    public class TriggersToString : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if(value is IList<Bot> list)
            {
                return list.Count.ToString();
            }
            throw new InvalidOperationException();
        }

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

In the window that appears when we run this example, I can right click and choose "Add mid" so an instance of Mid is added to the Top data context, and the list view is updated accordingly, showing the number 0 (as per the conversion logic)

However, when I click "Add bot to selected mid", an instance of Bot is added to the selected Mid (I can verify this using breakpoints), but the list view is not updated accordingly (I expected 0 to be changed to 1, but the converter is not called again for that particular instance of Mid ).

Why does this change not trigger an update of the GUI?

I know I can work around this with some hacks (like setting the data context to null and back, or possibly by invoking explicit updates using dependency properties), but there are two reasons why I'd like to avoid that:

  • My actual code is more complex than this MCVE and it would look very ugly.
  • I've already sprinkled all my (actual) classes with all the required the ObservableCollection s and the INotifyPropertyChanged interfaces, precisely so that I wouldn't need to perform manual updates — so I feel like automatic updates should happen in this case, unless I've missed something.

You can use multi binding:

<TextBlock>
    <TextBlock.Text>
        <MultiBinding Converter="{StaticResource TriggersToString}">
            <Binding Path="BotList" />
            <Binding Path="BotList.Count" />
        </MultiBinding>
    </TextBlock.Text>
</TextBlock>

and multi value converter:

public class TriggersToString : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) =>
        (values[0] as IList<Bot>)?.Count.ToString(); // first binding

    ...
}

This way the converter is called whenever either of bindings is updated.

Why does this change not trigger an update of the GUI?

Because the source property of the binding ( BotList ) is not updated. The converter is invoked only when the data bound property is updated.

You could use a MultiBinding as suggested by @Sinatr or you could

  • bind directly to the Count property of the collection:

     <TextBlock Text="{Binding BotList.Count}" /> 
  • implement the INotifyPropertyChanged interface in the Mid class and raise the PropertyChanged event for the BotList property whenever an item is added to it. Handle CollectionChanged .

You might also move your convert logic to the view model, bind to a property of this one and also raise the PropertyChanged for it whenever you want the binding to be refreshed.

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