Search code examples
c#wpfmvvmprism

Understanding Delegate Command in Prism


I'm struggling to understand the usage of delegate commands (from Prism) and I build a dummmy application in which I intend to do the following.

I have the command as

private readonly DelegateCommand selectAll;

public ICommand SelectAll
{
    get { return selectAll; }
}

and use it as

selectAll= new DelegateCommand(SelectAll,CanSelectAll);

private bool CanSelectAll()
{
   if (AllSelectedItems.Count()>3)
   {
      return true;
   }
   return false;
}
public IList<Student> AllItemsSelected
{
    get => m_Items;
    set => Set(ref m_Items, value);
}

I can see the button being disabled as expected when my ViewModel gets initialized but after even though sometimes this AllSelectedItems.count > 3, it doesn't seem to update and notify the UI.

What am I doing wrong here?


Solution

  • The CanSelectAll method is not called automatically when the collection changes, after all how should the command know when to reevaluate the the condition? You have to explicitly tell it to do so.

    An ICommand exposes a CanExecutChanged event that must be raised to notify the element binding the command to call the CanExecute method in order to evaluate if the command can be executed or not. This usually enables or disables the element in the UI, e.g. a Button. When and how this event is raised depends on the concrete implementation of the ICommand interface.

    In Prism for DelegateCommands, this can be done in two different ways.

    • Call the RaiseCanExecuteChanged on the command. This could be done in the setter of your AllItemsSelected property.

      public IList<Student> AllItemsSelected
      {
          get => m_Items;
          set 
          {
             Set(ref m_Items, value);
             selectAll.RaiseCanExecuteChanged();
          }
      }
      
    • Another way of doing this is using the ObservesProperty method when instantiating the command. You pass a lambda for the property to be observed and the command will automatically raise the CanExecuteChanged event once a PropertyChanged event is raised for it. That means this mechanism only works if your view model implements INotifyPropertyChanged and your property raises PropertyChanged.

      selectAll= new DelegateCommand(SelectAll, CanSelectAll).ObservesProperty(() => AllItemsSelected);
      

    Which mechanism you choose is up to you. For your specific case it is important to know how AllItemsSelected changes. If you always assign a new collection once the selection changes, the examples above will work, since then each time the setter of the property is called and PropertyChanged is raised and therefore ObservesProperty will pick up the change and call CanExecutChanged for example.

    However, if you reuse the same collection, e.g. only add and delete items from it, this will not work, as the actual collection object does not change, which means no call to the setter and no PropertyChanged. In this case put the call to RaiseCanExecuteChanged into the method that adds, deletes or modifies the collection.


    In case the collection is modified somewhere else e.g. items are added through the UI directly to the collection, you would have to use a collection type that supports notifying collection changes like ObservableCollection<T> (through the CollectionChanged event). You could add a handler to CollectionChanged which calls RaiseCanExecuteChanged.

    public class MyViewModel : BindableBase
    {
       private readonly DelegateCommand _selectAll;
    
       public MyViewModel()
       {
          _selectAll = new DelegateCommand(ExecuteSelectAll, CanExecuteSelectAll);
          AllSelectedItems = new ObservableCollection<Student>();
          AllSelectedItems.CollectionChanged += OnAllSelectedItemsChanged;
       }
    
       public ICommand SelectAll => _selectAll;
    
       public ObservableCollection<Student> AllSelectedItems
       {
          get => m_Items;
          set => Set(ref m_Items, value);
       }
    
       private void ExecuteSelectAll()
       {
          // ...your code.
       }
    
       private bool CanExecuteSelectAll()
       {
          return AllSelectedItems.Count > 3;
       }
    
       private void OnAllSelectedItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
       {
          _selectAll.RaiseCanExecuteChanged();
       }
    }