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.
Whenever a property changes fire RaiseCanExecuteChanged for the button cummand command?
For Example :
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:
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 thatOnDetailVmChanged
event handlers are correctly added and removed, theItems
property setter need to be extended to something like this:(And of course, with the above
Items
setter in place, the topmostItems.CollectionChanged
event handler attachment should not be included in the code.)Ideally, I would have used
if (value != null) _items.AddRange(value);
, but when theAddRange
method triggers theOnItemsCollectionChanged
event handler,e.NewItems
appear to be empty (ornull
). I have not explicitly verified thate.OldItems
is non-null when theClear()
method is invoked; otherwiseClear()
would also need to be replaced with one-by-one removal of the item:s in_items
.