Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question on how to show a modal dialog synchronously #4810

Closed
derekantrican opened this issue Oct 6, 2020 · 23 comments
Closed

Question on how to show a modal dialog synchronously #4810

derekantrican opened this issue Oct 6, 2020 · 23 comments

Comments

@derekantrican
Copy link
Contributor

derekantrican commented Oct 6, 2020

In WPF, the Window.ShowDialog(); method blocks the thread and continues when the dialog is closed (the same way MessageBox.Show(); works in WinForms). In Avalonia, the docs indicate that Window.ShowDialog() executes asynchronously, meaning the thread can continue before the dialog is closed.

Maybe this is a matter of me not being good at async vs sync coding but how would I produce a similar result to WPF/WinForms but using the way Avalonia has been architected? I want to call ShowDialog from a non-async method and have execution pause until the dialog is closed.

I've tried the following:

Task.WhenAll(dialog.ShowDialog(parent));, dialog.ShowDialog(parent).Result;, and dialog.ShowDialog(parent).Wait(); block the dialog thread and prevents the dialog from loading.

Task.Run(() => ShowDialog()).GetAwaiter().GetResult();
private async Task ShowDialog()
{
    Dialog dialog = new Dialog();
    await dialog.ShowDialog(parent);
}

throws an exception of System.InvalidOperationException: 'Call from invalid thread'

What is the proper way to do this?

@sn4k3
Copy link
Contributor

sn4k3 commented Oct 6, 2020

You have it right, just run in main UI thread (Outside the task)

So, in your main UI code:

async void callMyDialog()
{
   Dialog dialog = new Dialog();
   await dialog.ShowDialog(this);
}

@derekantrican
Copy link
Contributor Author

derekantrican commented Oct 6, 2020

Yeah, I've got that much. But how do I wait for that async method to complete from a non-async method? eg:

void showDialog()
{
    callMyDialog(); //I want to block the showDialog method from executing at this point until the dialog is closed
}

async void callMyDialog()
{
   Dialog dialog = new Dialog();
   await dialog.ShowDialog(this);
}

I've tried using callMyDialog().Wait() or similar (as mentioned above) but then I just get a blank dialog:
image

@sn4k3
Copy link
Contributor

sn4k3 commented Oct 6, 2020

All your calls must be awaitable and marked as async.
any wait such as callMyDialog().Wait() will freeze your window

async void showDialog()
{
    await callMyDialog(); //I want to block the showDialog method from executing at this point until the dialog is closed
}

async void callMyDialog()
{
   Dialog dialog = new Dialog();
   await dialog.ShowDialog(this);
}

await showDialog();

Example:
https://github.com/sn4k3/UVtools/blob/d9cd0022caf0b55d90c0e1ff5f458f7aecdb000b/UVtools.WPF/MainWindow.axaml.cs#L584

@derekantrican
Copy link
Contributor Author

That's exactly the point of this question: how do I do this without my method being async? Is it impossible?

@sn4k3
Copy link
Contributor

sn4k3 commented Oct 6, 2020

That's exactly the point of this question: how do I do this without my method being async? Is it impossible?

but why that requirement? why you want your methods not to be async?
if you call dialog.ShowDialog(this); without await the ui code executes below, but a await keyword require method be marked as async.

Anyway, you can just call dialog.ShowDialog(this);, and attach an OnClose event if you want to process some data from it in case you dont want to await. eg:

var window = new AboutWindow();
window.OnClosed += (...) => My code here after window close
window.ShowDialog(this);
// This will continue to execute after your last call, without wait for dialog close, 
// but will also lock the background window from user interaction

Also note you can't call UI components from another thread.

@MarkusAmshove
Copy link

How do you trigger the synchronous showDialog method? I think the misunderstanding is how to execute your stuff asynchronous in the first place.

Does it e.g. happen from a click/Command?

@workgroupengineering
Copy link
Contributor

void showDialog()
{
   dialog.ShowDialog(this).GetAwaiter().GetResult();
}

@jp2masa
Copy link
Contributor

jp2masa commented Oct 6, 2020

