Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Showing a Confirmation on window close in a reactive way

I have an observable that I use for displaying a confirmation dialog, roughly of signature:

IObservable<DialogResult> ShowDialog(string title, string message);

This shows the user the dialog, with a yes / no button combo.

In my main window, I'm accessing the closing event as such:

this.Events().Closing.[Do something here]

I want to be able to:

  1. Show the confirmation dialog when the closing observable fires
  2. Set the CancelEventArgs.Cancel property to true if the user clicks the "no" button.

I've tried straight-up subscribing:

this.Events().Closing.Subscribe(e =>
{
    var res = Dialogs.ShowDialog("Close?", "Really Close?").Wait();
    e.Cancel = res == DialogResult.Ok;
});

But that hangs because of the Wait() call, I've also tried an async variant:

this.Events().Closing.Subscribe(async e =>
{
    var res = await Dialogs.ShowDialog("Close?", "Really Close?");
    e.Cancel = res == DialogResult.Ok;
});

Which is no good, because it just returns right away.

What I really want to do is something like:

this.Events().Closing.ThenDo(_ => Dialogs.ShowDialog("Close?", "Really Close?"))
    .Subscribe((cancelEventArgs, dialogResult) =>
    {
        cancelEventArgs.Cancel == dialogResult == DialogResult.Ok;
    });

But that doesn't exist, I know the key here is in how I combine the two observables, but I've no idea how to do so, and the documentation is a little light on practical examples.

like image 696
Clint Avatar asked May 20 '15 17:05

Clint


1 Answers

You need to block the Closing event handler, hence async (or Rx manipulation) won't help you much here.

But you also need to block it in such a way that UI events are still processed, so the UI doesn't freeze.

The most common solution to that is to use Window.ShowDialog instead of Show, and this code works:

        this.Events().Closing.Subscribe(e =>
        {
            var ret = new Window().ShowDialog();
            e.Cancel = true;
        });

But using that in your ShowDialog Rx method will make its subscribe call block, and that's unlikely what you want (for other cases, in this case it is what you need).

Alternatively you can run a inner dispatcher loop, like this:

        this.Events().Closing.Subscribe(e =>
        {
            var dialog = Dialogs.ShowDialog("Close?", "Really Close?");
            var dispatcherFrame = new DispatcherFrame();
            dialog.Take(1).Subscribe(res => {
                e.Cancel = res == DialogResult.Ok;
                dispatcherFrame.Continue = false;
            });
            // this will process UI events till the above fires
            Dispatcher.PushFrame(dispatcherFrame);
        });

That'll work only if the same Dispatcher is used by both windows.

EDIT:

Alternatively, you can avoid blocking by always canceling the close, and closing the form yourself later, which is maybe more Rx-way, i.e.:

        var forceClose = default(bool);

        this.Events().Closing
            .Where(_ => !forceClose)
            .Do(e => e.Cancel = true)
            .SelectMany(_ => Dialogs.ShowDialog("Close?", "Really Close?"))
            .Where(res => res == DialogResult.Ok)
            .Do(_ => forceClose = true)
            .Subscribe(_ => this.Close());
like image 150
Gluck Avatar answered Oct 25 '22 23:10

Gluck