The Core Pattern
The key insight is that App_Closing (via Window.Closing or Application.SessionEnding) gives you a CancelEventArgs, so you can cancel the close, let the async save finish, then close programmatically.
Implementation
// MainWindow.xaml.cs
public partial class MainWindow : Window
{
private bool _isSaveHandled = false;
protected override void OnClosing(CancelEventArgs e)
{
// If the save was already handled (second pass), let it close
if (_isSaveHandled)
{
base.OnClosing(e);
return;
}
// Cancel the close immediately so we control the flow
e.Cancel = true;
// Handle the save prompt asynchronously
_ = HandleClosingAsync();
}
private async Task HandleClosingAsync()
{
var result = ShowSavePrompt();
switch (result)
{
case MessageBoxResult.Yes:
try
{
await ExecuteSaveAsync();
_isSaveHandled = true;
this.Close(); // Re-trigger closing, this time _isSaveHandled = true
}
catch (Exception ex)
{
MessageBox.Show(
$"Save failed: {ex.Message}\nClose anyway?",
"Save Error",
MessageBoxButton.YesNo,
MessageBoxImage.Error ) == MessageBoxResult.Yes ? ForceClose()
: /* stay open */ _ = Task.CompletedTask;
}
break;
case MessageBoxResult.No:
// Discard changes and close
_isSaveHandled = true;
this.Close();
break;
case MessageBoxResult.Cancel:
// Do nothing — close was already cancelled
break;
}
}
private MessageBoxResult ShowSavePrompt()
{
return MessageBox.Show(
"You have unsaved changes. Do you want to save before closing?",
"Save Changes?",
MessageBoxButton.YesNoCancel,
MessageBoxImage.Warning );
}
private async Task ExecuteSaveAsync()
{
// Your add-in DB save logic here
await YourAddIn.SaveToDatabaseAsync();
}
private void ForceClose()
{
_isSaveHandled = true;
this.Close();
}
}
Why e.Cancel = true First?
If you await anything inside OnClosing synchronously, the event returns before the await completes and the window closes anyway. By cancelling immediately and re-triggering Close() after the save, you get full control over the lifecycle.
Handling the HasUnsavedChanges Guard
You probably don't want the prompt to appear if nothing changed:
protected override void OnClosing(CancelEventArgs e)
{
if (_isSaveHandled || !YourAddIn.HasUnsavedChanges)
{
base.OnClosing(e);
return;
}
e.Cancel = true;
_ = HandleClosingAsync();
}
If You're Using Application.SessionEnding (System Shutdown)
This event has very limited time — avoid async DB saves here. Instead, do a synchronous, fast save or save to a local recovery file:
private void App_SessionEnding(object sender, SessionEndingCancelEventArgs e)
{
// Keep it fast and synchronous
YourAddIn.SaveRecoverySnapshot();
}
Summary of the Pattern
User Action Behavior
| Yes | Cancel close → await save → re-close |
| No | Cancel close → skip save → re-close |
| Cancel | Close stays cancelled, app stays open |
| System shutdown | Fast sync save only |
The _isSaveHandled flag acting as a re-entry guard is the cleanest way to avoid infinite loops when you call this.Close() from within the closing handler.