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

Add AsyncCommand from JT #43

Merged
merged 7 commits into from
Aug 19, 2019
Merged

Add AsyncCommand from JT #43

merged 7 commits into from
Aug 19, 2019

Conversation

jamesmontemagno
Copy link
Owner

@jamesmontemagno jamesmontemagno commented Aug 15, 2019

@brminnick
Copy link

brminnick commented Aug 16, 2019

Hey James!

I've been doing a lot of work lately with AsyncCommand, also inspired by John Thiret: https://github.com/brminnick/AsyncAwaitBestPractices/blob/Release-v3.1.0/Src/AsyncAwaitBestPractices.MVVM/AsyncCommand.cs

I've been iterating & improving this implementation for about a year and it's all available on NuGet: https://www.nuget.org/packages/AsyncAwaitBestPractices.MVVM/

Here's a few tips/gotchyas I learned when implementing AsyncCommand:

  • Subscribing to CanExecuteChanged can cause memory leaks for users
    • I recommend using a WeakReference, like I've implemented with WeakEventManager to avoid introducing any memory leaks into a user's code base
  • Validating the parameter passed into ICommand.Execute(object parameter) for IAsyncCommand<T>
  • In FireAndForgetSafeAsync , use .ConfigureAwait(false), e.g. await task.ConfigureAwait(false)
  • Make sure to throw the exception in FireAndForgetSafeAsync if handler is null
    • If the exception is not handled, it should be rethrown, otherwise it will never be surfaced to the user
  • In FireAndForgetSafeAsync, exception filters are more performant than catching an exception every time
    • If you check out the IL, catch (Exception e) when (handler != null) { handler.HandleError(e); } compiles down to more performant code than catch (Exception e){ handler?.HandleError(e); }

readonly Action<object> execute;
readonly WeakEventManager weakEventManager = new WeakEventManager();
/// <summary>
/// Implementaiton of ICommand

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo => Implementation

