IVSoftware.Portable.Threading 1.3.0-preview3

Prefix Reserved
This is a prerelease version of IVSoftware.Portable.Threading.
dotnet add package IVSoftware.Portable.Threading --version 1.3.0-preview3                
NuGet\Install-Package IVSoftware.Portable.Threading -Version 1.3.0-preview3                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="IVSoftware.Portable.Threading" Version="1.3.0-preview3" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add IVSoftware.Portable.Threading --version 1.3.0-preview3                
#r "nuget: IVSoftware.Portable.Threading, 1.3.0-preview3"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install IVSoftware.Portable.Threading as a Cake Addin
#addin nuget:?package=IVSoftware.Portable.Threading&version=1.3.0-preview3&prerelease

// Install IVSoftware.Portable.Threading as a Cake Tool
#tool nuget:?package=IVSoftware.Portable.Threading&version=1.3.0-preview3&prerelease                

This package addresses a need that is crucial and common in a test (e.g. MSTest) environment where evaluating asynchronous UI interactions in something like a WPF or Winforms app is often complex and fraught with challenges. These tests might involve stimuli that are either test-driven or interactively user-driven. They may also require monitoring for changes in typically synchronous methods like OnPropertyChanged, or tracking updates in a continuously running polling loop.

I saw the question recently worded as How can async void methods be tested? Or, to put a finer point on it, how can we await the unawaitable?

This is a tried and true approach that I've used extensively for testing my UI application in "just the basic" MSTest environment. My early attempts always seemed to pile additional timing uncertainties on top of the ones I was trying to test. This solution is dirt simple. This helper class that exposes an extension for object that fires a custom static event automatically tagged with the caller method name. There's also an args property that can carry a Dictionary<string, object> or a json payload (for example), and this is to provide context to the MSTest method that is listening to it, plus you have the sender object itself. Taken together, this provides a rich context in which to evaluate this moment in the app's asynchronous life.

One of the simplest examples I can think of would be the ability to await an expected property change from within the synchronous System.Windows.Window.OnPropertyChanged() method in the app under test. You can do this by adding one line to call the OnAwaited extension:

App under test
// <PackageReference Include="IVSoftware.Portable.Threading" Version="*" />
// using IVSoftware.Portable.Threading;
// The synchronous method you want to observe but can't (or shouldn't) call directly. 
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
    base.OnPropertyChanged(e);
    this.OnAwaited(new AwaitedEventArgs(args: new Dictionary<string, object>
    {
        { nameof(DependencyPropertyChangedEventArgs), e }
    }));
}
MSTest

In the test method, the static Awaited event is subscribed just for the duration of this particular test. Once its raised, it can be inspected to see who the sending object is, to see what method actually called it, and to examine whatever rich treasures have been pushed into the args object for detailed analysis.

// using static IVSoftware.Portable.Threading.Extensions;
[TestMethod]
public async Task MyTest_ReturnsData()
{
    SemaphoreSlim awaiter = new SemaphoreSlim(0, 1);
    string? actual = null;
    try
    {
        Awaited += localOnAwaited;

        WPFAppWindow?.CallSomeAsyncVoidMethod();
        // Wait for it to have a deterministic
        // effect after a non-deteministic time.
        Assert.IsTrue(
            condition: await awaiter.WaitAsync(timeout: TimeSpan.FromSeconds(10)),
            "Timed out waiting for property change.");
        Assert.AreEqual(
            expected: "MyExpectedValue",
            actual: actual,
            $"An unexpected value was detected in {nameof(WPFAppWindow)}.OnPropertyChanged().");
    }
    finally
    {
        // CRITICAL to unconditionally unsubscribe
        // from the static method when done.
        Awaited -= localOnAwaited;
    }
    #region L o c a l M e t h o d s
    void localOnAwaited(object? sender, AwaitedEventArgs e)
    {
                object? o;
                switch (e.Caller)
                {
                    // Very common scenario of listening for a
                    // property to change after a UI stimulus.
                    case "OnPropertyChanged":
                        if (e.Args is Dictionary<string, object> args)
                        {
                            if (args.TryGetValue(nameof(DependencyPropertyChangedEventArgs), out o) &&
                            o is DependencyPropertyChangedEventArgs wpfPropertyChanged)
                            {
                                switch (wpfPropertyChanged.Property.Name)
                                {
                                    case "MyTargetProperty":
                                        // The property we've been listening to has changed.
                                        actual = $"{wpfPropertyChanged.NewValue}";
                                        awaiter.Release();
                                        break;
                                }
                            }
                        }
                        break;
                }
    }
    #endregion L o c a l M e t h o d s
}
Use case : Override a property for Test

The design of the AwaitedEventArgs is designed to allow the local listener to inject values that can be utilized upon its return.

public static void SetEnv<T>(this Enum @enum, T value)
{
    var args = new AwaitedEventArgs
    {
        {$"{@enum.GetType().Name}.{@enum}", @enum }
    };
    @enum.OnAwaited(args);
    _env[@enum] = value;
}
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.3.0-preview3 89 9/22/2024
1.2.0 109 9/12/2024
1.1.0 96 9/9/2024

- OnAwaited can now be called without args.
- AwaitedEventArgs can now use Collection Initializer syntax.