0

Consider the following object, part of a WPF MVVM application:

public class MyObject : INotifyPropertyChanged
{
    // INotifyPropertyChanged gubbins

    private bool _isSelected;
    public bool IsSelected
    {
        get
        {
            return _isSelected;
        }
        set
        {
            _isSelected = value;
            OnPropertyChanged("IsSelected");
        }
    }
}

And its use in the following ViewModel:

public class MyViewModel : INotifyPropertyChanged
{
    // INotifyPropertyChanged gubbins

    private List<MyObject> _myObjects;
    public List<MyObject> MyObjects
    {
        get
        {
            return _myObjects;
        }
        set
        {
            _myObjects = value;
            OnPropertyChanged("MyObjects");
        }
    }

    public bool CanDoSomething
    {
        get
        {
            return MyObjects.Where(d => d.IsSelected).Count() > 0;
        }
    }
}

In this situation, I can track which of my objects have been selected, and selecting them will fire OnPropertyChanged and so can notify the parent view.

However, CanDoSomething will always be false because there's nowhere I can fire an OnPropertyChanged to create a notification. If I put it in MyObject, it doesn't know anything about the property and so does nothing. There's nowhere to put it in the ViewModel because there's nothing that reacts when an object in the list is selected.

I've tried substituting the List for an ObservableCollection and a custom "TrulyObservableCollection" (see Notify ObservableCollection when Item changes ) but neither work.

How can I get round this, without resorting to click events?

Community
  • 1
  • 1
Bob Tway
  • 9,301
  • 17
  • 80
  • 162
  • 1
    You could hook up the `PropertyChangedEvent` in the `MyViewModel` for all of the `MyObjects` and when the `IsSelected` property is changed on the Model it will notify the ViewModel and you can then raise another property changed event for `CanDoSomething` to show that this may have changed. – Stephen Ross Feb 29 '16 at 15:48
  • 1
    This is basically a question of how to communicate between ViewModels. One approach is to pass to each item parent ViewModel, so you can call a method of parent ViewModel in `IsSelected` setter. Other possible approach (should be more) is to use static event, which is triggered by `IsSelected` setter, subscribe to this event in your parent ViewModel and call in handler `NotifyPropertyChanged(nameof(CanDoSomething))`. – Sinatr Feb 29 '16 at 15:50
  • 1
    Good point, Sinatr. In this post, Matt Hamilton injects the parent ViewModel as a host object. This is acceptable in cases where the child ViewModel really cannot exist without the parent (like in that case a Tweet without a parent Timeline). http://matthamilton.net/nested-viewmodels – Christoph Feb 29 '16 at 15:52
  • With using a `List` the `View` will have to resort to usage of `CollectionViewSource` which has a `Refresh()` function which will call the `getter` of your list. However to avoid that I would have a property to accommodate for `SelectedItem`. `ListView` or `ComboBox` can have Selection set to Multiple and all of those selected items can be stored in a Property, this way you don't have to rely on click events. HTH – XAMlMAX Feb 29 '16 at 16:19
  • Try to put `CanDoSomething` in setter of `MyObjects`. – StepUp Feb 29 '16 at 16:23
  • Change CanDoSomething to be a regular property (i.e. getter/setter with INPC) and go back to using your TrulyObservableCollection. Then do something like `MyObjects.CollectionChanged += (s,e) => CanDoSomething = MyObjects.Any(d => d.IsSelected);}`. – Mark Feldman Mar 01 '16 at 00:21

2 Answers2

1

I feel like if I had a better idea of what your end goal was I might be able to recommend a better approach. There is some stuff going on that just feels a little out place. Like maybe 'CanDoSomething' should be part of a command object. And I am wondering if more than one MyObject be selected at a time? If not then I would approach this in an entirely differnt way.

So anyway, you want to update CanDoSomething any time the IsSelected property of one of the items in MyObjects changes. It sounds like you were using an ObservableCollection at one point and then abandoned it. That was a mistake. You need to update CanDoSomething any time one of two events occur; the first is when items are added to or removed from MyObjects and the second is when the IsSelected property of any of the objects in MyObjects changes. For the first event you need something that implements INotifyCollectionChanged, i.e. an ObservableCollection. You already have the second event covered because the objects implement INotifyPropertyChanged. So you just have to combine those two things.

