Reputation: 43
I need to perform an async operation when the window is closed (basically, to save all my data). The problem is that the program and the UI thread dies too soon and the task never completes.
This is the code example:
public partial class MainWindow : Window
{
public MainWindow()
{
Closed += MainWindow_Closed;
_items = new(Enumerable.Range(0,10));
}
private ObservableCollection<int> _items;
private async void MainWindow_Closed(object? sender, EventArgs e)
{
await LongTask();
}
private async Task LongTask()
{
await Task.Run(() =>
{
Trace.WriteLine("LongTask Start");
Application.Current.Dispatcher.BeginInvoke(() =>
{
// do some action in the UI thread
_items.Clear();
Trace.WriteLine("Cleared elements");
});
Thread.Sleep(5000);
Trace.WriteLine("LongTask End");
});
}
}
and the console output never prints 'Cleared elements' nor 'LongTask End'. I have put also an action inside the async task that needs the UI thread to complete the operation, which is needed in my use case. (Thus one cannot use LongTask().GetAwaiter().GetResult() in the main thread as it would result in a deadlock).
Thanks for the help!!
Upvotes: 1
Views: 158
Reputation: 18328
@IV answer is correct. Just adding some additional context. The key here is a difference in Closing versus Closed.
event CancelEventHandler Closing;
event EventHandler Closed;
In Closed, the process is terminating and any process you start here is doomed to be terminated with the main thread. In Closing, you have the option to intercept the operation and perform additional tasks if necessary.
Upvotes: 2
Reputation: 9438
As a proof of concept, here's one way you could implement the exceedingly common and routine goal of asynchronously saving your data upon app close. The key here is the _confirmClosure
bool, because if the user wants to "save and close" then we're going to:
Close()
.Close()
again, this time with _confirmClosure
set to false
.public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Closing += async (sender, e) =>
{
if (_confirmClosure)
{
switch (
MessageBox.Show(
"Do you want to save before closing?",
"Confirm Exit",
MessageBoxButton.YesNoCancel,
MessageBoxImage.Question))
{
case MessageBoxResult.Yes:
e.Cancel = true;
await Dispatcher.BeginInvoke(async () =>
{
try
{
Mouse.OverrideCursor = Cursors.Wait;
IsEnabled = false; // Prevent any more calls.
await DataContext.Save();
_confirmClosure = false;
Close();
}
finally
{
Mouse.OverrideCursor = null;
}
});
break;
case MessageBoxResult.No:
break;
case MessageBoxResult.Cancel:
e.Cancel = true;
break;
}
}
};
}
bool _confirmClosure = true;
new MainPageViewModel DataContext => (MainPageViewModel)base.DataContext;
}
Minimal VM for Test
We'll make a "big" list of 10000 items, then save it to a file upon close.
public class MainPageViewModel : INotifyPropertyChanged
{
public ObservableCollection<Item> Items { get; } = new ObservableCollection<Item>(
Enumerable.Range(1, 10000)
.Select(_=>new Item { Id = _, Name = $"Item {_}" }));
public event PropertyChangedEventHandler PropertyChanged;
internal async Task Save()
{
await Task.Run(() =>
{
var path = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"StackOverflow",
Assembly.GetEntryAssembly()?.GetName()?.Name ?? "SaveThenClose",
"list-data.json");
Directory.CreateDirectory(Path.GetDirectoryName(path));
var json = JsonConvert.SerializeObject(Items, Formatting.Indented);
File.WriteAllText(path, json);
});
// Add a few seconds for good measure, just for demo purposes.
await Task.Delay(TimeSpan.FromSeconds(2.5));
}
}
public class Item
{
public int Id { get; set; }
public string? Name { get; set; }
}
<Window x:Class="save_list_then_close.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:save_list_then_close"
mc:Ignorable="d"
Title="MainWindow" Height="250" Width="400"
WindowStartupLocation="CenterScreen">
<Window.DataContext>
<local:MainPageViewModel/>
</Window.DataContext>
<Grid>
<DataGrid
Name="dataGrid"
ItemsSource="{Binding Items}"
AutoGenerateColumns="True"
IsReadOnly="True"
AlternatingRowBackground="LightGray"/>
</Grid>
</Window>
Upvotes: 3