Caliburn.Micro and DataGrid: Reliable approach for

2019-08-31 05:42发布

问题:

I have an MVVM-based WPF application that relies on Caliburn.Micro.

In one view, I am displaying a DataGrid and a Button. The DataGrid displays a collection of items, where the item class derives from PropertyChangedBase.
The button should be enabled or disabled based on the contents in the editable DataGrid cells. What is the most reliable approach to achieve this with Caliburn.Micro?

Schematically, this is what my code looks right now:

public class ItemViewModel : PropertyChangedBase { }

...

public class ItemsViewModel : PropertyChangedBase
{
    private IObservableCollection<ItemViewModel> _items;

    // This is the DataGrid in ItemsView
    public IObservableCollection<ItemViewModel> Items
    {
        get { return _items; }
        set
        {
            _items = value;
            NotifyOfPropertyChange(() => Items);
        }
    }

    // This is the button in ItemsView
    public void DoWork() { }

    // This is the button enable "switch" in ItemsView
    public bool CanDoWork
    {
        get { return Items.All(item => item.NotifiableProperty == some_state); }
    }
}

As the code stands, there is no notification to ItemsViewModel.CanDoWork when one NotifiableProperty is changed, for example when the user edits one cell in the ItemsView´s DataGrid. Hence, the DoWork button enable state will never be changed.

One possible workaround is to add an (anonymous) event handler to every item in the Items collection:

foreach (var item in Items)
    item.PropertyChanged += 
        (sender, args) => NotifyOfPropertyChange(() => CanDoWork);

but then I also need to keep track of when (if) items are added or removed from the Items collection, or if the Items collection is re-initialized altogether.

Is there a more elegant and reliable solution to this problem? I am sure there is, but so far I have not been able to find it.

回答1:

I think this is a case where INPC works well; to simplify registering/deregistering adds and deletes just add a CollectionChanged handler to your Items collection:

Items.CollectionChanged += OnItemsCollectionChanged;

private void OnItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
    if (e.NewItems != null && e.NewItems.Count != 0) {
        foreach (ItemViewModel  vm in e.NewItems)
            vm.PropertyChanged += OnDetailVmChanged;
    }
    if (e.OldItems != null && e.OldItems.Count != 0) {
        foreach (ItemViewModel  vm in e.OldItems) {
            vm.PropertyChanged -= OnDetailVmChanged;
        }
    }
}

Josh Smith wrote a PropertyObserver class here that I find more elegant than shotgun INPC tracking, but in a master-detail scenario like yours you would still have to track the adds and deletes.

EDIT by Anders Gustafsson
Note that for the above code to work in the general case requires that Items has been initialized with the default constructor before the event handler is attached. To ensure that OnDetailVmChanged event handlers are correctly added and removed, the Items property setter need to be extended to something like this:

public IObservableCollection<ItemViewModel> Items
{
    get { return _items; }
    set
    {
        // If required, initialize empty _items collection and attach
        // event handler
        if (_items == null) {
            _items = new BindableCollection<ItemViewModel>();
            _items.CollectionChanged += OnItemsCollectionChanged;
        }
        // Clear old contents in _items
        _items.Clear();
        // Add value item:s one by one to _items
        if (value != null) foreach (var item in value) _items.Add(item);

        NotifyOfPropertyChange(() => Items);
    }
}

(And of course, with the above Items setter in place, the topmost Items.CollectionChanged event handler attachment should not be included in the code.)

Ideally, I would have used if (value != null) _items.AddRange(value);, but when the AddRange method triggers the OnItemsCollectionChanged event handler, e.NewItems appear to be empty (or null). I have not explicitly verified that e.OldItems is non-null when the Clear() method is invoked; otherwise Clear() would also need to be replaced with one-by-one removal of the item:s in _items.



回答2:

Whenever a property changes fire RaiseCanExecuteChanged for the button cummand command?

For Example :

public DelegateCommand<object> MyDeleteCommand { get; set; }

string _mySelectedItem;
    public string MySelectedItem
    {
        get { return _mySelectedItem; }
        set
        {
            _mySelectedItem = value;
            OnPropertyChanged("MySelectedItem");
            MyDeleteCommand.RaiseCanExecuteChanged();
        }
    }