{
// The parameter isn't null, so we don't have to worry whether null is a valid option
valid = o is T;
internal static bool IsValidParameter<T>(object o)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could find a better name that actually indicates for which class we are checking if the parameter is a valid one.

[TestClass]
public class WeakEventManagerTests
{
static int count;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I usually use Xunit which runs tests randomly and in parallel therefore seeing static in a unit test class always triggers a warning in my head. The comment is here just in case the behavior is similar and could cause breaking tests.

/// <summary>
/// Initializes a new instance of the <see cref="T:MvvmHelpersInvalidCommandParameterException"/> class.
/// </summary>
/// <param name="excpectedType">Excpected parameter type for AsyncCommand.Execute.</param>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo => excpectedType and Excpected

/// <summary>
/// Initializes a new instance of the <see cref="T:MvvmHelpers.InvalidCommandParameterException"/> class.
/// </summary>
/// <param name="excpectedType">Excpected parameter type for AsyncCommand.Execute.</param>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo => excpectedType and Excpected

@jamesmontemagno jamesmontemagno merged commit a37c6ce into master Aug 19, 2019
Copy link

@brminnick brminnick left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Breaking changes
    • Update minimum .NET Standard dependency to .NET Standard 2.0 because of IsValueType
  • Performance recommendations
    • Using catch filters
    • Using is null instead of == null
  • Philosophical recommendations
    • Remove the Async suffix from SafeFireAndForgetAsync
    • Don't catch an Exception in SafeFireAndForget when onException is null
    • Make WeakEventManager internal

}

// Not a Nullable, if it's a value type then null is not valid
valid = !t.GetTypeInfo().IsValueType;
Copy link

@brminnick brminnick Aug 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IsValueType is only available in .NET Standard 2.0: https://docs.microsoft.com/dotnet/api/system.type.isvaluetype.

It looks like MvvmHelpers' current minimum dependency is .NET Standard 1.0, so make sure to bump it up to .NET Standard 2.0, or add the .NET Standard 2.0 NuGet as a dependency.

Here's the support for IsValueType compared to the current dependencies for MvvmHelpers:

  • .NET Core
    • 3.0 Preview8, 2.2, 2.1, 2.0
  • .NET Framework
    • 4.8, 4.7.2, 4.7.1, 4.7, 4.6.2, 4.6.1, 4.6, 4.5.2, 4.5.1, 4.5, 4.0, 3.5, 3.0, 2.0, 1.1
  • .NET Standard
    • 2.1-Preview, 2.0
  • Xamarin.Android
    • 7.1
  • Xamarin.iOS
    • 10.8
      Xamarin.Mac
    • 3.0

Screen Shot 2019-08-19 at 11 07 47 AM

{
await task.ConfigureAwait(continueOnCapturedContext);
}
catch (Exception ex)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Careful - this is dangerous because it catches every exception thrown by the Task.

I recommend not catching the Exception if the user has not implemented onException because it means that they don't want to gracefully handle the exception. I like to think of onException as the way the user implements a try/catch for FireAndForgetAsync; if they haven't implemented onException, don't catch any exceptions; if they have, catch 'em (all 😜).

try
{
    await task.ConfigureAwait(continueOnCapturedContext);
}
catch (Exception ex)
{
    if(onException is null)
        throw;

    onException.Invoke(ex);
}

Same code, but optimized using Catch filters as recommended by the great Bill Wagner: brminnick/AsyncAwaitBestPractices#8

try
{
    await task.ConfigureAwait(continueOnCapturedContext);
}
catch (Exception ex) when (ex != null)
{
    onException.Invoke(ex);
}


namespace MvvmHelpers
{
public class WeakEventManager
Copy link

@brminnick brminnick Aug 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend making this class internal, because otherwise devs will start using it and you'll beholden to maintaining a WeakEventManager despite this library focusing on MVVM. I implemented it in AsyncAwaitBestPractices, and I wish I had kept it internal so that only AsyncCommand would use it. Here's why:

Story Time

I released AsyncAwaitBestPracitces, with WeakEventManager public, and shortly after its release someone asked me for WeakEventManager<T> for EventHandler<T>- sure, no problem!

But then someone pointed out that not every event is an EventHandler and that events can also be a Delegate (like PropertyChangedEventHandler which is used by NotifyPropertyChanged), so I added support for Delegate.

But THEN someone pointed out that Action can also be used for events, and so I added support for event Action. But this was easier said than done, because now I needed to add an overloaded method for WeakEventManager.HandleEvent because Action and Action<T> doesn't consume an object, so I also ended up creating a custom Exception to throw when a dev uses the incorrect overloaded method for HandleEvent: https://github.com/brminnick/AsyncAwaitBestPractices/blob/master/Src/AsyncAwaitBestPractices/WeakEventManager/EventManagerService.cs#L58

tl;dr - save yourself the headaches and make this internal

if (IsNullOrEmpty(eventName))
throw new ArgumentNullException(nameof(eventName));

if (handler == null)
Copy link

@brminnick brminnick Aug 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change handler == null to handler is null to squeeze out some more performance benefits: https://www.gullberg.tk/blog/is-null-versus-null-in-c/

tl;dr Pattern Matching is faster than Equality comparison in the .NET IL

if (IsNullOrEmpty(eventName))
throw new ArgumentNullException(nameof(eventName));

if (handler == null)
Copy link

@brminnick brminnick Aug 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change handler == null to handler is null to squeeze out some more performance benefits: https://www.gullberg.tk/blog/is-null-versus-null-in-c/

tl;dr Pattern Matching is faster than Equality comparison in the .NET IL


var subscriber = subscription.Subscriber.Target;

if (subscriber == null)
Copy link

@brminnick brminnick Aug 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change subscriber == null to subscriber is null to squeeze out some more performance benefits: https://www.gullberg.tk/blog/is-null-versus-null-in-c/

tl;dr Pattern Matching is faster than Equality comparison in the .NET IL

if (IsNullOrEmpty(eventName))
throw new ArgumentNullException(nameof(eventName));

if (handler == null)
Copy link

@brminnick brminnick Aug 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change handler == null to handler is null to squeeze out some more performance benefits: https://www.gullberg.tk/blog/is-null-versus-null-in-c/

tl;dr Pattern Matching is faster than Equality comparison in the .NET IL

if (IsNullOrEmpty(eventName))
throw new ArgumentNullException(nameof(eventName));

if (handler == null)
Copy link

@brminnick brminnick Aug 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change handler == null to handler is null to squeeze out some more performance benefits: https://www.gullberg.tk/blog/is-null-versus-null-in-c/

tl;dr Pattern Matching is faster than Equality comparison in the .NET IL

eventHandlers.Add(eventName, targets);
}

if (handlerTarget == null)
Copy link

@brminnick brminnick Aug 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change handlerTarget == null to handlerTarget is null to squeeze out some more performance benefits: https://www.gullberg.tk/blog/is-null-versus-null-in-c/

tl;dr Pattern Matching is faster than Equality comparison in the .NET IL

WithTimeout(task, (int)timeout.TotalMilliseconds);

#pragma warning disable RECS0165 // Asynchronous methods should return a Task instead of void
public static async void SafeFireAndForgetAsync(this Task task, Action<Exception> onException = null, bool continueOnCapturedContext = false)
Copy link

@brminnick brminnick Aug 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend removing the Async suffix because it may confuse devs who then try to await SafeFireAndForgetAsync which cannot be done.

Instead, change it to SafeFireAndForget.

valid = o is T;

if (!valid)
throw new InvalidCommandParameterException(typeof(T), null);
Copy link

@brminnick brminnick Aug 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we know o !=null, we can pass in its Type to InvalidCommandParameterException.

throw new InvalidCommandParameterException(typeof(T), o.GetType());

In the exception message, this will tell the user both the Expected Type and the Actual Type.

Example Error Message:

Unhandled Exception: MvvmHelpers.Exceptions.InvalidCommandParameterException: Invalid type for parameter. Expected Type: System.Int32, but received Type: System.String

valid = !t.GetTypeInfo().IsValueType;

if (!valid)
throw new InvalidCommandParameterException(typeof(T), null);
Copy link

@brminnick brminnick Aug 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, o is null, but passing in null to new InvalidCommandParameterException(Type expectedType, Type actualType) creates a confusing error message:

Unhandled Exception: MvvmHelpers.Exceptions.InvalidCommandParameterException: Invalid type for parameter. Expected Type: System.Int32, but received Type: 
   at MvvmHelpers.Commands.CommandUtils.IsValidCommandParameter[T](Object o)

I recommend adding additional constructors for InvalidCommandParameterException that only take in one Type parameter:

/// <summary>
/// Initializes a new instance of the <see cref="T:TaskExtensions.MVVM.InvalidCommandParameterException"/> class.
/// </summary>
/// <param name="excpectedType">Excpected parameter type for AsyncCommand.Execute.</param>
/// <param name="innerException">Inner Exception</param>
public InvalidCommandParameterException(Type excpectedType, Exception innerException) : base(CreateErrorMessage(excpectedType), innerException)
{

}

/// <summary>
/// Initializes a new instance of the <see cref="T:TaskExtensions.MVVM.InvalidCommandParameterException"/> class.
/// </summary>
/// <param name="excpectedType">Excpected parameter type for AsyncCommand.Execute.</param>
public InvalidCommandParameterException(Type excpectedType) : base(CreateErrorMessage(excpectedType))
{

}

static string CreateErrorMessage(Type excpectedType) => $"Invalid type for parameter. Expected Type {excpectedType}";


}

static string CreateErrorMessage(Type expectedType, Type actualType) =>
Copy link

@brminnick brminnick Aug 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add two more constructors and an additional method to align with this feedback: #43 (comment)

/// <summary>
/// Initializes a new instance of the <see cref="T:TaskExtensions.MVVM.InvalidCommandParameterException"/> class.
/// </summary>
/// <param name="excpectedType">Excpected parameter type for AsyncCommand.Execute.</param>
/// <param name="innerException">Inner Exception</param>
public InvalidCommandParameterException(Type excpectedType, Exception innerException) : base(CreateErrorMessage(excpectedType), innerException)
{

}

/// <summary>
/// Initializes a new instance of the <see cref="T:TaskExtensions.MVVM.InvalidCommandParameterException"/> class.
/// </summary>
/// <param name="excpectedType">Excpected parameter type for AsyncCommand.Execute.</param>
public InvalidCommandParameterException(Type excpectedType) : base(CreateErrorMessage(excpectedType))
{

}

static string CreateErrorMessage(Type excpectedType) => $"Invalid type for parameter. Expected Type {excpectedType}";

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

Successfully merging this pull request may close these issues.

3 participants