Ran into this while porting an old WinForms app to WPF. I think the important thing to keep in mind is that WPF accomplishes at lot of what it does under the hood by signaling between the view model and the view with events (i.e. INotifyPropertyChanged.PropertyChanged
, INotifyDataErrorInfo.ErrorsChanged
, etc). My solution to the problem was to take that example and run with it. In my view model:
/// <summary>
/// Occurs before the record is deleted
/// </summary>
public event CancelEventHandler DeletingRecord;
/// <summary>
/// Occurs before record changes are discarded (i.e. by a New or Close operation)
/// </summary>
public event DiscardingChangesEvent DiscardingChanges;
The view can then listen for these events, prompt the user if need be and cancel the event if directed to do so.
Note that CancelEventHandler
is defined for you by the framework. For DiscardingChanges
, however, you need a tri-state result to indicate how you want to handle the operation (i.e. save changes, discard changes or cancel what you are doing). For this, I just made my own:
public delegate void DiscardingChangesEvent(object sender, DiscardingChangesEventArgs e);
public class DiscardingChangesEventArgs
{
public DiscardingChangesOperation Operation { get; set; } = DiscardingChangesOperation.Cancel;
}
public enum DiscardingChangesOperation
{
Save,
Discard,
Cancel
}
I tried to come up with a better naming convention, but this was the best I could think of.
So, to putting it in action looks something like this:
ViewModel (this is actually a base class for my CRUD-based view models):
protected virtual void New()
{
// handle case when model is dirty
if (ModelIsDirty)
{
var args = new DiscardingChangesEventArgs(); // defaults to cancel, so someone will need to handle the event to signal discard/save
DiscardingChanges?.Invoke(this, args);
switch (args.Operation)
{
case DiscardingChangesOperation.Save:
if (!SaveInternal())
return;
break;
case DiscardingChangesOperation.Cancel:
return;
}
}
// continue with New operation
}
protected virtual void Delete()
{
var args = new CancelEventArgs();
DeletingRecord?.Invoke(this, args);
if (args.Cancel)
return;
// continue delete operation
}
View:
<UserControl.DataContext>
<vm:CompanyViewModel DeletingRecord="CompanyViewModel_DeletingRecord" DiscardingChanges="CompanyViewModel_DiscardingChanges"></vm:CompanyViewModel>
</UserControl.DataContext>
View code-behind:
private void CompanyViewModel_DeletingRecord(object sender, System.ComponentModel.CancelEventArgs e)
{
App.HandleRecordDeleting(sender, e);
}
private void CompanyViewModel_DiscardingChanges(object sender, DiscardingChangesEventArgs e)
{
App.HandleDiscardingChanges(sender, e);
}
And a couple of static methods that are part of the App class that every view can use:
public static void HandleDiscardingChanges(object sender, DiscardingChangesEventArgs e)
{
switch (MessageBox.Show("Save changes?", "Save", MessageBoxButton.YesNoCancel))
{
case MessageBoxResult.Yes:
e.Operation = DiscardingChangesOperation.Save;
break;
case MessageBoxResult.No:
e.Operation = DiscardingChangesOperation.Discard;
break;
case MessageBoxResult.Cancel:
e.Operation = DiscardingChangesOperation.Cancel;
break;
default:
throw new InvalidEnumArgumentException("Invalid MessageBoxResult returned from MessageBox.Show");
}
}
public static void HandleRecordDeleting(object sender, CancelEventArgs e)
{
e.Cancel = MessageBox.Show("Delete current record?", "Delete", MessageBoxButton.YesNo) == MessageBoxResult.No;
}
Centralizing the dialog box in these static methods lets us easily swap them out for custom dialogs later.