Asynchronous Event Processing in WinUI3

Mark Olbert 1 Reputation point
2023-04-29T22:52:35.53+00:00

This is a bit of a conceptual issue, so please bear with the somewhat lengthy description and oddball code. I fully admit I am fundamentally quite confused about cross-thread programming and thread waiting.

I'm struggling with interfacing traditional C# event processing with the Windows App SDK and UI thread updates.

I have a factory class which occasionally needs to request credentials from the end user. The factory class is in a separate library -- it is not part of the application per se. In fact, it is two steps away from the end user application, because the factory is called by a custom control class (which is in its own library).

My basic approach is to raise an event in the factory class and have the control library process it. This currently involves creating a ContentDialog instance and calling it's ShowAsync() method to request credentials.

And that's the heart of the problem. The request for credentials is inherently modal but apparently there is no way to launch a modal dialog box in the Windows App SDK. Requiring the dialog display to be async means, I think, I have to have some way for the factory class to wait until the dialog is displayed, the user fills in information, and closes the dialog.

What I'm currently trying involves incorporating a ManualResetEvent in the event arguments, and then calling its Set() method when the dialog box closes. Along the way I have to marshal the dialog box on the UI thread (the factory class runs on a separate thread).

Unfortunately, this doesn't work. Execution pauses on the WaitOne() call in the factory, but the task that should be launching the dialog box never starts.

// factory class code (context omitted)
var eventArgs = new CredentialsNeededEventArgs( projInfo.Name );
credentialsRequested = true;

CredentialsNeeded?.Invoke( this, eventArgs );
eventArgs.WaitHandle.WaitOne();

// custom control library code
MapControlViewModelLocator.Instance!.ProjectionFactory.CredentialsNeeded += ( _, args ) => { GetCredentials( args ); };

// event handling code
private void GetCredentials( CredentialsNeededEventArgs args )
{
    _curCredRequest = args;
    Task.Run( LaunchDialog );
}

private void LaunchDialog()
{
    _dispatcherCredDlg.TryEnqueue( () =>
    {
        var credDialog = CreateCredentialsDialog();

        if( credDialog != null )
        {
            if( credDialog.ShowAsync().GetResults() == ContentDialogResult.Primary )
            {
                _curCredRequest!.Credentials = ( (ICredentialsDialog) credDialog ).Credentials;
                _curCredRequest.CancelOnFailure = ( (ICredentialsDialog) credDialog ).CancelOnFailure;
            }
            else _curCredRequest!.CancelImmediately = true;
        }

        _curCredRequest!.WaitHandle.Set();
    } );
}

private ContentDialog? CreateCredentialsDialog()
{
    var credDialog = MapControlViewModelLocator.Instance!
        .CredentialsDialogFactory
        .CreateCredentialsDialog(_curCredRequest!.ProjectionName);

    if (credDialog == null)
    {
        _logger?.LogWarning("Could not create credentials dialog for {projection}", _curCredRequest!.ProjectionName);
        _curCredRequest!.CancelImmediately = true;
         return null;
    }

    credDialog.XamlRoot = XamlRoot;

    return credDialog;
}

Windows App SDK
Windows App SDK
A set of Microsoft open-source libraries, frameworks, components, and tools to be used in apps to access Windows platform functionality on many versions of Windows. Previously known as Project Reunion.
800 questions
C#
C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
11,092 questions
{count} votes

1 answer

Sort by: Most helpful
  1. Mark Olbert 1 Reputation point
    2023-05-01T18:07:17.22+00:00

    I was able to solve/avoid the problem by re-writing my library. For the benefit of anyone else struggling with something similar, here's what I did, and the takeaway lesson from it.

    My problem, fundamentally, was the result of trying to "keep trying to authenticate" in a base library by requesting new credentials from a derived library. The split was due to the fact that the base library is designed to be UI-agnostic. It neither knows nor cares what kind of UI framework it's being called from.

    The derived library (and the ultimate app) are >>not<< UI-agnostic. In fact, they are specifically designed to work with WinUI3 aka the Windows App SDK.

    The Windows App SDK encompasses an interesting mix of synchronous and asynchronous code (probably because it's descended from platforms that were developed long before the async/await conceptual framework existed). That mix enormously complicated handling my desire to "keep asking the user for credentials until they validate or give up".

    The solution was recognizing "keep asking the user for credentials" should not start from within the UI-agnostic base library. It should simply focus on trying to authenticate and reporting the results. All of the logic for "keep asking" should be in the UI-aware library (and app), which should simply use the authentication process as a service.

    Once I realized this I rewrote the code base, solved/sidestepped the problem, and got to throw away a bunch of complex code. Which is always, in my experience, a sign that the way you used to be doing something was "wrong".

    0 comments No comments

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.