Git Product home page Git Product logo

Comments (7)

mlimber avatar mlimber commented on August 26, 2024 1

It appears the NUnit team agrees with you. :-) They've fixed it in 3.11.

from mvvm.

StephenCleary avatar StephenCleary commented on August 26, 2024

Nito.Mvvm assumes it is running on a single-threaded context, like a UI context. To put it another way, all of the types have a "thread affinity" - they can only be accessed from the thread they were created on. The exception you're seeing is a cross-thread exception.

Using async/await in both NUnit and Console apps can switch threads. In order to prevent this, you'll need to use an async-compatible single-threaded context like AsyncContext. Something like this:

[Test]
public void Fixed()
{
  AsyncContext.Run(async () =>
  {
    AsyncCommand command = new AsyncCommand(
        async () => { await Task.Delay(1000); });
    await command.ExecuteAsync(null);
  });
}

from mvvm.

kevkeller avatar kevkeller commented on August 26, 2024

Hm, I thought not using ConfigureAwait(false) would prevent switching threads. I've already been playing around with [SingleThreaded] and [Apartment(ApartmentState.STA)] to no avail, but your solution seems to do the trick.

Thanks a lot!

from mvvm.

mlimber avatar mlimber commented on August 26, 2024

🧟‍♀️ Zombie thread resurrected for posterity. 🧟‍

Another option is to remove thread affinity requirement by duplicating Nito.Mvvm.Core's StringCanExecuteChanged (and/or WeakCanExecuteChanged) to remove thread affinity:

public sealed class AsyncCommandCanExecuteChanged : ICanExecuteChanged
{
    /// <summary>
    /// A factory function for creating CanExecuteChanged objects.
    /// </summary>
    /// <param name="sender">The object to emit the events.</param>
    /// <returns>The newly created <see cref="AsyncCommandCanExecuteChanged"/> object.</returns>
    public static ICanExecuteChanged Create( object sender )
    {
        return new AsyncCommandCanExecuteChanged( sender );
    }
        
    /// <summary>
    /// The sender of the <c>ICommand.CanExecuteChanged</c> event.
    /// </summary>
    private readonly object _sender;

    /// <summary>
    /// The collection of delegates for <see cref="CanExecuteChanged"/>.
    /// </summary>
    private event EventHandler CanExecuteChangedEvent;

    /// <summary>
    /// Creates a new strong-event implementation of <c>ICommand.CanExecuteChanged</c>.
    /// </summary>
    /// <param name="sender">The sender of the <c>ICommand.CanExecuteChanged</c> event.</param>
    public AsyncCommandCanExecuteChanged( object sender )
    {
        _sender = sender;
    }

    /// <inheritdoc />
    /// <summary>
    /// Provides notification that the result of <c>ICommand.CanExecute</c> may be different.
    /// </summary>
    public event EventHandler CanExecuteChanged
    {
        add => CanExecuteChangedEvent += value;
        remove => CanExecuteChangedEvent -= value;
    }

    /// <inheritdoc />
    /// <summary>
    /// Raises the <see cref="CanExecuteChanged" /> event.
    /// </summary>
    public void OnCanExecuteChanged()
    {
        CanExecuteChangedEvent?.Invoke( _sender, EventArgs.Empty );
    }
}

And then pass it into your commands at construction, e.g.:

DeleteSelectedObjectsCommand = new CustomAsyncCommand( 
    DeleteSelectedObjectsAsync,
    _ => HasSelection,
    AsyncCommandCanExecuteChanged.Create );

Then the test can await as usual:

[Test]
public async Task TestDeletion()
{
    // ... assembly here ...
    await _viewModel.DeleteSelectedObjectsCommand.ExecuteAsync( null );
    // ... asserts here ...
}

Of course you may find you get bit by thread affinity being off, but it could also be not required in your context, as (I think) it is in mine. (Perhaps Stephen will reply that this is ill-advised, but I'll wait and see. :-) )

from mvvm.

StephenCleary avatar StephenCleary commented on August 26, 2024

Well, yeah, the thread affinity was deliberately put in because lots of the code assumes it is running on the same thread. More specifically, events are raised using the simple pattern rather than using an extra variable, and all private state variables are completely unprotected (no lock, volatile, or barriers of any kind).

So, disabling the thread affinity guard voids your warranty. :)

I would still recommend using AsyncContext.Run, or an alternative test framework like xUnit that provides its own context.

from mvvm.

mlimber avatar mlimber commented on August 26, 2024

Ah, so you think this is a defect in NUnit?

from mvvm.

StephenCleary avatar StephenCleary commented on August 26, 2024

Not a defect per se. Just a different way of doing things that makes testing asynchronous UI code harder.

from mvvm.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.