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.
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
.
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();
}
}