In the following example I have taken your code and made some changes. To start with I changed MyObjects back to an ObservableCollection<MyObject>. It does not have a setter because I have found that there usually is not good reason to change an observable collection; just add and remove objects as necessary. Then in the viewmodel's constructor I am register for the CollectionChanged event of MyObjects. In that handler I am grabbing items that are added to the collection and hooking up their PropertyChanged event to the OnIsSelectedChanged event handler and I am unhooking the PropertyChanged event from OnIsSelectedChanged for any objects that were removed from the collection. Because items have been added or removed we have no idea what the state of IsSelected may be of the objects in MyObjects so this is a good opportunity to update CanDoSomething, and I do at the bottom of the event handler. Finally, the OnIsSelectedChanged is where the other half of the magic happens. Every object in MyObjects will have their PropertyChanged event hooked up to this event handler. Whenever the IsSelected property on any of these objects changes the event handler will update CanDoSomething.

public class MyViewModel : INotifyPropertyChanged
{
    // INotifyPropertyChanged gubbins
    public MyViewModel()
    {
        this._myObjects.CollectionChanged += (o, e) =>
        {
            if (e.NewItems != null)
            {
                foreach (var obj in e.NewItems.OfType<MyObject>())
                {
                    obj.PropertyChanged += this.OnIsSelectedChanged;
                }
            }

            if (e.OldItems != null)
            {
                foreach (var obj in e.OldItems.OfType<MyObject>())
                {
                    obj.PropertyChanged -= this.OnIsSelectedChanged;
                }
            }

            if (e.PropertyName == "IsSelected")
            {
                this.CanDoSomething = this.MyObjects.Any(x => x.IsSelected);
            }
        };
    }

    private readonly ObservableCollection<MyObject> _myObjects =
         new ObservableCollection<MyObject>();
    public ObservableCollection<MyObject> MyObjects
    {
        get
        {
            return _myObjects;
        }
    }

    private void OnIsSelectedChanged(object o, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "IsSelected")
        {
            this.CanDoSomething = this.MyObjects.Any(x => x.IsSelected);
        }
    }

    private bool _canDoSomething;
    public bool CanDoSomething
    {
        get { return this._canDoSomething; }
        private set
        {
            if (_canDoSomething != value)
            {
                _canDoSomething = value;
                OnPropertyChanged("CanDoSomething");
            }
        }
    }
}
Jason Boyd
  • 6,839
  • 4
  • 29
  • 47
1

First create a class that defines this attached property:

public static class ItemClickCommand 
{ 
    public static readonly DependencyProperty CommandProperty = 
    DependencyProperty.RegisterAttached("Command", typeof(ICommand), 
    typeof(ItemClickCommand), new PropertyMetadata(null, OnCommandPropertyChanged));

    public static void SetCommand(DependencyObject d, ICommand value) 
    { 
        d.SetValue(CommandProperty, value); 
    }

    public static ICommand GetCommand(DependencyObject d) 
    { 
        return (ICommand)d.GetValue(CommandProperty); 
    }

    private static void OnCommandPropertyChanged(DependencyObject d, 
        DependencyPropertyChangedEventArgs e) 
    { 
        var control = d as ListViewBase; 
        if (control != null) 
            control.ItemClick += OnItemClick; 
    }

    private static void OnItemClick(object sender, ItemClickEventArgs e) 
    { 
        var control = sender as ListViewBase; 
        var command = GetCommand(control);

        if (command != null && command.CanExecute(e.ClickedItem)) 
            command.Execute(e.ClickedItem); 
    } 
}

Then just bind this attached property to a delegate command in your view model: helper:ItemClickCommand.Command="{Binding MyItemClickCommand}"

You can find more detail in this blog post: https://marcominerva.wordpress.com/2013/03/07/how-to-bind-the-itemclick-event-to-a-command-and-pass-the-clicked-item-to-it/

Let me know if it works

Mirko Bellabarba
  • 562
  • 2
  • 13