Skip to content
This repository was archived by the owner on May 1, 2024. It is now read-only.

Remove Device dependency from command implementation (#825) #830

Merged
merged 10 commits into from
Feb 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ public void AsyncCommand_NoParameter_NoCanExecute_Test()
}

[Fact]
public void AsyncCommand_RaiseCanExecuteChanged_Test()
public void AsyncCommand_RaiseCanExecuteChanged_MainThreadCreation_MainThreadExecution_Test()
{
// Arrange
var canCommandExecute = false;
Expand Down Expand Up @@ -193,9 +193,117 @@ public void AsyncCommand_RaiseCanExecuteChanged_Test()
}

[Fact]
public void AsyncCommand_ChangeCanExecute_Test()
public Task AsyncCommand_RaiseCanExecuteChanged_BackgroundThreadCreation_BackgroundThreadExecution_Test() => Task.Run(async () =>
{
// Arrange
await Task.Delay(100).ConfigureAwait(false);

var canCommandExecute = false;
var didCanExecuteChangeFire = false;

var command = new AsyncCommand(NoParameterTask, commandCanExecute);
command.CanExecuteChanged += handleCanExecuteChanged;

bool commandCanExecute(object parameter) => canCommandExecute;

Assert.False(command.CanExecute(null));

// Act
canCommandExecute = true;

// Assert
Assert.True(command.CanExecute(null));
Assert.False(didCanExecuteChangeFire);

// Act
command.RaiseCanExecuteChanged();

// Assert
Assert.True(didCanExecuteChangeFire);
Assert.True(command.CanExecute(null));

void handleCanExecuteChanged(object sender, EventArgs e) => didCanExecuteChangeFire = true;
});

[Fact]
public async Task AsyncCommand_RaiseCanExecuteChanged_MainThreadCreation_BackgroundThreadExecution_Test()
{
// Arrange

var canCommandExecute = false;
var didCanExecuteChangeFire = false;

var command = new AsyncCommand(NoParameterTask, commandCanExecute);
command.CanExecuteChanged += handleCanExecuteChanged;

bool commandCanExecute(object parameter) => canCommandExecute;

Assert.False(command.CanExecute(null));

// Act
canCommandExecute = true;

// Assert
Assert.True(command.CanExecute(null));
Assert.False(didCanExecuteChangeFire);

// Act
await Task.Run(async () =>
{
await Task.Delay(100).ConfigureAwait(false);
command.RaiseCanExecuteChanged();

// Assert
Assert.True(didCanExecuteChangeFire);
Assert.True(command.CanExecute(null));
});

void handleCanExecuteChanged(object sender, EventArgs e) => didCanExecuteChangeFire = true;
}

[Fact]
public async Task AsyncCommand_RaiseCanExecuteChanged_BackgroundThreadCreation_MainThreadExecution_Test()
{
// Arrange
AsyncCommand command = null;
var didCanExecuteChangeFire = false;
var canCommandExecute = false;

await Task.Run(async () =>
{
await Task.Delay(100).ConfigureAwait(false);

command = new AsyncCommand(NoParameterTask, commandCanExecute);
command.CanExecuteChanged += handleCanExecuteChanged;

bool commandCanExecute(object parameter) => canCommandExecute;

Assert.False(command.CanExecute(null));

// Act
canCommandExecute = true;

// Assert
Assert.True(command.CanExecute(null));
Assert.False(didCanExecuteChangeFire);
}).ConfigureAwait(true);

// Act
command.RaiseCanExecuteChanged();

// Assert
Assert.True(didCanExecuteChangeFire);
Assert.True(command.CanExecute(null));

void handleCanExecuteChanged(object sender, EventArgs e) => didCanExecuteChangeFire = true;
}

[Fact]
public async Task AsyncCommand_ChangeCanExecute_Test()
{
// Arrange
var canExecuteChangedTCS = new TaskCompletionSource<object>();

var canCommandExecute = false;
var didCanExecuteChangeFire = false;

Expand All @@ -216,13 +324,18 @@ public void AsyncCommand_ChangeCanExecute_Test()
// Act
#pragma warning disable CS0618 // Type or member is obsolete
command.ChangeCanExecute();
await canExecuteChangedTCS.Task;
#pragma warning restore CS0618 // Type or member is obsolete

// Assert
Assert.True(didCanExecuteChangeFire);
Assert.True(command.CanExecute(null));

void handleCanExecuteChanged(object sender, EventArgs e) => didCanExecuteChangeFire = true;
void handleCanExecuteChanged(object sender, EventArgs e)
{
didCanExecuteChangeFire = true;
canExecuteChangedTCS.SetResult(null);
}
}

