admin管理员组

文章数量:1312978

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!!

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!!

Share Improve this question edited Feb 1 at 17:31 Ivan Petrov 5,1202 gold badges11 silver badges23 bronze badges asked Feb 1 at 17:22 Juan Manzanero TorricoJuan Manzanero Torrico 434 bronze badges 12
  • 1 Okay, when the window is closed, you need to perform an asynchronous operation. Why do you think so? It does not seem to make sense. Normally, you need to perform a synchronous operation because the order of execution is important here. Apparently, the code sample with a 5-second sleep is experimental. Why would you need it in reality? I don't think you need it. Long task? So what? This is the end of lifetime anyway, you don't need the application to be responsive. – Sergey A Kryukov Commented Feb 1 at 17:49
  • 1 Related: Awaiting Asynchronous function inside FormClosing Event. – Theodor Zoulias Commented Feb 1 at 17:59
  • 4 If you have to save your data before a window is closed, you usually handle the [Window].Closing event, so you can cancel the closing procedure, if that becomes necessary, or by user choice. Not clear, though, why you would do some action related to the UI itself, as clearing a collection of items, unless it's just an example. Anyway, if you await a procedure in the Closed event, the flow moves out the event handler in the meanwhile, so the window closes – Jimi Commented Feb 1 at 18:00
  • 1 Async void versus async Task. "Async void is 'ideally' suited for 'event handlers'". (i.e. What do you expect to do with "Task" when it's "all over"? Answer: nothing) – Gerry Schmitz Commented Feb 1 at 18:50
  • 1 @SergeyAKryukov the obvious reason to use asynchronous code is for keeping the UI responsive, by not blocking the UI thread. A frozen window that is not redrawing itself when covered and uncovered by other windows, is a bad user experience. – Theodor Zoulias Commented Feb 1 at 22:21
 |  Show 7 more comments

2 Answers 2

Reset to default 3

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:

  • Disable the UI to prevent any chance of reentry, but cancel the Close().
  • Asynchronously execute the save.
  • Then, after awaiting the save, call 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/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats./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>

@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.

本文标签: cWPF Asynchronous operation on window closeStack Overflow