/// <summary> /// Creates a reaction that operates on data of type T. /// </summary> /// <typeparam name="T">The type the reaction operates on.</typeparam> /// <param name="sharedState">The shared state to use.</param> /// <param name="expression">The expression that delivers a value.</param> /// <param name="effect">The effect that is executed when the value changes.</param> /// <param name="reactionOptions">The options to use for the reaction.</param> /// <returns>An <see cref="IDisposable"/> instance that can be used to stop the reaction.</returns> public static IDisposable Reaction <T>(this ISharedState sharedState, Func <Reaction, T> expression, Action <T, Reaction> effect, ReactionOptions <T> reactionOptions = null) { if (sharedState is null) { throw new ArgumentNullException(nameof(sharedState)); } if (expression is null) { throw new ArgumentNullException(nameof(expression)); } if (effect is null) { throw new ArgumentNullException(nameof(effect)); } if (reactionOptions is null) { reactionOptions = new ReactionOptions <T>(); } var name = reactionOptions.Name ?? $"Reaction@{sharedState.GetUniqueId()}"; var action = sharedState.CreateAction( name + "effect", reactionOptions.Context, reactionOptions.ErrorHandler != null ? WrapErrorHandler(reactionOptions.ErrorHandler, effect) : effect); var runSync = reactionOptions.Scheduler == null && reactionOptions.Delay == 0; var firstTime = true; var isScheduled = false; T value = default; Reaction reaction = null; var equals = reactionOptions.EqualityComparer != null ? new Func <T, T, bool>(reactionOptions.EqualityComparer.Equals) : new Func <T, T, bool>((x, y) => Equals(x, y)); var scheduler = CreateSchedulerFromOptions(reactionOptions, ReactionRunner); #pragma warning disable CA2000 // Dispose objects before losing scope #pragma warning disable IDE0067 // Dispose objects before losing scope reaction = new Reaction( sharedState, name, () => { if (firstTime || runSync) { ReactionRunner().GetAwaiter().GetResult(); } else if (!isScheduled) { isScheduled = true; scheduler().GetAwaiter().GetResult(); } }, reactionOptions.ErrorHandler); #pragma warning restore IDE0067 // Dispose objects before losing scope #pragma warning restore CA2000 // Dispose objects before losing scope Task ReactionRunner() { isScheduled = false; // Q: move into reaction runner? if (reaction.IsDisposed) { return(Task.CompletedTask); } var changed = false; reaction.Track(() => { var nextValue = expression(reaction); changed = firstTime || !equals(value, nextValue); value = nextValue; }); if (firstTime && reactionOptions.FireImmediately) { action(value, reaction); } if (!firstTime && changed) { action(value, reaction); } if (firstTime) { firstTime = false; } return(Task.CompletedTask); } reaction.Schedule(); return(new DisposableDelegate(() => reaction.Dispose())); }
/// <summary> /// Creates a reaction that operates on data of type T. /// </summary> /// <typeparam name="T">The type the reaction operates on.</typeparam> /// <param name="sharedState">The shared state to use.</param> /// <param name="expression">The expression that delivers a value.</param> /// <param name="effect">The effect that is executed when the value changes.</param> /// <param name="reactionOptions">The options to use for the reaction.</param> /// <returns>An <see cref="IDisposable"/> instance that can be used to stop the reaction.</returns> /// <remarks>Only pass asynchronous effect functions that wrap state modifications in actions.</remarks> public static IDisposable Reaction <T>(this ISharedState sharedState, Func <Reaction, T> expression, Func <T, Reaction, Task> effect, ReactionOptions <T> reactionOptions = null) { if (sharedState is null) { throw new ArgumentNullException(nameof(sharedState)); } if (expression is null) { throw new ArgumentNullException(nameof(expression)); } if (effect is null) { throw new ArgumentNullException(nameof(effect)); } if (reactionOptions is null) { reactionOptions = new ReactionOptions <T>(); } var name = reactionOptions.Name ?? $"Reaction@{sharedState.GetUniqueId()}"; var action = reactionOptions.ErrorHandler != null?WrapErrorHandler(reactionOptions.ErrorHandler, effect) : effect; var runSync = reactionOptions.Scheduler == null && reactionOptions.Delay == 0; var firstTime = true; var isScheduled = false; T value = default; Reaction reaction = null; var equals = reactionOptions.EqualityComparer != null ? new Func <T, T, bool>(reactionOptions.EqualityComparer.Equals) : new Func <T, T, bool>((x, y) => Equals(x, y)); var scheduler = CreateSchedulerFromOptions(reactionOptions, async() => { await ReactionRunner().ConfigureAwait(true); }); #pragma warning disable CA2000 // Dispose objects before losing scope #pragma warning disable IDE0067 // Dispose objects before losing scope reaction = new Reaction( sharedState, name, () => { if (firstTime || runSync) { var taskScheduler = sharedState.GetTaskScheduler(); isScheduled = true; Task.Factory.StartNew( ReactionRunner, CancellationToken.None, TaskCreationOptions.DenyChildAttach, taskScheduler).Unwrap().ContinueWith( t => { if (t.Exception != null) { reaction.ReportExceptionInReaction(t.Exception); } }, taskScheduler); } else if (!isScheduled) { var taskScheduler = sharedState.GetTaskScheduler(); isScheduled = true; Task.Factory.StartNew( scheduler, CancellationToken.None, TaskCreationOptions.DenyChildAttach, taskScheduler).Unwrap().ContinueWith( t => { if (t.Exception != null) { reaction.ReportExceptionInReaction(t.Exception); } }, taskScheduler); } }, reactionOptions.ErrorHandler); #pragma warning restore IDE0067 // Dispose objects before losing scope #pragma warning restore CA2000 // Dispose objects before losing scope async Task ReactionRunner() { isScheduled = false; // Q: move into reaction runner? if (reaction.IsDisposed) { return; } var changed = false; reaction.Track(() => { var nextValue = expression(reaction); changed = firstTime || !equals(value, nextValue); value = nextValue; }); if (firstTime && reactionOptions.FireImmediately) { await action(value, reaction).ConfigureAwait(true); } if (!firstTime && changed) { await action(value, reaction).ConfigureAwait(true); } if (firstTime) { firstTime = false; } } reaction.Schedule(); return(new DisposableDelegate(() => reaction.Dispose())); }