A workaround (adapted from #857 (comment)) is this:

https://github.com/jp2masa/dotnet-properties/blob/4584f2f4245d999eb270ece5ec013f9f97c0f8bf/src/dotnet-properties/Services/DialogService.cs#L28-L32

It will block until the dialog closes.

@derekantrican
Copy link
Contributor Author

derekantrican commented Oct 6, 2020

@MarkusAmshove There's a long line of synchronous methods that call this - it's not just a matter of making the caller (or even the caller's caller) async

@workgroupengineering I had tried that before - that still results in the empty dialog box issue where it prevents the dialog UI from loading.

@jp2masa Ah, thanks to your help I got it! Here is what I ended up using:

public void ShowDialog()
{
    using (var source = new CancellationTokenSource())
    {
        new Dialog().ShowDialog(parent).ContinueWith(t => source.Cancel(), TaskScheduler.FromCurrentSynchronizationContext());
        Dispatcher.UIThread.MainLoop(source.Token);
    }
}

And then I can iterate on top of that to add the dialog message and other things

@sn4k3
Copy link
Contributor

sn4k3 commented Oct 6, 2020

@derekantrican if that works fine for you and you use it for multiple windows, consider making extension to ease your code.
Something like:

public static void ShowDialogSync(this Window window, Window parent = null)
        {
            if (parent is null) parent = window;
            using (var source = new CancellationTokenSource())
            {
                window.ShowDialog(parent).ContinueWith(t => source.Cancel(), TaskScheduler.FromCurrentSynchronizationContext());
                Dispatcher.UIThread.MainLoop(source.Token);
            }
        }

        public static T ShowDialogSync<T>(this Window window, Window parent = null)
        {
            if (parent is null) parent = window;
            using (var source = new CancellationTokenSource())
            {
                var task = window.ShowDialog<T>(parent);
                task.ContinueWith(t => source.Cancel(), TaskScheduler.FromCurrentSynchronizationContext());
                Dispatcher.UIThread.MainLoop(source.Token);
                return task.Result;
            }

            return default(T);
        }

@derekantrican
Copy link
Contributor Author

After doing a bunch of iterating since yesterday (and stumbling into a number of issues) I've improved upon the solution here in a way that still provides the desired functionality (acting like Window.ShowDialog() in WinForms & WPF - pausing the current thread until the dialog is closed) but also works simultaneously from the UI thread and from a separate thread like a BackgroundWorker.DoWork & BackgroundWorker.RunWorkerAsync setup.

private MyDialogResult ShowDialog(DialogViewModel dialogViewModel)
{
    MyDialogResult result = MyDialogResult.Cancel;
    using (CancellationTokenSource source = new CancellationTokenSource())
    {
        if (Dispatcher.UIThread.CheckAccess()) //Check if we are already on the UI thread
        {
            Dialog dialog = new Dialog(dialogViewModel);
            dialog.ShowDialog<MyDialogResult>(Parent).ContinueWith(t =>
            {
                result = t.Result;
                source.Cancel();
            });

            Dispatcher.UIThread.MainLoop(source.Token);
        }
        else
        {
            Dispatcher.UIThread.InvokeAsync(() =>
            {
                Dialog dialog = new Dialog(dialogViewModel);
                dialog.ShowDialog<MyDialogResult>(Parent).ContinueWith(t =>
                {
                    result = t.Result;
                    source.Cancel();
                });
            });

            while (!source.IsCancellationRequested) { } //Loop until dialog is closed
        }
    }

    return result;
}

Of course, this is based on a MVVM format and could also be "extension-ized" like @sn4k3 's suggestion but should give a good starting point if anyone else comes across this

@derekantrican
Copy link
Contributor Author

For anyone coming here in the future, this is a bad idea if you plan to have your application run on MacOS: #5229 . Use async dialogs instead

@danipen
Copy link

danipen commented May 14, 2021

The following solution provided by @grokys worked for us.

https://github.com/grokys/ShowDialogSyncAvalonia/blob/master/ShowDialogSyncAvalonia/SyncDialogExtensions.cs

I tested it in macOS high Sierra, Windows 10, Ubuntu 19.04.

@thomaslevesque
Copy link

The Dispatcher.UIThread.MainLoop trick doesn't produce the desired result: the window is not modal, I can still interact with the parent window...

@thomaslevesque
Copy link

The Dispatcher.UIThread.MainLoop trick doesn't produce the desired result: the window is not modal, I can still interact with the parent window...

Actually, it looks like the problem is with MessageBox.Avalonia. When using a simple Window, it works as expected.

@marcussacana
Copy link

marcussacana commented May 23, 2022

The code provided grokys worked, but a strange bug happens here,
After the ShowDialog returns, the window still open while the event method returns...

My program look like this:

[1] Async Event (ButonClicked) => [2] calls a sync function => [3] sync function calls ShowDialog

after the ShowDialog returns, the window keep visible
while the Async Event don't return as well.

To be able to hide the window at least, I call Hide() with Dispatcher before Close() and it's fine.

(Tested under Linux)

Why I call a sync function?
The code will process data in a secondary thread since will take a while to process the data.
But the problem, while the program analyze the data, he found missing info, at that time
the background code will call a ShowDialog asking for the required data if is missing.
Isn't only that, have other method that is hard to use async too.

@SCLDGit
Copy link
Contributor

SCLDGit commented Jun 20, 2023

Running into this issue in a case where I need to spawn a PIN entry dialog from a callback delegate. I have no control over the library expecting the delegate, and the delegate cannot be async. A simple way of creating a modal dialog that stops the main thread until it returns would be nice to have.

@maxkatz6
Copy link
Member

maxkatz6 commented Jun 21, 2023

@SCLDGit on desktop (and only desktop) you can create this extension in 11.0:

internal static T WaitOnDispatcherFrame<T>(this Task<T> task)
{
    if (!task.IsCompleted)
    {
        var frame = new DispatcherFrame();
        _ = task.ContinueWith(static (_, s) => ((DispatcherFrame)s).Continue = false, frame);
        Dispatcher.UIThread.PushFrame(frame);
    }
    
    return task.GetAwaiter().GetResult();
}

But again, if it's possible, it should be avoided even at high cost.

@timunie
Copy link
Contributor

timunie commented Jun 21, 2023

My 2 cents here:

  • Display an overlay with a waiting message or similar
  • Open the dialog. If the dialog closes, inform the UI about the status
  • If positive, hide the overlay

Check out AwsomeAvalonia for inspiration (for example Aura.UI). Good luck.

@OmarAbdullwahhab
Copy link

OmarAbdullwahhab commented Mar 1, 2024

I know it is too late but here is what is the solution using Version 11.x (do not know if its the same for previous versions )
When showing a Window use
var result = await xxWindow.ShowDialog<TResponse>(xxOwnerWindow);
Where TResponse is the response type you want return from xxWindow .
And when closing the xxWindow (for example you clicked a button in that window ) pass the result you want to the Close method as follows
this.Close(rr);
Where rr is a variable of TResponse type

@danipen
Copy link

danipen commented May 3, 2024

on desktop (and only desktop) you can create this extension in 11.0:

internal static T WaitOnDispatcherFrame<T>(this Task<T> task)
{
    if (!task.IsCompleted)
    {
        var frame = new DispatcherFrame();
        task.ContinueWith(static (_, s) => ((DispatcherFrame)s).Continue = false, frame);
        Dispatcher.PushFrame(frame);
    }
    
    return task.GetAwaiter().GetResult();
}

@maxkatz6 using this approach seems to work, however, we're experiencing issues with TextBoxes, at least in macOS.

When we display a dialog using this mechanism, and the dialog has a TextBox it happens that when you hit a key, the character is not visible in the TextBox until you press another key again, or you move the mouse to hover the TextBox. It's like hitting the key is not forcing a repaint of the control, and hitting it again, then it's repainted.

input.mov

Is there any workaround for this? Thanks!

@kekekeks
Copy link
Member

kekekeks commented May 3, 2024

Please try the latest nightly, we've recently fixed an issue with timers not ticking in a nested event loop on macOS if the loop was started from a timer callback - #15425

@danipen
Copy link

danipen commented May 6, 2024

@kekekeks @maxkatz6 yes, #15425 fixes the issue. Could you please backport it to 11.0.x to be included in the next release?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests