Skip to content

Commit

Permalink
Merge pull request #19 from brminnick/Release-v3.0.0
Browse files Browse the repository at this point in the history
Release v3.0.0
  • Loading branch information
TheCodeTraveler authored Jul 30, 2019
2 parents 351ac97 + f662705 commit e768a96
Show file tree
Hide file tree
Showing 27 changed files with 1,020 additions and 122 deletions.
121 changes: 105 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ Inspired by [John Thiriet](https://github.com/johnthiriet)'s blog posts: [Removi

Available on NuGet: https://www.nuget.org/packages/AsyncAwaitBestPractices/

- `SafeFireAndForget`
- An extension method to safely fire-and-forget a `Task`:
- `WeakEventManager`
- `SafeFireAndForget`
- An extension method to safely fire-and-forget a `Task`
- Ensures the `Task` will rethrow an `Exception` if an `Exception` is caught in `IAsyncStateMachine.MoveNext()`
- `WeakEventManager`
- Avoids memory leaks when events are not unsubscribed
- Used by `AsyncCommand` and `AsyncCommand<T>`
- [Usage instructions](#asyncawaitbestpractices-3)
- [Usage instructions](#asyncawaitbestpractices-3)

### AsyncAwaitBestPractices.MVVM

Expand Down Expand Up @@ -113,15 +114,53 @@ Never, never, never, never, never use `.Result` or `.Wait()`:
An extension method to safely fire-and-forget a `Task`:

```csharp
public static async void SafeFireAndForget(this System.Threading.Tasks.Task task, bool continueOnCapturedContext = true, System.Action<System.Exception> onException = null)
public static async void SafeFireAndForget(this System.Threading.Tasks.Task task, bool continueOnCapturedContext = false, System.Action<System.Exception> onException = null)
```

#### Basic Usage

```csharp
void HandleButtonTapped(object sender, EventArgs e)
{
// Allows the async Task method to safely run on a different thread while not awaiting its completion
// onException: If an Exception is thrown, print it to the Console
ExampleAsyncMethod().SafeFireAndForget(onException: ex => Console.WriteLine(ex));

// HandleButtonTapped continues execution here while `ExampleAsyncMethod()` is running on a different thread
// ...
}

async Task ExampleAsyncMethod()
{
await Task.Delay(1000);
}
```

#### Advanced Usage

```csharp
void InitializeSafeFireAndForget()
{
// Initialize SafeFireAndForget
// Only use `shouldAlwaysRethrowException: true` when you want `.SafeFireAndForget()` to always rethrow every exception. This is not recommended, because there is no way to catch an Exception rethrown by `SafeFireAndForget()`; `shouldAlwaysRethrowException: true` should **not** be used in Production/Release builds.
SafeFireAndForgetExtensions.Initialize(shouldAlwaysRethrowException: false);

// SafeFireAndForget will print every exception to the Console
SafeFireAndForgetExtensions.SetDefaultExceptionHandling(ex => Console.WriteLine(ex));
}

void HandleButtonTapped(object sender, EventArgs e)
{
// Allows the async Task method to safely run on a different thread while not awaiting its completion
// If an exception is thrown, Console.WriteLine
ExampleAsyncMethod().SafeFireAndForget(onException: ex => Console.WriteLine(ex.ToString()));
// onException: If a WebException is thrown, print its StatusCode to the Console. **Note**: If a non-WebException is thrown, it will not be handled by `onException`
// Because we set `SetDefaultExceptionHandling` in `void InitializeSafeFireAndForget()`, the entire exception will also be printed to the Console
ExampleAsyncMethod().SafeFireAndForget<WebException>(onException: ex =>
{
if(e.Response is HttpWebResponse webResponse)
Console.WriteLine($"Status Code: {webResponse.StatusCode}");
});

// HandleButtonTapped continues execution here while `ExampleAsyncMethod()` is running on a different thread
// ...
Expand All @@ -130,26 +169,62 @@ void HandleButtonTapped(object sender, EventArgs e)
async Task ExampleAsyncMethod()
{
await Task.Delay(1000);
throw new WebException();
}
```

### `WeakEventManager`

An event implementation that enables the [garbage collector to collect an object without needing to unsubscribe event handlers](http://paulstovell.com/blog/weakevents), inspired by [Xamarin.Forms.WeakEventManager](https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Core/WeakEventManager.cs):
An event implementation that enables the [garbage collector to collect an object without needing to unsubscribe event handlers](http://paulstovell.com/blog/weakevents).

Inspired by [Xamarin.Forms.WeakEventManager](https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Core/WeakEventManager.cs).

#### Using `EventHandler`

```csharp
readonly WeakEventManager _weakEventManager = new WeakEventManager();
readonly WeakEventManager _canExecuteChangedEventManager = new WeakEventManager();

public event EventHandler CanExecuteChanged
{
add => _weakEventManager.AddEventHandler(value);
remove => _weakEventManager.RemoveEventHandler(value);
}

public void RaiseCanExecuteChanged() => _weakEventManager.HandleEvent(this, EventArgs.Empty, nameof(CanExecuteChanged));
public void OnCanExecuteChanged() => _canExecuteChangedEventManager.HandleEvent(this, EventArgs.Empty, nameof(CanExecuteChanged));
```

#### Using `Delegate`

```csharp
readonly WeakEventManager _propertyChangedEventManager = new WeakEventManager();

public event PropertyChangedEventHandler PropertyChanged
{
add => _propertyChangedEventManager.AddEventHandler(value);
remove => _propertyChangedEventManager.RemoveEventHandler(value);
}

public void OnPropertyChanged([CallerMemberName]string propertyName = "") => _propertyChangedEventManager.HandleEvent(this, new PropertyChangedEventArgs(propertyName), nameof(PropertyChanged));
```

#### Using `Action`

```csharp
readonly WeakEventManager _weakActionEventManager = new WeakEventManager();

public event Action ActionEvent
{
add => _weakActionEventManager.AddEventHandler(value);
remove => _weakActionEventManager.RemoveEventHandler(value);
}

public void OnActionEvent(string message) => _weakActionEventManager.HandleEvent(message, nameof(ActionEvent));
```

### `WeakEventManager<T>`
An event implementation that enables the [garbage collector to collect an object without needing to unsubscribe event handlers](http://paulstovell.com/blog/weakevents), inspired by [Xamarin.Forms.WeakEventManager](https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Core/WeakEventManager.cs).

#### Using `EventHandler<T>`

```csharp
readonly WeakEventManager<string> _errorOcurredEventManager = new WeakEventManager<string>();
Expand All @@ -160,7 +235,21 @@ public event EventHandler<string> ErrorOcurred
remove => _errorOcurredEventManager.RemoveEventHandler(value);
}

public void RaiseErrorOcurred(string message) => _weakEventManager.HandleEvent(this, message, nameof(ErrorOcurred));
public void OnErrorOcurred(string message) => _errorOcurredEventManager.HandleEvent(this, message, nameof(ErrorOcurred));
```

#### Using `Action<T>`

```csharp
readonly WeakEventManager<string> _weakActionEventManager = new WeakEventManager<string>();

public event Action<string> ActionEvent
{
add => _weakActionEventManager.AddEventHandler(value);
remove => _weakActionEventManager.RemoveEventHandler(value);
}

public void OnActionEvent(string message) => _weakActionEventManager.HandleEvent(message, nameof(ActionEvent));
```

## AsyncAwaitBestPractices.MVVM
Expand All @@ -178,14 +267,14 @@ Allows for `Task` to safely be used asynchronously with `ICommand`:
public AsyncCommand(Func<T, Task> execute,
Func<object, bool> canExecute = null,
Action<Exception> onException = null,
bool continueOnCapturedContext = true)
bool continueOnCapturedContext = false)
```

```csharp
public AsyncCommand(Func<Task> execute,
Func<object, bool> canExecute = null,
Action<Exception> onException = null,
bool continueOnCapturedContext = true)
bool continueOnCapturedContext = false)
```

```csharp
Expand All @@ -196,13 +285,13 @@ public class ExampleClass
ExampleAsyncCommand = new AsyncCommand(ExampleAsyncMethod);
ExampleAsyncIntCommand = new AsyncCommand<int>(ExampleAsyncMethodWithIntParameter);
ExampleAsyncExceptionCommand = new AsyncCommand(ExampleAsyncMethodWithException, onException: ex => Console.WriteLine(ex.ToString()));
ExampleAsyncCommandNotReturningToTheCallingThread = new AsyncCommand(ExampleAsyncMethod, continueOnCapturedContext: false);
ExampleAsyncCommandReturningToTheCallingThread = new AsyncCommand(ExampleAsyncMethod, continueOnCapturedContext: true);
}

public IAsyncCommand ExampleAsyncCommand { get; }
public IAsyncCommand<int> ExampleAsyncIntCommand { get; }
public IAsyncCommand ExampleAsyncExceptionCommand { get; }
public IAsyncCommand ExampleAsyncCommandNotReturningToTheCallingThread { get; }
public IAsyncCommand ExampleAsyncCommandReturningToTheCallingThread { get; }

async Task ExampleAsyncMethod()
{
Expand All @@ -225,7 +314,7 @@ public class ExampleClass
ExampleAsyncCommand.Execute(null);
ExampleAsyncIntCommand.Execute(1000);
ExampleAsyncExceptionCommand.Execute(null);
ExampleAsyncCommandNotReturningToTheCallingThread.Execute(null);
ExampleAsyncCommandReturningToTheCallingThread.Execute(null);
}
}
```
Expand Down
19 changes: 12 additions & 7 deletions Src/AsyncAwaitBestPractices.MVVM.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,34 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata minClientVersion="2.5">
<id>AsyncAwaitBestPractices.MVVM</id>
<version>2.1.1</version>
<version>3.0.0</version>
<title>Task Extensions for MVVM</title>
<authors>Brandon Minnick, John Thiriet</authors>
<owners>Brandon Minnick</owners>
<licenseUrl>https://github.com/brminnick/AsyncAwaitBestPractices/blob/master/LICENSE.md</licenseUrl>
<license type="expression">MIT</license>
<projectUrl>https://github.com/brminnick/AsyncAwaitBestPractices</projectUrl>
<!-- <iconUrl>TBD</iconUrl> -->
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>Includes AsyncCommand and IAsyncCommand which allows ICommand to safely be used asynchronously with Task.</description>
<summary>Includes AsyncCommand and IAsyncCommand which allows ICommand to safely be used asynchronously with Task.</summary>
<tags>task,fire and forget, threading, extensions, system.threading.tasks,async,await</tags>
<dependencies>
<dependency id="AsyncAwaitBestPractices" version="2.1.1" />
<dependency id="AsyncAwaitBestPractices" version="3.0.0" />
</dependencies>
<releaseNotes>
New In This Release:
- Performance improvements to SafeFireAndForget
- Added support for `event Action` and `event Action&lt;T&gt;`
- Added support for `.SafeFireAndForget&lt;TException&gt;()`
- Added `SafeFireAndForgetExtensions.SetDefaultExceptionHandling(Action&lt;Exception&gt; onException)` to set a default action for every call to `SafeFireAndForget`
- Added `SafeFireAndForgetExtensions.Initialize(bool shouldAlwaysRethrowException = false)`. When set to `true` will rethrow every exception caught by `SafeFireAndForget`. Warning: `SafeFireAndForgetExtensions.Initialize(true)` is only recommended for DEBUG environments.
- Added support for `Exception innerException` to `InvalidCommandParameterException`
- Breaking Change: Changed default value to `continueOnCapturedContext = false`. This improves performance by not requiring a context switch when `.SafeFireAndForget()` and `IAsyncCommand` have completed.
</releaseNotes>
<copyright>Copyright (c) 2018 Brandon Minnick</copyright>
</metadata>
<files>
<file src="AsyncAwaitBestPractices.MVVM\bin\Release\netstandard2.0\AsyncAwaitBestPractices.MVVM.pdb" target="lib\netstandard\AsyncAwaitBestPractices.MVVM.pdb" />
<file src="AsyncAwaitBestPractices.MVVM\bin\Release\netstandard2.0\AsyncAwaitBestPractices.MVVM.dll" target="lib\netstandard\AsyncAwaitBestPractices.MVVM.dll" />
<file src="AsyncAwaitBestPractices.MVVM\bin\Release\netstandard2.0\AsyncAwaitBestPractices.MVVM.xml" target="lib\netstandard\AsyncAwaitBestPractices.MVVM.xml" />
<file src="AsyncAwaitBestPractices.MVVM\bin\Release\netstandard2.0\AsyncAwaitBestPractices.MVVM.pdb" target="lib\netstandard2.0\AsyncAwaitBestPractices.MVVM.pdb" />
<file src="AsyncAwaitBestPractices.MVVM\bin\Release\netstandard2.0\AsyncAwaitBestPractices.MVVM.dll" target="lib\netstandard2.0\AsyncAwaitBestPractices.MVVM.dll" />
<file src="AsyncAwaitBestPractices.MVVM\bin\Release\netstandard2.0\AsyncAwaitBestPractices.MVVM.xml" target="lib\netstandard2.0\AsyncAwaitBestPractices.MVVM.xml" />
</files>
</package>
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ProduceReferenceAssembly>True</ProduceReferenceAssembly>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<DebugType>portable</DebugType>
<DocumentationFile>bin\Release\netstandard2.0\AsyncAwaitBestPractices.MVVM.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
Expand Down
4 changes: 2 additions & 2 deletions Src/AsyncAwaitBestPractices.MVVM/AsyncCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public sealed class AsyncCommand<T> : IAsyncCommand<T>
public AsyncCommand(Func<T, Task> execute,
Func<object, bool> canExecute = null,
Action<Exception> onException = null,
bool continueOnCapturedContext = true)
bool continueOnCapturedContext = false)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute), $"{nameof(execute)} cannot be null");
_canExecute = canExecute ?? (_ => true);
Expand Down Expand Up @@ -104,7 +104,7 @@ public sealed class AsyncCommand : IAsyncCommand
public AsyncCommand(Func<Task> execute,
Func<object, bool> canExecute = null,
Action<Exception> onException = null,
bool continueOnCapturedContext = true)
bool continueOnCapturedContext = false)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute), $"{nameof(execute)} cannot be null");
_canExecute = canExecute ?? (_ => true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,35 @@ namespace AsyncAwaitBestPractices.MVVM
/// <summary>
/// Represents errors that occur during IAsyncCommand execution.
/// </summary>
public class InvalidCommandParameterException : Exception
class InvalidCommandParameterException : Exception
{
/// <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="actualType">Actual parameter type for AsyncCommand.Execute.</param>
public InvalidCommandParameterException(Type excpectedType, Type actualType) : base(CreateErrorMessage(excpectedType, actualType))
/// <param name="innerException">Inner Exception</param>
public InvalidCommandParameterException(Type excpectedType, Type actualType, Exception innerException) : this(CreateErrorMessage(excpectedType, actualType), 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>
/// <param name="actualType">Actual parameter type for AsyncCommand.Execute.</param>
public InvalidCommandParameterException(Type excpectedType, Type actualType) : this(CreateErrorMessage(excpectedType, actualType))
{

}

/// <summary>
/// Initializes a new instance of the <see cref="T:TaskExtensions.MVVM.InvalidCommandParameterException"/> class.
/// </summary>
/// <param name="message">Exception Message</param>
/// <param name="innerException">Inner Exception</param>
public InvalidCommandParameterException(string message, Exception innerException) : base(message, innerException)
{

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
<TargetFramework>netcoreapp2.1</TargetFramework>
<LangVersion>latest</LangVersion>
<IsPackable>false</IsPackable>
<ProduceReferenceAssembly>True</ProduceReferenceAssembly>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="nunit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.13.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AsyncAwaitBestPractices.MVVM\AsyncAwaitBestPractices.MVVM.csproj" />
Expand Down
13 changes: 7 additions & 6 deletions Src/AsyncAwaitBestPractices.UnitTests/BaseTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,24 @@ protected event EventHandler<string> TestStringEvent
protected Task NoParameterTask() => Task.Delay(Delay);
protected Task IntParameterTask(int delay) => Task.Delay(delay);
protected Task StringParameterTask(string text) => Task.Delay(Delay);
protected Task NoParameterImmediateExceptionTask() => throw new Exception();
protected Task ParameterImmediateExceptionTask(int delay) => throw new Exception();
protected Task NoParameterImmediateNullReferenceExceptionTask() => throw new NullReferenceException();
protected Task ParameterImmediateNullReferenceExceptionTask(int delay) => throw new NullReferenceException();

protected async Task NoParameterDelayedExceptionTask()
protected async Task NoParameterDelayedNullReferenceExceptionTask()
{
await Task.Delay(Delay);
throw new Exception();
throw new NullReferenceException();
}

protected async Task IntParameterDelayedExceptionTask(int delay)
protected async Task IntParameterDelayedNullReferenceExceptionTask(int delay)
{
await Task.Delay(delay);
throw new Exception();
throw new NullReferenceException();
}

protected bool CanExecuteTrue(object parameter) => true;
protected bool CanExecuteFalse(object parameter) => false;
protected bool CanExecuteDynamic(object booleanParameter) => (bool)booleanParameter;
#endregion
}
}
Loading

0 comments on commit e768a96

Please sign in to comment.