WPF binding to property of all items in a collecti

2019-06-01 20:18发布

I need to bind to a bool property, that is only true, when one of the properties in the collection is true.

This is the binding:

<tk:BusyIndicator IsBusy="{Binding Tabs, Converter={StaticResource BusyTabsToStateConverter}}">

And the viewmodel:

public class MainWindowViewModel : INotifyPropertyChanged
{
    private ObservableCollection<Tab> _tabs;

    public ObservableCollection<Tab> Tabs
    {
        get
        {  return _tabs; }
        set
        {
            if (value != _tabs)
            {
                _tabs = value;
                NotifyPropertyChanged();
            }
        }
    }

The Tab class also has property change notification:

public class Tab : INotifyPropertyChanged
{
   public bool IsBusy { get{...} set{...NotifyPropertyChanged();} }

This is the converter:

public class BusyTabsToStateConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var tabs = value as ObservableCollection<Tab>;
        return tabs.Any(tab => tab.IsBusy);
    }
}

The problem is, when Tab.IsBusy changes the binding source is not notified, because it is bound to the observable collection and not to the IsBusy property.

Is there a way to make the notification trigger correctly when the IsBusy property on any of the items in the collection changes?

4条回答
We Are One
2楼-- · 2019-06-01 20:57

There's no way to get this for free unfortunately. I would create an IsBusy property on MainWindowViewModel. When Tabs is set, add a listener for collection changes and have that update the IsBusy property.

查看更多
劫难
3楼-- · 2019-06-01 21:06

I've taken @Clemens' answer, and converted in to an extension method that could make it easier to use on multiple collections. It will take an PropertyChangedEventHandler and automatically add and remove it from the items in the collection as they are added and removed. If you up-vote this, please up-vote@Clemens' answer also, since it is based on his work.

Be careful not use use an anonymous method as your PropertyChanged handler (this goes for all event handlers in general, not just this solution) without taking special precautions, as they can be difficult to remove.

(Note: this requires C# 7, as it uses a local function to make dealing with the CollectionChanged handler's delegate easier.)

public static class ObservableCollectionExtensions
{
    public static Hook<TList> RegisterPropertyChangeHook<TList>(this ObservableCollection<TList> collection, PropertyChangedEventHandler handler) where TList : INotifyPropertyChanged
    {
        void Collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    foreach (TList item in e.NewItems)
                    {
                        item.PropertyChanged += handler;
                    }
                    break;
                case NotifyCollectionChangedAction.Remove:
                    foreach (TList item in e.OldItems)
                    {
                        item.PropertyChanged -= handler;
                    }
                    break;
                default:
                    break;
            }
        }

        return new Hook<TList>(collection, Collection_CollectionChanged);
    }

    public class Hook<TList> where TList : INotifyPropertyChanged
    {
        internal Hook(ObservableCollection<TList> collection, NotifyCollectionChangedEventHandler handler)
        {
            _handler = handler;
            _collection = collection;

            collection.CollectionChanged += handler;
        }

        private NotifyCollectionChangedEventHandler _handler;
        private ObservableCollection<TList> _collection;

        public void Unregister()
        {
            _collection.CollectionChanged -= _handler;
        }
    }
}

You can use it like this:

void Main()
{
    var list = new ObservableCollection<Animal>();

    list.RegisterPropertyChangeHook(OnPropertyChange);

    var animal = new Animal(); // Has a "Name" property that raises PropertyChanged
    list.Add(animal);
    animal.Name="Charlie"; // OnPropertyChange called

    list.Remove(animal);
    animal.Name="Sam"; // OnPropertyChange not called
}

private void OnPropertyChange(object sender, PropertyChangedEventArgs e)
{
    Console.WriteLine($"property changed: {e.PropertyName}");
}

If you want to be able to unregister the hook, do this:

var hook = list.RegisterPropertyChangeHook(OnPropertyChange);
hook.Unregister();

Unregistering ended up being trickier than I expected, due to extension method classes not supporting generics. It uses the "memento" pattern to return an object that you can use to unregister later.

查看更多
时光不老,我们不散
4楼-- · 2019-06-01 21:14

To propagate notification from Model to Collection of Model, You need to have a Notifiable property in Collection itself.

Maybe you can extend the ObservableCollection and have a Property in that which can notify the UI

查看更多
放荡不羁爱自由
5楼-- · 2019-06-01 21:17

Instead of a Binding Converter, you could have a AnyTabBusy property in MainWindowViewModel, for which a change notification is fired by a PropertyChanged event handler, which is attached or detached to individual elements from the Tabs collection when they are added to or removed from the collection.

In the example below, the Tabs property is readonly. If it has to be writeable, you would have to attach and detach the TabsCollectionChanged handler in the Tabs setter.

public class MainWindowViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public ObservableCollection<Tab> Tabs { get; } = new ObservableCollection<Tab>();

    public bool AnyTabBusy
    {
        get { return Tabs.Any(t => t.IsBusy); }
    }

    public MainWindowViewModel()
    {
        Tabs.CollectionChanged += TabsCollectionChanged;
    }

    private void TabsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                foreach (Tab tab in e.NewItems)
                {
                    tab.PropertyChanged += TabPropertyChanged;
                }
                break;
            case NotifyCollectionChangedAction.Remove:
                foreach (Tab tab in e.OldItems)
                {
                    tab.PropertyChanged -= TabPropertyChanged;
                }
                break;
            default:
                break;
        }
    }

    private void TabPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == nameof(Tab.IsBusy))
        {
            PropertyChanged?.Invoke(this,
                new PropertyChangedEventArgs(nameof(AnyTabBusy)));
        }
    }
}

If you want to make this code reusable, you could put it into a derived collection class like shown below, where you could attach a handler for an ItemPropertyChanged event.

public class ObservableItemCollection<T>
    : ObservableCollection<T> where T : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler ItemPropertyChanged;

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        base.OnCollectionChanged(e);

        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                foreach (INotifyPropertyChanged item in e.NewItems)
                {
                    item.PropertyChanged += OnItemPropertyChanged;
                }
                break;
            case NotifyCollectionChangedAction.Remove:
                foreach (INotifyPropertyChanged item in e.OldItems)
                {
                    item.PropertyChanged -= OnItemPropertyChanged;
                }
                break;
            default:
                break;
        }
    }

    private void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        ItemPropertyChanged?.Invoke(this, e);
    }
}

The view model could now be reduced to this:

public class MainWindowViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public ObservableItemCollection<Tab> Tabs { get; }
        = new ObservableItemCollection<Tab>();

    public bool AnyTabBusy
    {
        get { return Tabs.Any(t => t.IsBusy); }
    }

    public MainWindowViewModel()
    {
        Tabs.ItemPropertyChanged += TabPropertyChanged;
    }

    private void TabPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == nameof(Tab.IsBusy))
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AnyTabBusy)));
        }
    }
}
查看更多
登录 后发表回答