[Fact]
Expand Down Expand Up @@ -257,6 +370,8 @@ public async Task AsyncCommand_CanExecuteChanged_AllowsMultipleExecutions_Test()
public async Task AsyncCommand_CanExecuteChanged_DoesNotAllowMultipleExecutions_Test()
{
// Arrange
var canExecuteChangedGreaterThan1TCS = new TaskCompletionSource<object>();

var canExecuteChangedCount = 0;

var command = new AsyncCommand<int>(IntParameterTask, allowsMultipleExecutions: false);
Expand All @@ -273,12 +388,17 @@ public async Task AsyncCommand_CanExecuteChanged_DoesNotAllowMultipleExecutions_

// Act
await asyncCommandTask;
await canExecuteChangedGreaterThan1TCS.Task;

// Assert
Assert.True(command.CanExecute(null));
Assert.Equal(2, canExecuteChangedCount);

void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++;
void handleCanExecuteChanged(object sender, EventArgs e)
{
if (++canExecuteChangedCount > 1)
canExecuteChangedGreaterThan1TCS.SetResult(null);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,11 @@ public void AsyncValueCommandNoParameter_NoCanExecute_Test()
}

[Fact]
public void AsyncValueCommand_RaiseCanExecuteChanged_Test()
public async Task AsyncValueCommand_RaiseCanExecuteChanged_Test()
{
// Arrange
var handleCanExecuteChangedTCS = new TaskCompletionSource<object>();

var canCommandExecute = false;
var didCanExecuteChangeFire = false;

Expand All @@ -207,18 +209,25 @@ public void AsyncValueCommand_RaiseCanExecuteChanged_Test()

// Act
command.RaiseCanExecuteChanged();
await handleCanExecuteChangedTCS.Task;

// Assert
Assert.True(didCanExecuteChangeFire);
Assert.True(command.CanExecute(null));

void handleCanExecuteChanged(object sender, EventArgs e) => didCanExecuteChangeFire = true;
void handleCanExecuteChanged(object sender, EventArgs e)
{
didCanExecuteChangeFire = true;
handleCanExecuteChangedTCS.SetResult(null);
}
}

[Fact]
public void AsyncValueCommand_ChangeCanExecute_Test()
public async Task AsyncValueCommand_ChangeCanExecute_Test()
{
// Arrange
var handleCanExecuteChangedTCS = new TaskCompletionSource<object>();

var canCommandExecute = false;
var didCanExecuteChangeFire = false;

Expand All @@ -239,13 +248,18 @@ public void AsyncValueCommand_ChangeCanExecute_Test()
// Act
#pragma warning disable CS0618 // Type or member is obsolete
command.ChangeCanExecute();
await handleCanExecuteChangedTCS.Task;
#pragma warning restore CS0618 // Type or member is obsolete

// Assert
Assert.True(didCanExecuteChangeFire);
Assert.True(command.CanExecute(null));

void handleCanExecuteChanged(object sender, EventArgs e) => didCanExecuteChangeFire = true;
void handleCanExecuteChanged(object sender, EventArgs e)
{
didCanExecuteChangeFire = true;
handleCanExecuteChangedTCS.SetResult(null);
}
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows.Input;
Expand All @@ -9,6 +10,7 @@ namespace Xamarin.CommunityToolkit.ObjectModel.Internals
/// <summary>
/// Abstract Base Class used by AsyncValueCommand
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public class BaseAsyncCommand<TExecute, TCanExecute> : BaseCommand<TCanExecute>, ICommand
{
readonly Func<TExecute, Task> execute;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows.Input;
Expand All @@ -9,7 +10,8 @@ namespace Xamarin.CommunityToolkit.ObjectModel.Internals
/// <summary>
/// Abstract Base Class used by AsyncValueCommand
/// </summary>
public class BaseAsyncValueCommand<TExecute, TCanExecute> : BaseCommand<TCanExecute>, ICommand
[EditorBrowsable(EditorBrowsableState.Never)]
public abstract class BaseAsyncValueCommand<TExecute, TCanExecute> : BaseCommand<TCanExecute>, ICommand
{
readonly Func<TExecute, ValueTask> execute;
readonly Action<Exception> onException;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using Android.OS;

// Inspired by Xamarin.Essentials.MainThread https://github.com/xamarin/Essentials/tree/main/Xamarin.Essentials/MainThread
namespace Xamarin.CommunityToolkit.ObjectModel.Internals
{
public abstract partial class BaseCommand<TCanExecute>
{
static volatile Handler handler;

static bool IsMainThread
{
get
{
if (Build.VERSION.SdkInt >= BuildVersionCodes.M)
return Looper.MainLooper.IsCurrentThread;

return Looper.MyLooper() == Looper.MainLooper;
}
}

static void BeginInvokeOnMainThread(Action action)
{
if (handler?.Looper != Looper.MainLooper)
handler = new Handler(Looper.MainLooper);

handler.Post(action);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using System.Threading;

namespace Xamarin.CommunityToolkit.ObjectModel.Internals
{
public abstract partial class BaseCommand<TCanExecute>
{
static readonly Thread mainThread = Thread.CurrentThread;

static bool IsMainThread => Thread.CurrentThread == mainThread;

static void BeginInvokeOnMainThread(Action action)
{
GLib.Idle.Add(() =>
{
action();
return false;
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using Foundation;

// Inspired by Xamarin.Essentials.MainThread https://github.com/xamarin/Essentials/tree/main/Xamarin.Essentials/MainThread
namespace Xamarin.CommunityToolkit.ObjectModel.Internals
{
public abstract partial class BaseCommand<TCanExecute>
{
static bool IsMainThread => NSThread.Current.IsMainThread;

static void BeginInvokeOnMainThread(Action action) => NSRunLoop.Main.BeginInvokeOnMainThread(action.Invoke);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using System.Threading;

// Inspired by Prism.Commands https://github.com/PrismLibrary/Prism/blob/0fc56abbea733a319c9734c65d1110db07f7ebd4/src/Prism.Core/Commands/DelegateCommandBase.cs#L37-L47
namespace Xamarin.CommunityToolkit.ObjectModel.Internals
{
public abstract partial class BaseCommand<TCanExecute>
{
static readonly SynchronizationContext synchronizationContext = SynchronizationContext.Current;

static bool IsMainThread => SynchronizationContext.Current == synchronizationContext;

static void BeginInvokeOnMainThread(Action action)
{
if (synchronizationContext != null && SynchronizationContext.Current != synchronizationContext)
synchronizationContext.Post((o) => action(), null);
else
action();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ namespace Xamarin.CommunityToolkit.ObjectModel.Internals
/// <summary>
/// Abstract Base Class used by AsyncCommand and AsyncValueCommand
/// </summary>
public abstract class BaseCommand<TCanExecute>
[EditorBrowsable(EditorBrowsableState.Never)]
public abstract partial class BaseCommand<TCanExecute>
{
readonly Func<TCanExecute, bool> canExecute;
readonly WeakEventManager weakEventManager = new WeakEventManager();
Expand Down Expand Up @@ -83,7 +84,10 @@ protected int ExecutionCount
public void RaiseCanExecuteChanged()
{
// Automatically marshall to the Main Thread to adhere to the way that Xamarin.Forms automatically marshalls binding events to Main Thread
Device.BeginInvokeOnMainThread(() => weakEventManager.RaiseEvent(this, EventArgs.Empty, nameof(CanExecuteChanged)));
if (IsMainThread)
weakEventManager.RaiseEvent(this, EventArgs.Empty, nameof(CanExecuteChanged));
else
BeginInvokeOnMainThread(() => weakEventManager.RaiseEvent(this, EventArgs.Empty, nameof(CanExecuteChanged)));
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;
using ElmSharp;

// Inspired by Xamarin.Essentials.MainThread https://github.com/xamarin/Essentials/tree/main/Xamarin.Essentials/MainThread
namespace Xamarin.CommunityToolkit.ObjectModel.Internals
{
public abstract partial class BaseCommand<TCanExecute>
{
static bool IsMainThread => EcoreMainloop.IsMainThread;

static void BeginInvokeOnMainThread(Action action)
{
if (IsMainThread)
action();
else
EcoreMainloop.PostAndWakeUp(action);
}
}
}
Loading