1

Wpf

I am attempting to delay window closing until all tasks are completed using the async/await library of StephenCleary https://github.com/StephenCleary/AsyncEx.

The event handler delegate and event arguments definitions:

public delegate void CancelEventHandlerAsync(object sender, CancelEventArgsAsync e);

public class CancelEventArgsAsync : CancelEventArgs
{
    private readonly DeferralManager _deferrals = new DeferralManager();

    public IDisposable GetDeferral()
    {
        return this._deferrals.GetDeferral();
    }

    public Task WaitForDefferalsAsync()
    {
        return this._deferrals.SignalAndWaitAsync();
    }
}

Then in the code behind of the NewWindowDialog.xaml, I override the OnClosing event:

public NewWindowDialog()
        {
            InitializeComponent();

        }

        protected override async void OnClosing(System.ComponentModel.CancelEventArgs e)
        {
            e.Cancel = true;
            base.OnClosing(e);
            await LaunchAsync();
        }

        private async Task LaunchAsync()
        {
            var vm =(NewProgressNoteViewModel)DataContext;
            var cancelEventArgs = new CancelEventArgsAsync();
            using (var deferral = cancelEventArgs.GetDeferral())
            {
                // a very long procedure!
                await vm.WritingLayer.CompletionAsync();
            }

        }

Clearly, this fails since e.Cancel = true is executed before the await. So what am I missing to correctly use GetDeferral() to delay the window closing while the tasks are being completed (in WPF).

TIA

Edit: With the help of everybody, I am currently using this. However, does anybody have a good example of the Deferral pattern on window closing?

Thanks to all.

private bool _handleClose = true;
        protected override async void OnClosing(System.ComponentModel.CancelEventArgs e)
        {
            using (new BusyCursor())
            {
                if (_handleClose)
                {
                    _handleClose = false;
                    IsEnabled = false;
                    e.Cancel = true;

                    var vm = (NewProgressNoteViewModel)DataContext;

                    await vm.WritingLayer.SaveAsync();

                    e.Cancel = false;
                    base.OnClosing(e);
                }
            }
        }
Alan Wayne
  • 5,122
  • 10
  • 52
  • 95
  • I don't think you should be calling `base.OnClosing(e)` after `await` (as well as setting `e.Cancel = false` prior to that), because that would be out of sequence. Too see what I mean, check out [how it's called by the Framework](https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Window.cs,dee77f5097cad59d). Your continuation after `await` would be invoked on a completely different stack frame and on another message loop iteration. – noseratio Nov 10 '18 at 00:08
  • @noseration Didn't think about that...but it seems to work correctly?! (And I really don't like forcing a reentry on the same OnClosing method with Close(). ) :) – Alan Wayne Nov 10 '18 at 18:16
  • 1
    note that in the current version of your code you're still not protected from re-entrancy when user clicks on Close in the windows caption or hits Alt-F4. Setting `IsEnabled = false` doesn't prevent that from happening. In which case, `vm.WritingLayer.SaveAsync()` would get called twice or more, with whatever side effects this may leave you with, if any :) – noseratio Nov 13 '18 at 01:56

2 Answers2

1

You don't need a deferral. Just set the CancelEventArgs.Cancel property to true, await the long-running operation and then close. You could use a flag to avoid doing the same thing more than once:

private bool _handleClose = true;
protected override async void OnClosing(System.ComponentModel.CancelEventArgs e)
{
    if (_handleClose)
    {
        e.Cancel = true;
        await Task.Delay(5000);// a very long procedure!
        _handleClose = false;
        Close();
    }
}
mm8
  • 163,881
  • 10
  • 57
  • 88
  • I was coming to that conclusion as well. But I would still be interested in some examples of how to use the Deferral pattern if you know of any. (Google seems very lacking on this). Thanks. – Alan Wayne Nov 08 '18 at 15:57
  • I would only add that for me, changing Close() to e.Cancel = false; base.OnClosing(e); seems to run smoother. – Alan Wayne Nov 08 '18 at 23:23
1

I believe it's a more user-friendly approach to show a modal "Please wait..." message inside your Window.Closing event handler that goes away when the Task is complete. This way, the control flow doesn't leave your Closing handler until it's safe to close the app.

Below is a complete WPF example of how it can be done. Error handling is skipped for brevity. Here's also a related question dealing with a similar problem.

using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;

namespace WpfApp
{
    public partial class MainWindow : Window
    {
        Task _longRunningTask;

        private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            if (_longRunningTask?.IsCompleted != false)
            {
                return;
            }

            var canClose = false;

            var dialog = new Window
            {
                Owner = this,
                Width = 320,
                Height = 200,
                WindowStartupLocation = WindowStartupLocation.CenterOwner,
                Content = new TextBox {
                    Text = "Please wait... ",
                    HorizontalContentAlignment = HorizontalAlignment.Center,
                    VerticalContentAlignment = VerticalAlignment.Center
                },
                WindowStyle = WindowStyle.None
            };

            dialog.Closing += (_, args) =>
            {
                args.Cancel = !canClose;
            };

            dialog.Loaded += async (_, args) =>
            {
                await WaitForDefferalsAsync();
                canClose = true;
                dialog.Close();
            };

            dialog.ShowDialog();
        }

        Task WaitForDefferalsAsync()
        {
            return _longRunningTask;
        }

        public MainWindow()
        {
            InitializeComponent();

            this.Closing += MainWindow_Closing;

            _longRunningTask = Task.Delay(5000);
        }
    }
}
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • 1
    I agree, although for simplicity I used a BusyCursor. Thanks. :) – Alan Wayne Nov 09 '18 at 18:14
  • 1
    @AlanWayne, you still may want to disable the UI while showing the busy cursor to prevent users from clicking around while it's pending :) – noseratio Nov 09 '18 at 20:45
  • 1
    Yes. Please see the code I settled on at the bottom of my question. It became quickly obvious that you are correct; when the close button is pressed repeatedly strange and not good things happen. Thanks. – Alan Wayne Nov 09 '18 at 22:16