From 6781c1bf73b2181b9eb91fc516a36eabc9e83959 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Wed, 10 Mar 2021 11:08:22 -0800 Subject: [PATCH 1/2] Implement Safe-Fire-And-Forget --- .../Effects/Touch/GestureManager.shared.cs | 27 +++---- .../Touch/PlatformTouchEffect.android.cs | 52 +++++++------- .../Effects/Touch/PlatformTouchEffect.ios.cs | 34 +++++---- .../Touch/PlatformTouchEffect.macos.cs | 22 +++--- .../Touch/PlatformTouchEffect.tizen.cs | 37 +++++----- .../Effects/Touch/TouchEffect.shared.cs | 30 ++++---- .../SafeFireAndForgetExtensions.shared.cs | 70 +++++++++++++++++++ .../Internals/BaseAsyncCommand.shared.cs | 8 +-- .../Internals/BaseAsyncValueCommand.shared.cs | 8 +-- .../Android/FormsVideoView.android.cs | 4 +- 10 files changed, 180 insertions(+), 112 deletions(-) create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/SafeFireAndForgetExtensions.shared.cs diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/GestureManager.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/GestureManager.shared.cs index ebb2ee306..3cfbc78ba 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/GestureManager.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/GestureManager.shared.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Xamarin.CommunityToolkit.Extensions; +using Xamarin.CommunityToolkit.Helpers; using Xamarin.Forms; using static System.Math; @@ -27,7 +28,7 @@ sealed class GestureManager TouchState animationState; - internal async Task HandleTouch(TouchEffect sender, TouchStatus status) + internal void HandleTouch(TouchEffect sender, TouchStatus status) { if (sender.IsDisabled) return; @@ -58,11 +59,11 @@ internal async Task HandleTouch(TouchEffect sender, TouchStatus status) ? 1 - animationProgress : animationProgress; - await UpdateStatusAndState(sender, status, state); + UpdateStatusAndState(sender, status, state); if (status == TouchStatus.Canceled) { - await sender.ForceUpdateState(false); + sender.ForceUpdateState(false); return; } @@ -76,7 +77,7 @@ internal async Task HandleTouch(TouchEffect sender, TouchStatus status) : TouchState.Pressed; } - await UpdateStatusAndState(sender, status, state); + UpdateStatusAndState(sender, status, state); } if (status == TouchStatus.Completed) @@ -92,7 +93,7 @@ internal void HandleUserInteraction(TouchEffect sender, TouchInteractionStatus i } } - internal async ValueTask HandleHover(TouchEffect sender, HoverStatus status) + internal void HandleHover(TouchEffect sender, HoverStatus status) { if (!sender.Element?.IsEnabled ?? true) return; @@ -104,7 +105,7 @@ internal async ValueTask HandleHover(TouchEffect sender, HoverStatus status) if (sender.HoverState != hoverState) { sender.HoverState = hoverState; - await sender.RaiseHoverStateChanged(); + sender.RaiseHoverStateChanged(); } if (sender.HoverStatus != status) @@ -140,7 +141,7 @@ internal async Task ChangeStateAsync(TouchEffect sender, bool animated) var durationMultiplier = this.durationMultiplier; this.durationMultiplier = null; - await GetAnimationTask(sender, state, hoverState, animationTokenSource.Token, durationMultiplier.GetValueOrDefault()).ConfigureAwait(false); + await RunAnimationTask(sender, state, hoverState, animationTokenSource.Token, durationMultiplier.GetValueOrDefault()).ConfigureAwait(false); return; } @@ -148,7 +149,7 @@ internal async Task ChangeStateAsync(TouchEffect sender, bool animated) if (pulseCount == 0 || (state == TouchState.Normal && !isToggled.HasValue)) { - await GetAnimationTask(sender, state, hoverState, animationTokenSource.Token).ConfigureAwait(false); + await RunAnimationTask(sender, state, hoverState, animationTokenSource.Token).ConfigureAwait(false); return; } do @@ -157,7 +158,7 @@ internal async Task ChangeStateAsync(TouchEffect sender, bool animated) ? TouchState.Normal : TouchState.Pressed; - await GetAnimationTask(sender, rippleState, hoverState, animationTokenSource.Token); + await RunAnimationTask(sender, rippleState, hoverState, animationTokenSource.Token); if (token.IsCancellationRequested) return; @@ -165,7 +166,7 @@ internal async Task ChangeStateAsync(TouchEffect sender, bool animated) ? TouchState.Pressed : TouchState.Normal; - await GetAnimationTask(sender, rippleState, hoverState, animationTokenSource.Token); + await RunAnimationTask(sender, rippleState, hoverState, animationTokenSource.Token); if (token.IsCancellationRequested) return; } @@ -269,12 +270,12 @@ internal void AbortAnimations(TouchEffect sender) element.AbortAnimations(); } - async ValueTask UpdateStatusAndState(TouchEffect sender, TouchStatus status, TouchState state) + void UpdateStatusAndState(TouchEffect sender, TouchStatus status, TouchState state) { if (sender.State != state || status != TouchStatus.Canceled) { sender.State = state; - await sender.RaiseStateChanged(); + sender.RaiseStateChanged(); } sender.Status = status; @@ -586,7 +587,7 @@ Color GetBackgroundColor(Color color) ? color : defaultBackgroundColor; - Task GetAnimationTask(TouchEffect sender, TouchState touchState, HoverState hoverState, CancellationToken token, double? durationMultiplier = null) + Task RunAnimationTask(TouchEffect sender, TouchState touchState, HoverState hoverState, CancellationToken token, double? durationMultiplier = null) { if (sender.Element == null) return Task.FromResult(false); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.android.cs index ae0147a2d..e3db9123e 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.android.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.android.cs @@ -1,6 +1,5 @@ using System; using System.ComponentModel; -using System.Threading.Tasks; using Android.Content; using Android.Content.Res; using Android.Graphics.Drawables; @@ -12,7 +11,6 @@ using Xamarin.CommunityToolkit.Effects; using Xamarin.Forms; using Xamarin.Forms.Platform.Android; -using AndroidOS = Android.OS; using AView = Android.Views.View; using Color = Android.Graphics.Color; @@ -161,7 +159,7 @@ void UpdateClickHandler() } } - async void OnTouch(object? sender, AView.TouchEventArgs e) + void OnTouch(object? sender, AView.TouchEventArgs e) { e.Handled = false; @@ -174,47 +172,51 @@ async void OnTouch(object? sender, AView.TouchEventArgs e) switch (e.Event?.ActionMasked) { case MotionEventActions.Down: - await OnTouchDown(e); + OnTouchDown(e); break; case MotionEventActions.Up: - await OnTouchUp(); + OnTouchUp(); break; case MotionEventActions.Cancel: - await OnTouchCancel(); + OnTouchCancel(); break; case MotionEventActions.Move: - await OnTouchMove(sender, e); + OnTouchMove(sender, e); break; case MotionEventActions.HoverEnter: - await OnHoverEnter(); + OnHoverEnter(); break; case MotionEventActions.HoverExit: - await OnHoverExit(); + OnHoverExit(); break; } } - async Task OnTouchDown(AView.TouchEventArgs e) + void OnTouchDown(AView.TouchEventArgs e) { _ = e.Event ?? throw new NullReferenceException(); IsCanceled = false; + startX = e.Event.GetX(); startY = e.Event.GetY(); + effect?.HandleUserInteraction(TouchInteractionStatus.Started); - await (effect?.HandleTouch(TouchStatus.Started) ?? Task.CompletedTask); + effect?.HandleTouch(TouchStatus.Started); + StartRipple(e.Event.GetX(), e.Event.GetY()); + if (effect?.DisallowTouchThreshold > 0) Group?.Parent?.RequestDisallowInterceptTouchEvent(true); } - Task OnTouchUp() + void OnTouchUp() => HandleEnd(effect?.Status == TouchStatus.Started ? TouchStatus.Completed : TouchStatus.Canceled); - Task OnTouchCancel() + void OnTouchCancel() => HandleEnd(TouchStatus.Canceled); - async Task OnTouchMove(object? sender, AView.TouchEventArgs e) + void OnTouchMove(object? sender, AView.TouchEventArgs e) { if (IsCanceled || e.Event == null) return; @@ -226,7 +228,7 @@ async Task OnTouchMove(object? sender, AView.TouchEventArgs e) var disallowTouchThreshold = effect?.DisallowTouchThreshold; if (disallowTouchThreshold > 0 && maxDiff > disallowTouchThreshold) { - await HandleEnd(TouchStatus.Canceled); + HandleEnd(TouchStatus.Canceled); return; } @@ -239,11 +241,11 @@ async Task OnTouchMove(object? sender, AView.TouchEventArgs e) if (isHoverSupported && ((status == TouchStatus.Canceled && effect?.HoverStatus == HoverStatus.Entered) || (status == TouchStatus.Started && effect?.HoverStatus == HoverStatus.Exited))) - await effect.HandleHover(status == TouchStatus.Started ? HoverStatus.Entered : HoverStatus.Exited); + effect.HandleHover(status == TouchStatus.Started ? HoverStatus.Entered : HoverStatus.Exited); if (effect?.Status != status) { - await (effect?.HandleTouch(status) ?? Task.CompletedTask); + effect?.HandleTouch(status); if (status == TouchStatus.Started) StartRipple(e.Event.GetX(), e.Event.GetY()); @@ -252,23 +254,23 @@ async Task OnTouchMove(object? sender, AView.TouchEventArgs e) } } - async ValueTask OnHoverEnter() + void OnHoverEnter() { isHoverSupported = true; if (effect != null) - await effect.HandleHover(HoverStatus.Entered); + effect.HandleHover(HoverStatus.Entered); } - async ValueTask OnHoverExit() + void OnHoverExit() { isHoverSupported = true; if (effect != null) - await effect.HandleHover(HoverStatus.Exited); + effect.HandleHover(HoverStatus.Exited); } - async void OnClick(object? sender, EventArgs args) + void OnClick(object? sender, EventArgs args) { if (effect?.IsDisabled ?? true) return; @@ -277,10 +279,10 @@ async void OnClick(object? sender, EventArgs args) return; IsCanceled = false; - await HandleEnd(TouchStatus.Completed); + HandleEnd(TouchStatus.Completed); } - async Task HandleEnd(TouchStatus status) + void HandleEnd(TouchStatus status) { if (IsCanceled) return; @@ -289,7 +291,7 @@ async Task HandleEnd(TouchStatus status) if (effect?.DisallowTouchThreshold > 0) Group?.Parent?.RequestDisallowInterceptTouchEvent(false); - await (effect?.HandleTouch(status) ?? Task.CompletedTask); + effect?.HandleTouch(status); effect?.HandleUserInteraction(TouchInteractionStatus.Completed); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.ios.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.ios.cs index ec21c6aa5..0b7d16c59 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.ios.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.ios.cs @@ -46,7 +46,7 @@ protected override void OnAttached() if (XCT.IsiOS13OrNewer) { - hoverGesture = new UIHoverGestureRecognizer(async () => await OnHover()); + hoverGesture = new UIHoverGestureRecognizer(OnHover); View.AddGestureRecognizer(hoverGesture); } @@ -79,7 +79,7 @@ protected override void OnDetached() effect = null; } - async ValueTask OnHover() + void OnHover() { if (effect == null || effect.IsDisabled) return; @@ -88,10 +88,10 @@ async ValueTask OnHover() { case UIGestureRecognizerState.Began: case UIGestureRecognizerState.Changed: - await effect.HandleHover(HoverStatus.Entered); + effect.HandleHover(HoverStatus.Entered); break; case UIGestureRecognizerState.Ended: - await effect.HandleHover(HoverStatus.Exited); + effect.HandleHover(HoverStatus.Exited); break; } } @@ -126,38 +126,44 @@ public TouchUITapGestureRecognizer(TouchEffect effect) UIView? Renderer => (UIView?)effect?.Element.GetRenderer(); - public override async void TouchesBegan(NSSet touches, UIEvent evt) + public override void TouchesBegan(NSSet touches, UIEvent evt) { if (effect?.IsDisabled ?? true) return; IsCanceled = false; startPoint = GetTouchPoint(touches); - await HandleTouch(TouchStatus.Started, TouchInteractionStatus.Started); + + HandleTouch(TouchStatus.Started, TouchInteractionStatus.Started).SafeFireAndForget(); + base.TouchesBegan(touches, evt); } - public override async void TouchesEnded(NSSet touches, UIEvent evt) + public override void TouchesEnded(NSSet touches, UIEvent evt) { if (effect?.IsDisabled ?? true) return; - await HandleTouch(effect?.Status == TouchStatus.Started ? TouchStatus.Completed : TouchStatus.Canceled, TouchInteractionStatus.Completed); + HandleTouch(effect?.Status == TouchStatus.Started ? TouchStatus.Completed : TouchStatus.Canceled, TouchInteractionStatus.Completed).SafeFireAndForget(); + IsCanceled = true; + base.TouchesEnded(touches, evt); } - public override async void TouchesCancelled(NSSet touches, UIEvent evt) + public override void TouchesCancelled(NSSet touches, UIEvent evt) { if (effect?.IsDisabled ?? true) return; - await HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed); + HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed).SafeFireAndForget(); + IsCanceled = true; + base.TouchesCancelled(touches, evt); } - public override async void TouchesMoved(NSSet touches, UIEvent evt) + public override void TouchesMoved(NSSet touches, UIEvent evt) { if (effect?.IsDisabled ?? true) return; @@ -171,7 +177,7 @@ public override async void TouchesMoved(NSSet touches, UIEvent evt) var maxDiff = Math.Max(diffX, diffY); if (maxDiff > disallowTouchThreshold) { - await HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed); + HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed).SafeFireAndForget(); IsCanceled = true; base.TouchesMoved(touches, evt); return; @@ -183,7 +189,7 @@ public override async void TouchesMoved(NSSet touches, UIEvent evt) : TouchStatus.Canceled; if (effect?.Status != status) - await HandleTouch(status); + HandleTouch(status).SafeFireAndForget(); base.TouchesMoved(touches, evt); } @@ -216,7 +222,7 @@ public async Task HandleTouch(TouchStatus status, TouchInteractionStatus? intera interactionStatus = null; } - await (effect?.HandleTouch(status) ?? Task.CompletedTask); + effect?.HandleTouch(status); if (interactionStatus.HasValue) effect?.HandleUserInteraction(interactionStatus.Value); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.macos.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.macos.cs index 7c39d83bc..561bfd946 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.macos.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.macos.cs @@ -77,20 +77,20 @@ public override void UpdateTrackingAreas() AddTrackingArea(trackingArea); } - public override async void MouseEntered(NSEvent theEvent) + public override void MouseEntered(NSEvent theEvent) { if (effect == null || effect.IsDisabled) return; - await effect.HandleHover(HoverStatus.Entered); + effect.HandleHover(HoverStatus.Entered); } - public override async void MouseExited(NSEvent theEvent) + public override void MouseExited(NSEvent theEvent) { if (effect == null || effect.IsDisabled) return; - await effect.HandleHover(HoverStatus.Exited); + effect.HandleHover(HoverStatus.Exited); } protected override void Dispose(bool disposing) @@ -137,18 +137,18 @@ Rectangle ViewRect } } - public override async void MouseDown(NSEvent mouseEvent) + public override void MouseDown(NSEvent mouseEvent) { if (effect == null || effect.IsDisabled) return; effect.HandleUserInteraction(TouchInteractionStatus.Started); - await effect.HandleTouch(TouchStatus.Started); + effect.HandleTouch(TouchStatus.Started); base.MouseDown(mouseEvent); } - public override async void MouseUp(NSEvent mouseEvent) + public override void MouseUp(NSEvent mouseEvent) { if (effect == null || effect.IsDisabled) return; @@ -160,14 +160,14 @@ public override async void MouseUp(NSEvent mouseEvent) ? TouchStatus.Completed : TouchStatus.Canceled; - await effect.HandleTouch(status); + effect.HandleTouch(status); } effect.HandleUserInteraction(TouchInteractionStatus.Completed); base.MouseUp(mouseEvent); } - public override async void MouseDragged(NSEvent mouseEvent) + public override void MouseDragged(NSEvent mouseEvent) { if (effect == null || effect.IsDisabled) return; @@ -176,10 +176,10 @@ public override async void MouseDragged(NSEvent mouseEvent) if ((status == TouchStatus.Canceled && effect.HoverStatus == HoverStatus.Entered) || (status == TouchStatus.Started && effect.HoverStatus == HoverStatus.Exited)) - await effect.HandleHover(status == TouchStatus.Started ? HoverStatus.Entered : HoverStatus.Exited); + effect.HandleHover(status == TouchStatus.Started ? HoverStatus.Entered : HoverStatus.Exited); if (effect.Status != status) - await effect.HandleTouch(status); + effect.HandleTouch(status); base.MouseDragged(mouseEvent); } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.tizen.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.tizen.cs index 4382ed442..dd71cea77 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.tizen.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.tizen.cs @@ -1,5 +1,4 @@ -using System.Threading.Tasks; -using ElmSharp; +using ElmSharp; using Xamarin.CommunityToolkit.Effects; using Xamarin.CommunityToolkit.Tizen.Effects; using Xamarin.Forms; @@ -51,13 +50,13 @@ sealed class TouchTapGestureRecognizer : GestureLayer public TouchTapGestureRecognizer(EvasObject parent) : base(parent) { - SetTapCallback(GestureType.Tap, GestureState.Start, async data => await OnTapStarted(data)); - SetTapCallback(GestureType.Tap, GestureState.End, async data => await OnGestureEnded(data)); - SetTapCallback(GestureType.Tap, GestureState.Abort, async data => await OnGestureAborted(data)); + SetTapCallback(GestureType.Tap, GestureState.Start, OnTapStarted); + SetTapCallback(GestureType.Tap, GestureState.End, OnGestureEnded); + SetTapCallback(GestureType.Tap, GestureState.Abort, OnGestureAborted); - SetTapCallback(GestureType.LongTap, GestureState.Start, async data => await OnLongTapStarted(data)); - SetTapCallback(GestureType.LongTap, GestureState.End, async data => await OnGestureEnded(data)); - SetTapCallback(GestureType.LongTap, GestureState.Abort, async data => await OnGestureAborted(data)); + SetTapCallback(GestureType.LongTap, GestureState.Start, OnLongTapStarted); + SetTapCallback(GestureType.LongTap, GestureState.End, OnGestureEnded); + SetTapCallback(GestureType.LongTap, GestureState.Abort, OnGestureAborted); } public TouchTapGestureRecognizer(EvasObject parent, TouchEffect effect) @@ -69,16 +68,16 @@ public TouchTapGestureRecognizer(EvasObject parent, TouchEffect effect) public bool IsCanceled { get; set; } = true; - async Task OnTapStarted(TapData data) + void OnTapStarted(TapData data) { if (effect?.IsDisabled ?? true) return; IsCanceled = false; - await HandleTouch(TouchStatus.Started, TouchInteractionStatus.Started); + HandleTouch(TouchStatus.Started, TouchInteractionStatus.Started); } - async Task OnLongTapStarted(TapData data) + void OnLongTapStarted(TapData data) { if (effect?.IsDisabled ?? true) return; @@ -86,20 +85,20 @@ async Task OnLongTapStarted(TapData data) IsCanceled = false; longTapStarted = true; - await HandleTouch(TouchStatus.Started, TouchInteractionStatus.Started); + HandleTouch(TouchStatus.Started, TouchInteractionStatus.Started); } - async Task OnGestureEnded(TapData data) + void OnGestureEnded(TapData data) { if (effect == null || effect.IsDisabled) return; - await HandleTouch(effect.Status == TouchStatus.Started ? TouchStatus.Completed : TouchStatus.Canceled, TouchInteractionStatus.Completed); + HandleTouch(effect.Status == TouchStatus.Started ? TouchStatus.Completed : TouchStatus.Canceled, TouchInteractionStatus.Completed); IsCanceled = true; tapCompleted = true; } - async Task OnGestureAborted(TapData data) + void OnGestureAborted(TapData data) { if (effect?.IsDisabled ?? true) return; @@ -111,11 +110,11 @@ async Task OnGestureAborted(TapData data) return; } - await HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed); + HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed); IsCanceled = true; } - public async Task HandleTouch(TouchStatus status, TouchInteractionStatus? touchInteractionStatus = null) + public void HandleTouch(TouchStatus status, TouchInteractionStatus? touchInteractionStatus = null) { if (IsCanceled || effect == null) return; @@ -129,7 +128,7 @@ public async Task HandleTouch(TouchStatus status, TouchInteractionStatus? touchI touchInteractionStatus = null; } - await effect.HandleTouch(status); + effect.HandleTouch(status); if (touchInteractionStatus.HasValue) effect.HandleUserInteraction(touchInteractionStatus.Value); @@ -140,7 +139,7 @@ public async Task HandleTouch(TouchStatus status, TouchInteractionStatus? touchI return; var control = effect.Element; - if (!(Platform.GetOrCreateRenderer(control)?.NativeView is Widget nativeView)) + if (Platform.GetOrCreateRenderer(control)?.NativeView is not Widget nativeView) return; if (status == TouchStatus.Started) diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/TouchEffect.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/TouchEffect.shared.cs index 4be18f20f..b75220400 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/TouchEffect.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/TouchEffect.shared.cs @@ -910,19 +910,15 @@ static void TryGenerateEffect(BindableObject? bindable, object oldValue, object view.Effects.Add(new TouchEffect { IsAutoGenerated = true }); } - static async void ForceUpdateStateAndTryGenerateEffect(BindableObject bindable, object oldValue, object newValue) + static void ForceUpdateStateAndTryGenerateEffect(BindableObject bindable, object oldValue, object newValue) { - if (GetFrom(bindable)?.ForceUpdateState() is Task forceUpdateState) - await forceUpdateState; - + GetFrom(bindable)?.ForceUpdateState(); TryGenerateEffect(bindable, oldValue, newValue); } - static async void ForceUpdateStateWithoutAnimationAndTryGenerateEffect(BindableObject bindable, object oldValue, object newValue) + static void ForceUpdateStateWithoutAnimationAndTryGenerateEffect(BindableObject bindable, object oldValue, object newValue) { - if (GetFrom(bindable)?.ForceUpdateState() is Task forceUpdateState) - await forceUpdateState; - + GetFrom(bindable)?.ForceUpdateState(); TryGenerateEffect(bindable, oldValue, newValue); } @@ -1109,8 +1105,6 @@ public bool? IsToggled ForceUpdateState(); } - - async void ForceUpdateState() => await this.ForceUpdateState(false); } } @@ -1128,18 +1122,18 @@ public bool? IsToggled ?? effects?.FirstOrDefault(); } - internal Task HandleTouch(TouchStatus status) + internal void HandleTouch(TouchStatus status) => gestureManager.HandleTouch(this, status); internal void HandleUserInteraction(TouchInteractionStatus interactionStatus) => gestureManager.HandleUserInteraction(this, interactionStatus); - internal ValueTask HandleHover(HoverStatus status) + internal void HandleHover(HoverStatus status) => gestureManager.HandleHover(this, status); - internal async Task RaiseStateChanged() + internal void RaiseStateChanged() { - await ForceUpdateState(); + ForceUpdateState(); HandleLongPress(); weakEventManager.RaiseEvent(Element, new TouchStateChangedEventArgs(State), nameof(StateChanged)); } @@ -1150,9 +1144,9 @@ internal void RaiseInteractionStatusChanged() internal void RaiseStatusChanged() => weakEventManager.RaiseEvent(Element, new TouchStatusChangedEventArgs(Status), nameof(StatusChanged)); - internal async Task RaiseHoverStateChanged() + internal void RaiseHoverStateChanged() { - await ForceUpdateState(); + ForceUpdateState(); weakEventManager.RaiseEvent(Element, new HoverStateChangedEventArgs(HoverState), nameof(HoverStateChanged)); } @@ -1162,12 +1156,12 @@ internal void RaiseHoverStatusChanged() internal void RaiseCompleted() => weakEventManager.RaiseEvent(Element, new TouchCompletedEventArgs(CommandParameter), nameof(Completed)); - internal async Task ForceUpdateState(bool animated = true) + internal void ForceUpdateState(bool animated = true) { if (Element == null) return; - await gestureManager.ChangeStateAsync(this, animated); + gestureManager.ChangeStateAsync(this, animated).SafeFireAndForget(); } internal void HandleLongPress() diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/SafeFireAndForgetExtensions.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/SafeFireAndForgetExtensions.shared.cs new file mode 100644 index 000000000..b2abbe0f6 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/SafeFireAndForgetExtensions.shared.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading.Tasks; + +// Inspired by https://github.com/brminnick/AsyncAwaitBestPractices +namespace Xamarin.CommunityToolkit.Helpers +{ + /// + /// Extension methods for System.Threading.Tasks.Task and System.Threading.Tasks.ValueTask + /// + static class SafeFireAndForgetExtensions + { + /// + /// Safely execute the ValueTask without waiting for it to complete before moving to the next line of code; commonly known as "Fire And Forget". Inspired by John Thiriet's blog post, "Removing Async Void": https://johnthiriet.com/removing-async-void/. + /// + /// ValueTask. + /// If an exception is thrown in the ValueTask, onException will execute. If onException is null, the exception will be re-thrown + /// If set to true, continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false, continue on a different context; this will allow the Synchronization Context to continue on a different thread + public static void SafeFireAndForget(this ValueTask task, in Action? onException = null, in bool continueOnCapturedContext = false) => HandleSafeFireAndForget(task, continueOnCapturedContext, onException); + + /// + /// Safely execute the ValueTask without waiting for it to complete before moving to the next line of code; commonly known as "Fire And Forget". Inspired by John Thiriet's blog post, "Removing Async Void": https://johnthiriet.com/removing-async-void/. + /// + /// ValueTask. + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + /// If set to true, continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false, continue on a different context; this will allow the Synchronization Context to continue on a different thread + /// Exception type. If an exception is thrown of a different type, it will not be handled + public static void SafeFireAndForget(this ValueTask task, in Action? onException = null, in bool continueOnCapturedContext = false) where TException : Exception => HandleSafeFireAndForget(task, continueOnCapturedContext, onException); + + /// + /// Safely execute the Task without waiting for it to complete before moving to the next line of code; commonly known as "Fire And Forget". Inspired by John Thiriet's blog post, "Removing Async Void": https://johnthiriet.com/removing-async-void/. + /// + /// Task. + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + /// If set to true, continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false, continue on a different context; this will allow the Synchronization Context to continue on a different thread + public static void SafeFireAndForget(this Task task, in Action? onException = null, in bool continueOnCapturedContext = false) => HandleSafeFireAndForget(task, continueOnCapturedContext, onException); + + /// + /// Safely execute the Task without waiting for it to complete before moving to the next line of code; commonly known as "Fire And Forget". Inspired by John Thiriet's blog post, "Removing Async Void": https://johnthiriet.com/removing-async-void/. + /// + /// Task. + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + /// If set to true, continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false, continue on a different context; this will allow the Synchronization Context to continue on a different thread + /// Exception type. If an exception is thrown of a different type, it will not be handled + public static void SafeFireAndForget(this Task task, in Action? onException = null, in bool continueOnCapturedContext = false) where TException : Exception => HandleSafeFireAndForget(task, continueOnCapturedContext, onException); + + static async void HandleSafeFireAndForget(ValueTask valueTask, bool continueOnCapturedContext, Action? onException) where TException : Exception + { + try + { + await valueTask.ConfigureAwait(continueOnCapturedContext); + } + catch (TException ex) when (onException != null) + { + onException(ex); + } + } + + static async void HandleSafeFireAndForget(Task task, bool continueOnCapturedContext, Action? onException) where TException : Exception + { + try + { + await task.ConfigureAwait(continueOnCapturedContext); + } + catch (TException ex) when (onException != null) + { + onException(ex); + } + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseAsyncCommand.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseAsyncCommand.shared.cs index 8f67a4e10..1336600d3 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseAsyncCommand.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseAsyncCommand.shared.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using System.Windows.Input; using Xamarin.CommunityToolkit.Exceptions; +using Xamarin.CommunityToolkit.Helpers; namespace Xamarin.CommunityToolkit.ObjectModel.Internals { @@ -100,11 +101,11 @@ void ICommand.Execute(object parameter) switch (parameter) { case TExecute validParameter: - Execute(validParameter); + ExecuteAsync(validParameter).SafeFireAndForget(onException, continueOnCapturedContext); break; case null when !typeof(TExecute).GetTypeInfo().IsValueType: - Execute((TExecute?)parameter); + ExecuteAsync((TExecute?)parameter).SafeFireAndForget(onException, continueOnCapturedContext); break; case null: @@ -113,9 +114,6 @@ void ICommand.Execute(object parameter) default: throw new InvalidCommandParameterException(typeof(TExecute), parameter.GetType()); } - - // Use local method to defer async void from ICommand.Execute, allowing InvalidCommandParameterException to be thrown on the calling thread context before reaching an async method - async void Execute(TExecute? parameter) => await ExecuteAsync(parameter).ConfigureAwait(continueOnCapturedContext); } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseAsyncValueCommand.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseAsyncValueCommand.shared.cs index eef0a1cff..2f6edbc9a 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseAsyncValueCommand.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseAsyncValueCommand.shared.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using System.Windows.Input; using Xamarin.CommunityToolkit.Exceptions; +using Xamarin.CommunityToolkit.Helpers; namespace Xamarin.CommunityToolkit.ObjectModel.Internals { @@ -100,11 +101,11 @@ void ICommand.Execute(object parameter) switch (parameter) { case TExecute validParameter: - Execute(validParameter); + ExecuteAsync(validParameter).SafeFireAndForget(onException, continueOnCapturedContext); break; case null when !typeof(TExecute).GetTypeInfo().IsValueType: - Execute((TExecute?)parameter); + ExecuteAsync((TExecute?)parameter).SafeFireAndForget(onException, continueOnCapturedContext); break; case null: @@ -113,9 +114,6 @@ void ICommand.Execute(object parameter) default: throw new InvalidCommandParameterException(typeof(TExecute), parameter.GetType()); } - - // Use local method to defer async void from ICommand.Execute, allowing InvalidCommandParameterException to be thrown on the calling thread context before reaching an async method - async void Execute(TExecute? parameter) => await ExecuteAsync(parameter).ConfigureAwait(continueOnCapturedContext); } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/Android/FormsVideoView.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/Android/FormsVideoView.android.cs index 45faabe78..8c2c1711c 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/Android/FormsVideoView.android.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/Android/FormsVideoView.android.cs @@ -53,12 +53,12 @@ protected void ExtractMetadata(MediaMetadataRetriever retriever) public override async void SetVideoURI(global::Android.Net.Uri? uri, IDictionary? headers) { if (uri != null) - await GetMetadata(uri, headers); + await SetMetadata(uri, headers); base.SetVideoURI(uri, headers); } - protected async Task GetMetadata(global::Android.Net.Uri uri, IDictionary? headers) + protected async Task SetMetadata(global::Android.Net.Uri uri, IDictionary? headers) { var retriever = new MediaMetadataRetriever(); From f3eb1ae6c2815a0a0f86e157ec629119ef97b588 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Date: Wed, 10 Mar 2021 19:44:19 -0800 Subject: [PATCH 2/2] Change `public` to `internal` --- .../Helpers/SafeFireAndForgetExtensions.shared.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/SafeFireAndForgetExtensions.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/SafeFireAndForgetExtensions.shared.cs index b2abbe0f6..2ad792125 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/SafeFireAndForgetExtensions.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/SafeFireAndForgetExtensions.shared.cs @@ -15,7 +15,8 @@ static class SafeFireAndForgetExtensions /// ValueTask. /// If an exception is thrown in the ValueTask, onException will execute. If onException is null, the exception will be re-thrown /// If set to true, continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false, continue on a different context; this will allow the Synchronization Context to continue on a different thread - public static void SafeFireAndForget(this ValueTask task, in Action? onException = null, in bool continueOnCapturedContext = false) => HandleSafeFireAndForget(task, continueOnCapturedContext, onException); + internal static void SafeFireAndForget(this ValueTask task, in Action? onException = null, in bool continueOnCapturedContext = false) => + HandleSafeFireAndForget(task, continueOnCapturedContext, onException); /// /// Safely execute the ValueTask without waiting for it to complete before moving to the next line of code; commonly known as "Fire And Forget". Inspired by John Thiriet's blog post, "Removing Async Void": https://johnthiriet.com/removing-async-void/. @@ -24,7 +25,8 @@ static class SafeFireAndForgetExtensions /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown /// If set to true, continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false, continue on a different context; this will allow the Synchronization Context to continue on a different thread /// Exception type. If an exception is thrown of a different type, it will not be handled - public static void SafeFireAndForget(this ValueTask task, in Action? onException = null, in bool continueOnCapturedContext = false) where TException : Exception => HandleSafeFireAndForget(task, continueOnCapturedContext, onException); + internal static void SafeFireAndForget(this ValueTask task, in Action? onException = null, in bool continueOnCapturedContext = false) where TException : Exception => + HandleSafeFireAndForget(task, continueOnCapturedContext, onException); /// /// Safely execute the Task without waiting for it to complete before moving to the next line of code; commonly known as "Fire And Forget". Inspired by John Thiriet's blog post, "Removing Async Void": https://johnthiriet.com/removing-async-void/. @@ -32,7 +34,8 @@ static class SafeFireAndForgetExtensions /// Task. /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown /// If set to true, continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false, continue on a different context; this will allow the Synchronization Context to continue on a different thread - public static void SafeFireAndForget(this Task task, in Action? onException = null, in bool continueOnCapturedContext = false) => HandleSafeFireAndForget(task, continueOnCapturedContext, onException); + internal static void SafeFireAndForget(this Task task, in Action? onException = null, in bool continueOnCapturedContext = false) => + HandleSafeFireAndForget(task, continueOnCapturedContext, onException); /// /// Safely execute the Task without waiting for it to complete before moving to the next line of code; commonly known as "Fire And Forget". Inspired by John Thiriet's blog post, "Removing Async Void": https://johnthiriet.com/removing-async-void/. @@ -41,7 +44,8 @@ static class SafeFireAndForgetExtensions /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown /// If set to true, continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false, continue on a different context; this will allow the Synchronization Context to continue on a different thread /// Exception type. If an exception is thrown of a different type, it will not be handled - public static void SafeFireAndForget(this Task task, in Action? onException = null, in bool continueOnCapturedContext = false) where TException : Exception => HandleSafeFireAndForget(task, continueOnCapturedContext, onException); + internal static void SafeFireAndForget(this Task task, in Action? onException = null, in bool continueOnCapturedContext = false) where TException : Exception => + HandleSafeFireAndForget(task, continueOnCapturedContext, onException); static async void HandleSafeFireAndForget(ValueTask valueTask, bool continueOnCapturedContext, Action? onException) where TException : Exception {