I have a desktop app already released (so I'd appreciate an answer that keeps the changes and regression tests at a minimum) and I need to add a consistency check CanBeDeleted
when the grid is changed.
<DataGrid AutoGenerateColumns="False"
ItemsSource="{Binding CurrentPosIn.PosInLocationsList}"
CanUserAddRows="{Binding UpdateEnabled}" CanUserDeleteRows="{Binding UpdateEnabled}" >
I'm using UpdateEnabled
for something different (profile permissions) and I don't want to make the DataGrid
read only either: I'd prefer (unless it is too complex) to see a blocking alert (a MessageBox
) preventing changes.
What I've done till now is
- against MVVM, because I've put the alert in the Model (but I can accept this, if it makes the changes quick and simple)
- not working, because the second part (see below) of my changes produces an invalid operation exception
The ViewModel contains the following list
[Association(ThisKey="Partita", OtherKey="Partita", CanBeNull=true, IsBackReference=true)]
public ObservableCollection<Model.PosInLocation> posin_locations_list = new ObservableCollection<Model.PosInLocation>();
public ObservableCollection<PosInLocation> PosInLocationsList {
get { return posin_locations_list; }
set {
posin_locations_list = value;
OnPropertyChanged( () => PosInLocationsList );
}
}
and I'm adding the consistency check here
string _storage;
[Column(Name = "storage"), PrimaryKey]
public string Storage {
get { return _storage; }
set {
if (this.loadedEF) {
string validate_msg;
if (!PurchasePosIn.CanBeDeleted(out validate_msg)) {
// against MVVM
MessageBox.Show(validate_msg, "Alert", MessageBoxButton.OK);
OnPropertyChanged( () => Storage );
return;
}
Persistence.MyContext.deletePosInLocation(this);
}
_storage = value;
OnPropertyChanged( () => Storage );
if (this.loadedEF) {
Persistence.MyContext.insertPosInLocation(this);
}
}
}
and here (second part)
internal void posin_locations_list_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs args)
{
string validate_msg;
if (!CanBeDeleted(out validate_msg)) {
// indirectly produces an invalid operation exception
MessageBox.Show(validate_msg, "Alert", MessageBoxButton.OK);
return;
}
if (args.OldItems != null)
foreach(var oldItem in args.OldItems) {
if ( ((PosInLocation)oldItem).Partita != null)
Persistence.MyContext.deletePosInLocation((PosInLocation)oldItem);
}
if (args.NewItems != null)
foreach(var newItem in args.NewItems)
{
PosInLocation newPosInLocation = (PosInLocation)newItem;
if ( newPosInLocation.Partita == null) {
newPosInLocation.Partita = this.Partita;
newPosInLocation.PurchasePosIn = this;
newPosInLocation.loadedEF = true;
}
}
}
Maybe it's ugly (really, it's not so ugly: imho it's a nice MVVM approach, also applicable to modern mahapps.metro
Dialogs
), but now I'm settingan invisible
TextBox
where I'm dispatching the alert
Rolling back added/deleted items
I see a connection with this other question Prevent adding the new Item on ObservableCollection.CollectionChanged event, in my case I'd say that prevent deleting is even more important. I don't know if there are more updated answers than this one (Can I rollback collection changes on collection changed event? which appears wrong) about this topic.
While the
PropertyChanged
can be easily raised to rollback an item update, for the collection changes I've been forced to pass and reference the view dispatcher inside theCollectionChanged
eventto rollback the added/deleted items
The solution from @Tesseract, subclassing
ObservableCollection
and subscribingRemoveItem
is already MVVM oriented.What is still missing is a correct, modern way to send the alert from the ViewModel. This is where Mahapps approach would be helpful.
In your XAML
where the attached property DialogPartecipation will keep track of the views through a dictionary
The DialogCoordinator will match the ViewModel to the View
through the above said attached property (the
context
being the view model)and display the dialog, invoking the appropriate method on the retrieved view: this is how, if you have multiple windows open, the dialog will display on the correct window.
If only the ObservableCollection implemented a "previewCollectionChanged", things would be so much easier.
For your needs I would recommend creating a subclass of ObservableCollection and overloading the protected method RemoveItem.
Depending on what you are doing with your application, you might want to override other methods than just RemoveItem (like ClearItems).
When subclassing ObservableCollection, there are 5 protected methods you can override : ClearItems, RemoveItem, InsertItem, SetItem and MoveItem.
These methods are, in the end, called by all the public methods, so overriding them gives you full control.
Here's a small app you can run that demonstrates this :
ObservableCollection SubClass
I use the DeletionDenied event so that this class is not responsible for showing the error window, and it makes it more reusable.
ViewModel
The ViewModel for the MainWindow.
It knows it's window for the sole purpose of displaying the error message.
(I'm sure there's a better solution than this out there but I have yet to find a good and short way of displaying popup windows in mvvm.)
When the DeletionDenied event triggers, the error window is called.
Model
XAML
XAML.CS
APP MAIN
I've set the ViewModel's window in the startup, but you should probably do it wherever you create your ViewModel