Showing a Confirmation on window close in a reactive way
Asked Answered
J

2

6

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.

Jarredjarrell answered 20/5, 2015 at 17:56 Comment(0)
D
3

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());
Demolition answered 20/7, 2015 at 11:18 Comment(0)
V
-1

The best way to combine to observable streams is CombineLatest. This will fire when either one is updated with the last value in each stream.

Channel 9 video on Combine Latest

Vallation answered 16/7, 2015 at 18:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.