From fbe7acf44e2eec745905d498d8ea34af2680d932 Mon Sep 17 00:00:00 2001 From: Andres Pineda Date: Wed, 21 Aug 2024 09:26:37 -0400 Subject: [PATCH 01/13] feat(iState): add fluent API on ForEach operator --- .../Operators/Given_StateForEach.cs | 65 +++++++++++--- .../Core/State.Extensions.cs | 85 +++++++++++++++++-- src/Uno.Extensions.Reactive/Core/State.T.cs | 16 +++- .../Operators/StateForEach.cs | 2 + 4 files changed, 143 insertions(+), 25 deletions(-) diff --git a/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs b/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs index 60c8a9cd38..52d3e7b095 100644 --- a/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs +++ b/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs @@ -7,6 +7,8 @@ using System.Threading.Tasks; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Uno.Extensions.Equality; +using Uno.Extensions.Reactive.Messaging; using Uno.Extensions.Reactive.Testing; namespace Uno.Extensions.Reactive.Tests.Extensions; @@ -18,20 +20,20 @@ public class Given_StateForEach : FeedTests [ExpectedException(typeof(InvalidOperationException))] // Note: This is a compilation tests! public async Task When_ForEachAsync_Then_AcceptsNotNullAndStruct() { - default(IState)!.ForEachAsync(async (i, ct) => this.ToString()); - default(IState)!.ForEachAsync(async (i, ct) => this.ToString()); - default(IState)!.ForEachAsync(async (i, ct) => this.ToString()); + _ = default(IState)!.ForEach(async (i, ct) => this.ToString()); + _ = default(IState)!.ForEach(async (i, ct) => this.ToString()); + _ = default(IState)!.ForEach(async (i, ct) => this.ToString()); #nullable disable #pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. - default(IState)!.ForEachAsync(async (i, ct) => this.ToString()); + _ = default(IState)!.ForEach(async (i, ct) => this.ToString()); #pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. #nullable restore - default(IState)!.ForEachAsync(async (i, ct) => this.ToString()); - default(IState)!.ForEachAsync(async (i, ct) => this.ToString()); - default(IState)!.ForEachAsync(async (i, ct) => this.ToString()); + _ = default(IState)!.ForEach(async (i, ct) => this.ToString()); + _ = default(IState)!.ForEach(async (i, ct) => this.ToString()); + _ = default(IState)!.ForEach(async (i, ct) => this.ToString()); #nullable disable #pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. - default(IState)!.ForEachAsync(async (i, ct) => this.ToString()); + _ = default(IState)!.ForEach(async (i, ct) => this.ToString()); #pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. #nullable restore } @@ -40,7 +42,7 @@ public async Task When_ForEachAsync_Then_AcceptsNotNullAndStruct() [ExpectedException(typeof(InvalidOperationException))] // Note: This is a compilation tests! public async Task When_ForEachDataAsync_Then_AcceptsNotNullAndStruct() { - default(IState)!.ForEachAsync(async (i, ct) => this.ToString()); + _ = default(IState)!.ForEach(async (i, ct) => this.ToString()); default(IState)!.ForEachDataAsync(async (i, ct) => this.ToString()); default(IState)!.ForEachDataAsync(async (i, ct) => this.ToString()); default(IState)!.ForEachDataAsync(async (i, ct) => this.ToString()); @@ -56,7 +58,22 @@ public async Task When_UpdateState_Then_CallbackInvokedIgnoringInitialValue() var state = State.Value(this, () => 1); var result = new List(); - state.ForEachAsync(async (i, ct) => result.Add(i)); + _ = state.ForEach(async (i, ct) => result.Add(i)); + + await state.SetAsync(2, CT); + await state.SetAsync(3, CT); + await state.SetAsync(4, CT); + + result.Should().BeEquivalentTo(new[] { 2, 3, 4 }); + } + + [TestMethod] + public async Task When_Fluent_UpdateState_Then_CallbackInvokedIgnoringInitialValue() + { + var result = new List(); + + var state = State.Async(this, async ct => 1) + .ForEach(async (i, ct) => result.Add(i)); await state.SetAsync(2, CT); await state.SetAsync(3, CT); @@ -73,7 +90,7 @@ public async Task When_UpdateStateAndCallbackIsAsync_Then_CallsAreQueued() var tcs1 = new TaskCompletionSource(); var tcs2 = new TaskCompletionSource(); - state.ForEachAsync(async (i, ct) => + _ = state.ForEach(async (i, ct) => { await (i switch { @@ -100,7 +117,7 @@ public async Task When_UpdateStateAndCallbackFails_Then_CallbackInvokedOnNextUpd var state = State.Value(this, () => 1); var result = new List(); - state.ForEachAsync(async (i, ct) => + await state.ForEach(async (i, ct) => { if (i is 42) { @@ -120,7 +137,7 @@ public async Task When_UpdateStateAndCallbackFails_Then_CallbackInvokedOnNextUpd public async Task When_DisposeState_Then_EnumerationStop() { var state = State.Value(this, () => 1); - var sut = state.ForEachAsync(async (i, ct) => this.ToString()); + await state.ForEach(async (i, ct) => this.ToString(), out var sut); await state.DisposeAsync(); @@ -137,9 +154,29 @@ public async Task When_DisposeState_Then_EnumerationStop() public async Task When_DisposeExecute_Then_EnumerationStop() { var state = State.Value(this, () => 1); - var sut = state.ForEachAsync(async (i, ct) => this.ToString()); + await state.ForEach(async (i, ct) => this.ToString(), out var sut); sut.Dispose(); + + await state.SetAsync(42, CT); + + var enumerationTask = sut.GetType().GetField("_task", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(sut) as Task; + if (enumerationTask is null) + { + Assert.Fail("Unable to get the private _task field of the StateListener."); + } + + enumerationTask.Status.Should().Be(TaskStatus.RanToCompletion); + } + + [TestMethod] + public async Task When_Fluent_And_DisposeExecute_Then_EnumerationStop() + { + var state = State.Value(this, () => 1) + .ForEach(async (i, ct) => this.ToString(), out var sut); + + sut.Dispose(); + await state.SetAsync(42, CT); var enumerationTask = sut.GetType().GetField("_task", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(sut) as Task; diff --git a/src/Uno.Extensions.Reactive/Core/State.Extensions.cs b/src/Uno.Extensions.Reactive/Core/State.Extensions.cs index 0a2cc0a7c0..e0112687de 100644 --- a/src/Uno.Extensions.Reactive/Core/State.Extensions.cs +++ b/src/Uno.Extensions.Reactive/Core/State.Extensions.cs @@ -177,6 +177,29 @@ public static ValueTask Set(this IState state, T? value, CancellationToken public static ValueTask Set(this IState state, string? value, CancellationToken ct) => SetAsync(state, value, ct); + /// + /// [DEPRECATED] Use ForEach instead + /// + [EditorBrowsable(EditorBrowsableState.Never)] +#if DEBUG // To avoid usage in internal reactive code, but without forcing apps to update right away + [Obsolete("Use ForEach")] +#endif + public static IDisposable ForEachAsync(this IState state, AsyncAction action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) + where T : notnull + => new StateForEach(state, action.SomeOrNone(), $"ForEachAsync defined in {caller} at line {line}."); + + /// + /// [DEPRECATED] Use ForEach instead + /// + [EditorBrowsable(EditorBrowsableState.Never)] +#if DEBUG // To avoid usage in internal reactive code, but without forcing apps to update right away + [Obsolete("Use ForEach")] +#endif + public static IDisposable ForEachAsync(this IState state, AsyncAction action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) + where T : struct + => new StateForEach(state, action.SomeOrNone(), $"ForEachAsync defined in {caller} at line {line}."); + + /// /// Execute an async callback each time the state is being updated. /// @@ -185,10 +208,32 @@ public static ValueTask Set(this IState state, string? value, Cancellati /// The callback to invoke on each update of the state. /// For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this. /// For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this. - /// A that can be used to remove the callback registration. - public static IDisposable ForEachAsync(this IState state, AsyncAction action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) + /// An that can be used to chain other operations. + public static IState ForEach(this IState state, AsyncAction action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) where T : notnull - => new StateForEach(state, action.SomeOrNone(), $"ForEachAsync defined in {caller} at line {line}."); + { + _ = new StateForEach(state, action.SomeOrNone(), $"ForEach defined in {caller} at line {line}."); + + return state; + } + + /// + /// Execute an async callback each time the state is being updated. + /// + /// The type of the state + /// The state to listen. + /// The callback to invoke on each update of the state. + /// For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this. + /// For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this. + /// A that can be used to remove the callback registration. + /// An that can be used to chain other operations. + public static IState ForEach(this IState state, AsyncAction action, out IDisposable disposable, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) + where T : notnull + { + disposable = new StateForEach(state, action.SomeOrNone(), $"ForEach defined in {caller} at line {line}."); + + return state; + } /// /// Execute an async callback each time the state is being updated. @@ -199,9 +244,31 @@ public static IDisposable ForEachAsync(this IState state, AsyncAction /// For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this. /// For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this. /// A that can be used to remove the callback registration. - public static IDisposable ForEachAsync(this IState state, AsyncAction action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) + public static IState ForEach(this IState state, AsyncAction action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) where T : struct - => new StateForEach(state, action.SomeOrNone(), $"ForEachAsync defined in {caller} at line {line}."); + { + _ = new StateForEach(state, action.SomeOrNone(), $"ForEach defined in {caller} at line {line}."); + + return state; + } + + /// + /// Execute an async callback each time the state is being updated. + /// + /// The type of the state + /// The state to listen. + /// The callback to invoke on each update of the state. + /// For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this. + /// For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this. + /// A that can be used to remove the callback registration. + /// An that can be used to chain other operations. + public static IState ForEach(this IState state, AsyncAction action, out IDisposable disposable, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) + where T : struct + { + disposable = new StateForEach(state, action.SomeOrNone(), $"ForEach defined in {caller} at line {line}."); + + return state; + } /// @@ -221,9 +288,13 @@ public static IDisposable ForEachDataAsync(this IState state, AsyncAction< /// [EditorBrowsable(EditorBrowsableState.Never)] #if DEBUG // To avoid usage in internal reactive code, but without forcing apps to update right away - [Obsolete("Use ForEachAsync")] + [Obsolete("Use ForEach")] #endif public static IDisposable Execute(this IState state, AsyncAction action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) where T : notnull - => ForEachAsync(state, action, caller, line); + { + _ = ForEachAsync(state, action, caller, line); + + return Disposable.Empty; + } } diff --git a/src/Uno.Extensions.Reactive/Core/State.T.cs b/src/Uno.Extensions.Reactive/Core/State.T.cs index 4872ce8764..4fe43e6dc4 100644 --- a/src/Uno.Extensions.Reactive/Core/State.T.cs +++ b/src/Uno.Extensions.Reactive/Core/State.T.cs @@ -84,7 +84,9 @@ public static IState Empty(TOwner owner, [CallerMemberName] string? n public static IState Value(TOwner owner, Func valueProvider) where TOwner : class // Note: We force the usage of delegate so 2 properties which are doing State.Value(this, () => 42) will effectively have 2 distinct states. - => AttachedProperty.GetOrCreate(owner, valueProvider, static (o, v) => SourceContext.GetOrCreate(o).CreateState(Option.SomeOrNone(v()))); + => AttachedProperty.GetOrCreate(owner: owner, + key: valueProvider, + factory: static (o, v) => SourceContext.GetOrCreate(o).CreateState(Option.SomeOrNone(v()))); /// /// Gets or creates a state from a static initial value. @@ -96,7 +98,9 @@ public static IState Value(TOwner owner, Func valueProvider) public static IState Value(TOwner owner, Func> valueProvider) where TOwner : class // Note: We force the usage of delegate so 2 properties which are doing State.Value(this, () => 42) will effectively have 2 distinct states. - => AttachedProperty.GetOrCreate(owner, valueProvider, static (o, v) => SourceContext.GetOrCreate(o).CreateState(v())); + => AttachedProperty.GetOrCreate(owner: owner, + key: valueProvider, + factory: static (o, v) => SourceContext.GetOrCreate(o).CreateState(v())); /// /// Gets or creates a state from an async method. @@ -108,7 +112,9 @@ public static IState Value(TOwner owner, Func> valueProvide /// A feed that encapsulate the source. public static IState Async(TOwner owner, AsyncFunc> valueProvider, Signal? refresh = null) where TOwner : class - => AttachedProperty.GetOrCreate(owner, (valueProvider, refresh), static (o, args) => S(o, new AsyncFeed(args.valueProvider, args.refresh))); + => AttachedProperty.GetOrCreate(owner: owner, + key: (valueProvider, refresh), + factory: static (o, args) => S(o, new AsyncFeed(args.valueProvider, args.refresh))); /// /// Gets or creates a state from an async method. @@ -132,7 +138,9 @@ internal static IState Async(AsyncFunc> valueProvider, Signal? refr /// A feed that encapsulate the source. public static IState Async(TOwner owner, AsyncFunc valueProvider, Signal? refresh = null) where TOwner : class - => AttachedProperty.GetOrCreate(owner, (valueProvider, refresh), static (o, args) => S(o, new AsyncFeed(args.valueProvider.SomeOrNoneWhenNotNull(), args.refresh))); + => AttachedProperty.GetOrCreate(owner: owner, + key: (valueProvider, refresh), + factory: static (o, args) => S(o, new AsyncFeed(args.valueProvider.SomeOrNoneWhenNotNull(), args.refresh))); /// /// Gets or creates a state from an async method. diff --git a/src/Uno.Extensions.Reactive/Operators/StateForEach.cs b/src/Uno.Extensions.Reactive/Operators/StateForEach.cs index aab9a26443..9cc187d214 100644 --- a/src/Uno.Extensions.Reactive/Operators/StateForEach.cs +++ b/src/Uno.Extensions.Reactive/Operators/StateForEach.cs @@ -14,7 +14,9 @@ internal sealed class StateForEach : IDisposable private readonly CancellationTokenSource _ct = new(); private readonly AsyncAction> _action; private readonly string _name; +#pragma warning disable IDE0052 // Remove unread private members private readonly Task _task; // Holds ref on the enumeration task. This is also accessed by reflection in tests! +#pragma warning restore IDE0052 // Remove unread private members public StateForEach(ISignal> state, AsyncAction> action, string name = "-unnamed-") { From ea3da394b2995fefe951d52c2a40b4a77b3c1bd4 Mon Sep 17 00:00:00 2001 From: Andres Pineda Date: Wed, 21 Aug 2024 09:52:49 -0400 Subject: [PATCH 02/13] feat(iState): add Observe operator --- .../MessengerExtensions.cs | 5 +- .../StateExtensions.cs | 35 +++++++++++++ .../Messaging/Given_Messaging.cs | 51 +++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) diff --git a/src/Uno.Extensions.Reactive.Messaging/MessengerExtensions.cs b/src/Uno.Extensions.Reactive.Messaging/MessengerExtensions.cs index b1412e4552..711db28468 100644 --- a/src/Uno.Extensions.Reactive.Messaging/MessengerExtensions.cs +++ b/src/Uno.Extensions.Reactive.Messaging/MessengerExtensions.cs @@ -23,7 +23,10 @@ public static class MessengerExtensions /// A selector to get a unique identifier of a . /// A disposable that can be used to unbind the state from the messenger. public static IDisposable Observe(this IMessenger messenger, IState state, Func keySelector) - => AttachedProperty.GetOrCreate(state, keySelector, messenger, (s, ks, msg) => new Recipient, TEntity, TKey>(s, msg, ks, StateExtensions.Update)); + => AttachedProperty.GetOrCreate(owner: state, + key: keySelector, + state: messenger, + factory: (s, ks, msg) => new Recipient, TEntity, TKey>(s, msg, ks, StateExtensions.Update)); /// /// Listen for on the given and updates the accordingly. diff --git a/src/Uno.Extensions.Reactive.Messaging/StateExtensions.cs b/src/Uno.Extensions.Reactive.Messaging/StateExtensions.cs index a35ec79a69..cb46cf9d87 100644 --- a/src/Uno.Extensions.Reactive.Messaging/StateExtensions.cs +++ b/src/Uno.Extensions.Reactive.Messaging/StateExtensions.cs @@ -2,8 +2,10 @@ using System.ComponentModel; using System.Threading; using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Messaging; using Uno.Extensions.Reactive.Core; using Uno.Extensions.Reactive.Sources; +using Uno.Extensions.Reactive.Utils; namespace Uno.Extensions.Reactive.Messaging; @@ -153,4 +155,37 @@ public static async ValueTask TryRefreshAsync(this IListState listSt return await Task.WhenAny(refreshed, messageListener) == refreshed; } + + /// + /// Listen for on the given and updates the accordingly. + /// + /// Type of the value of the state. + /// Type of the identifier that uniquely identifies a . + /// The messenger to listen for + /// The state to update. + /// A selector to get a unique identifier of a . + /// An that can be used to chain other operations. + public static IState Observe(this IState state, IMessenger messenger, Func keySelector) + { + _ = messenger.Observe(state, keySelector); + + return state; + } + + /// + /// Listen for on the given and updates the accordingly. + /// + /// Type of the value of the state. + /// Type of the identifier that uniquely identifies a . + /// The messenger to listen for + /// The state to update. + /// A selector to get a unique identifier of a . + /// A that can be used to remove the callback registration. + /// An that can be used to chain other operations. + public static IState Observe(this IState state, IMessenger messenger, Func keySelector, out IDisposable disposable) + { + disposable = messenger.Observe(state, keySelector); + + return state; + } } diff --git a/src/Uno.Extensions.Reactive.Tests/Messaging/Given_Messaging.cs b/src/Uno.Extensions.Reactive.Tests/Messaging/Given_Messaging.cs index e389359efc..835cefb538 100644 --- a/src/Uno.Extensions.Reactive.Tests/Messaging/Given_Messaging.cs +++ b/src/Uno.Extensions.Reactive.Tests/Messaging/Given_Messaging.cs @@ -44,6 +44,57 @@ public async Task When_Updated_Then_StateUpdated() result.Should().BeEquivalentTo(new MyEntity(42, 1)); } + [TestMethod] + public async Task When_Fluent_Value_Updated_Then_StateUpdated() + { + var messenger = new WeakReferenceMessenger(); + var state = State.Value(this, () => new MyEntity(42)) + .Observe(messenger, i => i.Key); + + messenger.Send(new EntityMessage(EntityChange.Updated, new(42, 1))); + + var result = await state; + result.Should().BeEquivalentTo(new MyEntity(42, 1)); + } + + [TestMethod] + public async Task When_Fluent_Value_Updated_Then_StateUpdated_And_ForEach() + { + var versions = new List(); + + var messenger = new WeakReferenceMessenger(); + var state = State.Value(this, () => new MyEntity(42)) + .Observe(messenger, i => i.Key) + .ForEach(async (i, ct) => versions.Add(i!.Version)); + + messenger.Send(new EntityMessage(EntityChange.Updated, new(42, 3))); + messenger.Send(new EntityMessage(EntityChange.Updated, new(42, 4))); + + var result = await state; + + result.Should().BeEquivalentTo(new MyEntity(42, 4)); + versions.Should().BeEquivalentTo(new[] { 3, 4 }); + } + + [TestMethod] + public async Task When_Fluent_Async_Updated_Then_StateUpdated_And_ForEach() + { + var versions = new List(); + + var messenger = new WeakReferenceMessenger(); + var state = State.Async(this, async ct => new MyEntity(42)) + .Observe(messenger, i => i.Key) + .ForEach(async (i, ct) => versions.Add(i!.Version)); + + messenger.Send(new EntityMessage(EntityChange.Updated, new(42, 5))); + messenger.Send(new EntityMessage(EntityChange.Updated, new(42, 1))); + + var result = await state; + + result.Should().BeEquivalentTo(new MyEntity(42, 1)); + versions.Should().BeEquivalentTo(new[] { 5, 1 }); + } + [TestMethod] public async Task When_Deleted_Then_StateUpdated() { From 10663224713163e29b9ba237e8173058b852d9e1 Mon Sep 17 00:00:00 2001 From: Andres Pineda Date: Wed, 21 Aug 2024 11:39:30 -0400 Subject: [PATCH 03/13] feat(IState): add caching to the ForEach operator --- .../Operators/Given_StateForEach.cs | 4 ++-- .../Core/State.Extensions.cs | 17 ++++++++++++----- .../Operators/StateForEach.cs | 2 ++ 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs b/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs index 52d3e7b095..7453241a68 100644 --- a/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs +++ b/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs @@ -17,7 +17,7 @@ namespace Uno.Extensions.Reactive.Tests.Extensions; public class Given_StateForEach : FeedTests { [TestMethod] - [ExpectedException(typeof(InvalidOperationException))] // Note: This is a compilation tests! + [ExpectedException(typeof(ArgumentNullException))] // Note: This is a compilation tests! public async Task When_ForEachAsync_Then_AcceptsNotNullAndStruct() { _ = default(IState)!.ForEach(async (i, ct) => this.ToString()); @@ -39,7 +39,7 @@ public async Task When_ForEachAsync_Then_AcceptsNotNullAndStruct() } [TestMethod] - [ExpectedException(typeof(InvalidOperationException))] // Note: This is a compilation tests! + [ExpectedException(typeof(ArgumentNullException))] // Note: This is a compilation tests! public async Task When_ForEachDataAsync_Then_AcceptsNotNullAndStruct() { _ = default(IState)!.ForEach(async (i, ct) => this.ToString()); diff --git a/src/Uno.Extensions.Reactive/Core/State.Extensions.cs b/src/Uno.Extensions.Reactive/Core/State.Extensions.cs index e0112687de..f810b004cb 100644 --- a/src/Uno.Extensions.Reactive/Core/State.Extensions.cs +++ b/src/Uno.Extensions.Reactive/Core/State.Extensions.cs @@ -212,8 +212,9 @@ public static IDisposable ForEachAsync(this IState state, AsyncAction public static IState ForEach(this IState state, AsyncAction action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) where T : notnull { - _ = new StateForEach(state, action.SomeOrNone(), $"ForEach defined in {caller} at line {line}."); - + _ = AttachedProperty.GetOrCreate(owner: state, + key: action, + factory: (s, ks) => new StateForEach(state, action.SomeOrNone(), $"ForEach defined in {caller} at line {line}.")); return state; } @@ -230,7 +231,9 @@ public static IState ForEach(this IState state, AsyncAction action, public static IState ForEach(this IState state, AsyncAction action, out IDisposable disposable, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) where T : notnull { - disposable = new StateForEach(state, action.SomeOrNone(), $"ForEach defined in {caller} at line {line}."); + disposable = AttachedProperty.GetOrCreate(owner: state, + key: action, + factory: (s, ks) => new StateForEach(state, action.SomeOrNone(), $"ForEach defined in {caller} at line {line}.")); return state; } @@ -247,7 +250,9 @@ public static IState ForEach(this IState state, AsyncAction action, public static IState ForEach(this IState state, AsyncAction action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) where T : struct { - _ = new StateForEach(state, action.SomeOrNone(), $"ForEach defined in {caller} at line {line}."); + _ = AttachedProperty.GetOrCreate(owner: state, + key: action, + factory: (s, ks) => new StateForEach(state, action.SomeOrNone(), $"ForEach defined in {caller} at line {line}.")); return state; } @@ -265,7 +270,9 @@ public static IState ForEach(this IState state, AsyncAction action, public static IState ForEach(this IState state, AsyncAction action, out IDisposable disposable, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) where T : struct { - disposable = new StateForEach(state, action.SomeOrNone(), $"ForEach defined in {caller} at line {line}."); + disposable = AttachedProperty.GetOrCreate(owner: state, + key: action, + factory: (s, ks) => new StateForEach(state, action.SomeOrNone(), $"ForEach defined in {caller} at line {line}.")); return state; } diff --git a/src/Uno.Extensions.Reactive/Operators/StateForEach.cs b/src/Uno.Extensions.Reactive/Operators/StateForEach.cs index 9cc187d214..4c55ffe335 100644 --- a/src/Uno.Extensions.Reactive/Operators/StateForEach.cs +++ b/src/Uno.Extensions.Reactive/Operators/StateForEach.cs @@ -20,6 +20,8 @@ internal sealed class StateForEach : IDisposable public StateForEach(ISignal> state, AsyncAction> action, string name = "-unnamed-") { + ArgumentNullException.ThrowIfNull(state); + if (state is not IStateImpl impl) { throw new InvalidOperationException("Execute is supported only on internal state implementation."); From 8bf71bf5729c712bf9629a13a281ae11f76e3f02 Mon Sep 17 00:00:00 2001 From: Andres Pineda Date: Wed, 21 Aug 2024 11:40:07 -0400 Subject: [PATCH 04/13] test: Observe assertions for multiple subscriptions --- .../Messaging/Given_Messaging.cs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/Uno.Extensions.Reactive.Tests/Messaging/Given_Messaging.cs b/src/Uno.Extensions.Reactive.Tests/Messaging/Given_Messaging.cs index 835cefb538..8579a5777b 100644 --- a/src/Uno.Extensions.Reactive.Tests/Messaging/Given_Messaging.cs +++ b/src/Uno.Extensions.Reactive.Tests/Messaging/Given_Messaging.cs @@ -57,6 +57,42 @@ public async Task When_Fluent_Value_Updated_Then_StateUpdated() result.Should().BeEquivalentTo(new MyEntity(42, 1)); } + [TestMethod] + public async Task When_Fluent_Multiple_Observe_Value_Updated_Then_StateUpdated_Once() + { + int callsCount = 0; + var messenger = new WeakReferenceMessenger(); + var state = State.Value(this, () => new MyEntity(42)) + .Observe(messenger, i => i.Key) + .Observe(messenger, i => i.Key) + .ForEach(async (i, ct) => callsCount++); + + messenger.Send(new EntityMessage(EntityChange.Updated, new(42, 1))); + + var result = await state; + result.Should().BeEquivalentTo(new MyEntity(42, 1)); + callsCount.Should().Be(1); + } + + [TestMethod] + public async Task When_Mixed_Multiple_Observe_Value_Updated_Then_StateUpdated_Once() + { + int callsCount = 0; + var messenger = new WeakReferenceMessenger(); + var state = State.Value(this, () => new MyEntity(42)) + .Observe(messenger, i => i.Key) + .ForEach(async (i, ct) => callsCount++); + + + messenger.Observe(state, i => i.Key); + + messenger.Send(new EntityMessage(EntityChange.Updated, new(42, 1))); + + var result = await state; + result.Should().BeEquivalentTo(new MyEntity(42, 1)); + callsCount.Should().Be(1); + } + [TestMethod] public async Task When_Fluent_Value_Updated_Then_StateUpdated_And_ForEach() { @@ -262,6 +298,21 @@ public async Task When_DisposedAndUpdated_Then_StateNotUpdated() result.Should().BeEquivalentTo(new MyEntity(42)); } + [TestMethod] + public async Task When_Fluent_DisposedAndUpdated_Then_StateNotUpdated() + { + var messenger = new WeakReferenceMessenger(); + var state = State.Value(this, () => new MyEntity(42)) + .Observe(messenger, i => i.Key, out var disposable); + + disposable.Dispose(); + + messenger.Send(new EntityMessage(EntityChange.Updated, new(42, 1))); + + var result = await state; + result.Should().BeEquivalentTo(new MyEntity(42)); + } + [TestMethod] public async Task When_DisposedAndUpdated_Then_ListStateNotUpdated() { From 6f24e31ff61c9e4e73ebe4e98148bcdc9fc21486 Mon Sep 17 00:00:00 2001 From: Andres Pineda Date: Wed, 21 Aug 2024 14:15:18 -0400 Subject: [PATCH 05/13] test: assert cancelling subscription --- .../Operators/Given_StateForEach.cs | 66 ++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs b/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs index 7453241a68..bf0d9a934d 100644 --- a/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs +++ b/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -82,6 +82,53 @@ public async Task When_Fluent_UpdateState_Then_CallbackInvokedIgnoringInitialVal result.Should().BeEquivalentTo(new[] { 2, 3, 4 }); } + [TestMethod] + public async Task When_Fluent_Multiple_UpdateState_Then_CallbackInvokedIgnoringInitialValue() + { + var result = new List(); + + ValueTask UpdateResultAsync(int i, CancellationToken ct) + { + result.Add(i); + + return ValueTask.CompletedTask; + } + + var state = State.Async(this, async ct => 1) + .ForEach(UpdateResultAsync) + .ForEach(UpdateResultAsync); + + await state.SetAsync(2, CT); + await state.SetAsync(3, CT); + await state.SetAsync(4, CT); + + result.Should().BeEquivalentTo(new[] { 2, 3, 4 }); + } + + [TestMethod] + public async Task When_Mixed_Multiple_UpdateState_Then_CallbackInvokedIgnoringInitialValue() + { + var result = new List(); + + ValueTask UpdateResultAsync(int i, CancellationToken ct) + { + result.Add(i); + + return ValueTask.CompletedTask; + } + + var state = State.Async(this, async ct => 1) + .ForEach(UpdateResultAsync); + + await state.ForEach(UpdateResultAsync); + + await state.SetAsync(2, CT); + await state.SetAsync(3, CT); + await state.SetAsync(4, CT); + + result.Should().BeEquivalentTo(new[] { 2, 3, 4 }); + } + [TestMethod] public async Task When_UpdateStateAndCallbackIsAsync_Then_CallsAreQueued() { @@ -150,6 +197,23 @@ public async Task When_DisposeState_Then_EnumerationStop() enumerationTask.Status.Should().Be(TaskStatus.RanToCompletion); } + [TestMethod] + public async Task When_Fluent_DisposeState_Then_EnumerationStop() + { + var state = State.Value(this, () => 1) + .ForEach(async (i, ct) => this.ToString(), out var sut); + + await state.DisposeAsync(); + + var enumerationTask = sut.GetType().GetField("_task", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(sut) as Task; + if (enumerationTask is null) + { + Assert.Fail("Unable to get the private _task field of the StateListener."); + } + + enumerationTask.Status.Should().Be(TaskStatus.RanToCompletion); + } + [TestMethod] public async Task When_DisposeExecute_Then_EnumerationStop() { From a7489f274026b2555dbdef3457f5200fc3f3a81a Mon Sep 17 00:00:00 2001 From: Andres Pineda Date: Wed, 21 Aug 2024 14:15:38 -0400 Subject: [PATCH 06/13] test: fix flaky test in CI --- .../Operators/Given_StateForEach.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs b/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs index bf0d9a934d..c7a218e961 100644 --- a/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs +++ b/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -119,7 +119,7 @@ ValueTask UpdateResultAsync(int i, CancellationToken ct) var state = State.Async(this, async ct => 1) .ForEach(UpdateResultAsync); - + await state.ForEach(UpdateResultAsync); await state.SetAsync(2, CT); @@ -177,6 +177,9 @@ await state.ForEach(async (i, ct) => await state.SetAsync(42, CT); await state.SetAsync(3, CT); + //Test fails sometime because of time needed to process the exception + await Task.Delay(100); + result.Should().BeEquivalentTo(new[] { 2, 3 }); } From 8b13a046fa6652b36e3c12baa3522699846daa71 Mon Sep 17 00:00:00 2001 From: Andres Pineda Date: Wed, 21 Aug 2024 16:15:19 -0400 Subject: [PATCH 07/13] feat(IListState): implement FluentAPI --- .../ListStateExtensions.cs | 40 ++++++++++++++++ .../Messaging/Given_Messaging.cs | 41 ++++++++++++++++ .../Operators/Given_StateForEach.cs | 15 +++++- .../Core/ListState.Extensions.cs | 48 +++++++++++++++++-- 4 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 src/Uno.Extensions.Reactive.Messaging/ListStateExtensions.cs diff --git a/src/Uno.Extensions.Reactive.Messaging/ListStateExtensions.cs b/src/Uno.Extensions.Reactive.Messaging/ListStateExtensions.cs new file mode 100644 index 0000000000..c8fba1409b --- /dev/null +++ b/src/Uno.Extensions.Reactive.Messaging/ListStateExtensions.cs @@ -0,0 +1,40 @@ +using CommunityToolkit.Mvvm.Messaging; + +namespace Uno.Extensions.Reactive.Messaging; + +/// +/// Set of extensions to update an from messaging structures. +/// +public static class ListStateExtensions +{ + /// + /// Listen for on the given and updates the accordingly. + /// + /// Type of the value of the state. + /// Type of the identifier that uniquely identifies a . + /// The list state to update. + /// The messenger to listen for + /// A selector to get a unique identifier of a . + /// A disposable that can be used to unbind the state from the messenger. + /// An that can be used to chain other operations. + public static IListState Observe(this IListState listState, IMessenger messenger, Func keySelector, out IDisposable disposable) + { + disposable = messenger.Observe(listState, keySelector); + return listState; + } + + /// + /// Listen for on the given and updates the accordingly. + /// + /// Type of the value of the state. + /// Type of the identifier that uniquely identifies a . + /// The list state to update. + /// The messenger to listen for + /// A selector to get a unique identifier of a . + /// An that can be used to chain other operations. + public static IListState Observe(this IListState listState, IMessenger messenger, Func keySelector) + { + _ = messenger.Observe(listState, keySelector); + return listState; + } +} diff --git a/src/Uno.Extensions.Reactive.Tests/Messaging/Given_Messaging.cs b/src/Uno.Extensions.Reactive.Tests/Messaging/Given_Messaging.cs index 8579a5777b..c4a5a3f55a 100644 --- a/src/Uno.Extensions.Reactive.Tests/Messaging/Given_Messaging.cs +++ b/src/Uno.Extensions.Reactive.Tests/Messaging/Given_Messaging.cs @@ -159,6 +159,19 @@ public async Task When_Created_Then_ListStateUpdated() result.Should().BeEquivalentTo(Items(42)); } + [TestMethod] + public async Task When_Fluent_Created_Then_ListStateUpdated() + { + var messenger = new WeakReferenceMessenger(); + var state = ListState.Empty(this) + .Observe(messenger, i => i.Key); + + messenger.Send(new EntityMessage(EntityChange.Created, new(42))); + + var result = await state; + result.Should().BeEquivalentTo(Items(42)); + } + [TestMethod] public async Task When_Updated_Then_ListStateUpdated() { @@ -173,6 +186,19 @@ public async Task When_Updated_Then_ListStateUpdated() result.Should().BeEquivalentTo(Items((42, 1))); } + [TestMethod] + public async Task When_Fluent_Updated_Then_ListStateUpdated() + { + var messenger = new WeakReferenceMessenger(); + var state = ListState.Value(this, () => Items(42)) + .Observe(messenger, i => i.Key); + + messenger.Send(new EntityMessage(EntityChange.Updated, new(42, 1))); + + var result = await state; + result.Should().BeEquivalentTo(Items((42, 1))); + } + [TestMethod] public async Task When_Deleted_Then_ListStateUpdated() { @@ -327,6 +353,21 @@ public async Task When_DisposedAndUpdated_Then_ListStateNotUpdated() result.Should().BeEquivalentTo(Items(42)); } + [TestMethod] + public async Task When_Fluent_DisposedAndUpdated_Then_ListStateNotUpdated() + { + var messenger = new WeakReferenceMessenger(); + var state = ListState.Value(this, () => Items(42)) + .Observe(messenger, i => i.Key, out var sut); + + sut.Dispose(); + + messenger.Send(new EntityMessage(EntityChange.Updated, new(42, 1))); + + var result = await state; + result.Should().BeEquivalentTo(Items(42)); + } + [TestMethod] public async Task When_DisposedAndUpdatedUsingOther_Then_StateNotUpdated() { diff --git a/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs b/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs index c7a218e961..1ebde035db 100644 --- a/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs +++ b/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs @@ -261,7 +261,20 @@ public async Task When_UpdateListStateWithNone_Then_CallbackGetsEmptyList() var state = ListState.Value(this, () => ImmutableList.Create(1, 2, 3)); var result = new List>(); - state.ForEachAsync(async (list, ct) => result.Add(list)); + await state.ForEach(async (list, ct) => result.Add(list)); + + await state.UpdateDataAsync(_ => Option.None>(), CT); + + result.Single().Should().NotBeNull().And.BeEquivalentTo(ImmutableList.Empty); + } + + [TestMethod] + public async Task When_Fluent_UpdateListStateWithNone_Then_CallbackGetsEmptyList() + { + var result = new List>(); + + var state = ListState.Value(this, () => ImmutableList.Create(1, 2, 3)) + .ForEach(async (list, ct) => result.Add(list)); await state.UpdateDataAsync(_ => Option.None>(), CT); diff --git a/src/Uno.Extensions.Reactive/Core/ListState.Extensions.cs b/src/Uno.Extensions.Reactive/Core/ListState.Extensions.cs index 7c8ec47fec..fa8e046c95 100644 --- a/src/Uno.Extensions.Reactive/Core/ListState.Extensions.cs +++ b/src/Uno.Extensions.Reactive/Core/ListState.Extensions.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Uno.Extensions.Equality; +using Uno.Extensions.Reactive.Utils; namespace Uno.Extensions.Reactive; @@ -181,13 +182,26 @@ public static ValueTask UpdateAsync(this IListState listState, T item, Can [EditorBrowsable(EditorBrowsableState.Never)] [MethodImpl(MethodImplOptions.AggressiveInlining)] #if DEBUG // To avoid usage in internal reactive code, but without forcing apps to update right away - [Obsolete("Use ForEachAsync")] + [Obsolete("Use ForEach")] #endif public static IDisposable Execute(this IListState state, AsyncAction> action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) where T : notnull => ForEachAsync(state, action, caller, line); + /// + /// [DEPRECATED] Use .ForEach instead + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if DEBUG // To avoid usage in internal reactive code, but without forcing apps to update right away + [Obsolete("Use ForEach")] +#endif + public static IDisposable ForEachAsync(this IListState state, AsyncAction> action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) + where T : notnull + => new StateForEach>(state, (list, ct) => action(list.SomeOrDefault() ?? ImmutableList.Empty, ct), $"ForEachAsync defined in {caller} at line {line}."); + + /// /// Execute an async callback each time the state is being updated. /// @@ -196,10 +210,36 @@ public static IDisposable Execute(this IListState state, AsyncActionThe callback to invoke on each update of the state. /// For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this. /// For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this. - /// A that can be used to remove the callback registration. - public static IDisposable ForEachAsync(this IListState state, AsyncAction> action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) + /// A that can be used to chain other operations. + public static IListState ForEach(this IListState state, AsyncAction> action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) where T : notnull - => new StateForEach>(state, (list, ct) => action(list.SomeOrDefault() ?? ImmutableList.Empty, ct), $"ForEachAsync defined in {caller} at line {line}."); + { + _ = AttachedProperty.GetOrCreate(owner: state, + key: action, + factory: (s, ks) => new StateForEach>(state, (list, ct) => action(list.SomeOrDefault() ?? ImmutableList.Empty, ct), $"ForEachAsync defined in {caller} at line {line}.")); + return state; + } + + /// + /// Execute an async callback each time the state is being updated. + /// + /// The type of the state + /// The state to listen. + /// The callback to invoke on each update of the state. + /// For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this. + /// For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this. + /// A that can be used to remove the callback registration. + /// A that can be used to chain other operations. + public static IListState ForEach(this IListState state, AsyncAction> action, out IDisposable disposable, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) + where T : notnull + { + disposable = AttachedProperty.GetOrCreate(owner: state, + key: action, + factory: (s, ks) => new StateForEach>(state, (list, ct) => action(list.SomeOrDefault() ?? ImmutableList.Empty, ct), $"ForEachAsync defined in {caller} at line {line}.")); + + return state; + } + #endregion /// From a2b905f53e6950a43bc20cd32af35637287e3136 Mon Sep 17 00:00:00 2001 From: Andres Pineda Date: Wed, 21 Aug 2024 16:52:16 -0400 Subject: [PATCH 08/13] docs: update docs to match changes --- doc/Learn/Mvux/Advanced/Selection.md | 36 ++++++++++++------------- doc/Learn/Mvux/ListStates.md | 10 +++---- doc/Learn/Mvux/States.md | 40 +++++++++++++++++++--------- 3 files changed, 51 insertions(+), 35 deletions(-) diff --git a/doc/Learn/Mvux/Advanced/Selection.md b/doc/Learn/Mvux/Advanced/Selection.md index 585d967950..0414b91ac6 100644 --- a/doc/Learn/Mvux/Advanced/Selection.md +++ b/doc/Learn/Mvux/Advanced/Selection.md @@ -4,15 +4,15 @@ uid: Uno.Extensions.Mvux.Advanced.Selection # Selection -MVUX has embedded support for both [single item](#single-item-selection) and [multi-item selection](#multi-item-selection). -Any control that inherits [`Selector`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.primitives.selector) (e.g. [`ListView`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.listview), [`GridView`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.gridview), [`ComboBox`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.combobox), [`FlipView`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.flipview)), has automatic support for updating a List-State with its current selection. -Binding to the `SelectedItem` property is not even required, as this works automatically. +MVUX has embedded support for both [single item](#single-item-selection) and [multi-item selection](#multi-item-selection). +Any control that inherits [`Selector`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.primitives.selector) (e.g. [`ListView`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.listview), [`GridView`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.gridview), [`ComboBox`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.combobox), [`FlipView`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.flipview)), has automatic support for updating a List-State with its current selection. +Binding to the `SelectedItem` property is not even required, as this works automatically. To synchronize to the selected value in the `Model` side, use the `Selection` operator of the `IListFeed`. ## Recap of the *PeopleApp* example -We'll be using the *PeopleApp* example which we've built step-by-step in [this tutorial](xref:Uno.Extensions.Mvux.HowToListFeed). +We'll be using the *PeopleApp* example which we've built step-by-step in [this tutorial](xref:Uno.Extensions.Mvux.HowToListFeed). The *PeopleApp* uses an `IListFeed` where `T` is a `Person` [record](https://learn.microsoft.com/dotnet/csharp/language-reference/builtin-types/record) with the properties `FirstName` and `LastName`. It has a service that has the following contract: @@ -50,19 +50,19 @@ The data is then displayed on the View using a `ListView`: ``` -> [!NOTE] +> [!NOTE] > The use of the `FeedView` is not necessary in our example, hence the `ListView` has been extracted from it, and its `ItemsSource` property has been directly data-bound to the Feed. ### Implement selection in the *PeopleApp* MVUX has two extension methods of `IListFeed`, that enable single or multi-selection. -> [!NOTE] +> [!NOTE] > The source code for the sample app demonstrated in this section can be found on [GitHub](https://github.com/unoplatform/Uno.Samples/tree/master/UI/MvuxHowTos/SelectionPeopleApp). ## Single-item selection -A Feed doesn't store any state, so the `People` property won't be able to hold any information, nor the currently selected item. +A Feed doesn't store any state, so the `People` property won't be able to hold any information, nor the currently selected item. To enable storing the selected value in the model, we'll create an `IState` which will be updated by the `Selection` operator of the `IListFeed` (it's an extension method). Let's change the `PeopleModel` as follows: @@ -81,12 +81,12 @@ public partial record PeopleModel(IPeopleService PeopleService) The `SelectedPerson` State is initialized with an empty value using `State.Empty(this)` (we still need a reference to the current instance to enable caching). -> [!NOTE] +> [!NOTE] > Read [this](xref:Uno.Extensions.Mvux.States#other-ways-to-create-feeds) to learn more about States and the `Empty` factory method. The `Selection` operator was added to the existing `ListFeed.Async(...)` line, it will listen to the `People` List-Feed and will affect its selection changes onto the `SelectedPerson` State property. -In the View side, wrap the `ListView` element in a `Grid`, and insert additional elements to reflect the currently selected value via the `SelectedPerson` State. +In the View side, wrap the `ListView` element in a `Grid`, and insert additional elements to reflect the currently selected value via the `SelectedPerson` State. We'll also add a separator (using `Border`) to be able to distinguish them. The View code shall look like the following: @@ -118,7 +118,7 @@ When running the app, the top section will reflect the item the user selects in ![A video demonstrating selection with MVUX](../Assets/Selection.gif) -> [!NOTE] +> [!NOTE] > The source code for the sample app can be found [GitHub](https://github.com/unoplatform/Uno.Samples/tree/master/UI/MvuxHowTos/SelectionPeopleApp). ### Listening to the selected value @@ -139,9 +139,9 @@ A `TextBlock` can then be added in the UI to display the selected value: ``` -#### Using the ForEachAsync operator +#### Using the ForEach operator -Selection can also be propagated manually to a State using the [`ForEachAsync`](xref:Uno.Extensions.Mvux.States#foreachasync) operator. +Selection can also be propagated manually to a State using the [`ForEach`](xref:Uno.Extensions.Mvux.States#foreach) operator. First, we need to create a State with a default value, which will be used to store the processed value once a selection has occurred. ```csharp @@ -155,11 +155,11 @@ public partial record PeopleModel { private IPeopleService _peopleService; - public PeopleModel(IPeopleService peopleService) + public PeopleModel(IPeopleService peopleService) { _peopleService = peopleService; - SelectedPerson.ForEachAsync(action: SelectionChanged); + SelectedPerson.ForEach(action: SelectionChanged); } ... @@ -176,7 +176,7 @@ public partial record PeopleModel The `ForEach` operator listens to a selection occurrence and invokes the `SelectionChanged` callback with the newly available data, in this case, the recently selected `Person` entity. -> [!TIP] +> [!TIP] > MVUX takes care of the lifetime of the subscription, so it will be disposed of along with its declaring `Model` being garbage-collected. #### On-demand using a Command parameter @@ -192,7 +192,7 @@ public ValueTask CheckSelection(Person selectedPerson) In the above example, since `selectedPerson` has the same name as the `SelectedPerson` feed, it will be automatically evaluated and provided as a parameter on the command execution. -> [!TIP] +> [!TIP] > This behavior can also be controlled using attributes. > To learn more about commands and how they can be configured using attributes, refer to the [Commands](xref:Uno.Extensions.Mvux.Advanced.Commands) page. @@ -224,12 +224,12 @@ public partial record PeopleModel(IPeopleService PeopleService) Head to the View and enable multi-selection in the `ListView` by changing its `SelectionMode` property to `Multiple`. -> [!NOTE] +> [!NOTE] > The source code for the sample app can be found [here](https://github.com/unoplatform/Uno.Samples/tree/master/UI/MvuxHowTos/SelectionPeopleApp). ## Manual selection -The options above explained how to subscribe to selection that has been requested in the View by a Selector control (i.e. `ListView`). +The options above explained how to subscribe to selection that has been requested in the View by a Selector control (i.e. `ListView`). If you want to manually select an item or multiple items, rather use a [List-State](xref:Uno.Extensions.Mvux.ListStates) instead of a List-Feed to load the items, so that you can update their selection state. You can then use the List-State's selection operators to manually select items. Refer to the [selection operators](xref:Uno.Extensions.Mvux.ListStates#selection-operators) section in the List-State page for documentation on how to use manual selection. diff --git a/doc/Learn/Mvux/ListStates.md b/doc/Learn/Mvux/ListStates.md index 3d20cfc312..c0f875859b 100644 --- a/doc/Learn/Mvux/ListStates.md +++ b/doc/Learn/Mvux/ListStates.md @@ -94,7 +94,7 @@ await MyStrings.InsertAsync("Margaret Atwood", cancellationToken); There are various ways to update values in the list-state: -The `Update` method has an `updater` parameter like the `State` does. +The `Update` method has an `updater` parameter like the `State` does. This parameter is a `Func, IImmutableList>`, which when called passes in the existing collection, allows you to apply your modifications to it, and then returns it. For example: @@ -134,12 +134,12 @@ await MyStrings.RemoveAllAsync( ct: cancellationToken); ``` -#### ForEachAsync +#### ForEach This operator can be called from an `IListState` to execute an asynchronous action when the data changes. The action is invoked once for the entire set of data, rather than for individual items: ```csharp -await MyStrings.ForEachAsync(async(list, ct) => await PerformAction(items, ct)); +await MyStrings.ForEach(async(list, ct) => await PerformAction(items, ct)); ... @@ -152,7 +152,7 @@ private async ValueTask PerformAction(IImmutableList items, Cancellation ### Selection operators -Like list-feed, list-state provides out-the-box support for Selection. +Like list-feed, list-state provides out-the-box support for Selection. This feature enables flagging single or multiple items in the State as 'selected'. Selection works seamlessly and automatically with the `ListView` and other selection controls. @@ -199,5 +199,5 @@ await MyStrings.ClearSelection(cancellationToken); ### Subscribing to the selection -You can create a Feed that reflects the currently selected item or items (when using multi-selection) of a Feed. +You can create a Feed that reflects the currently selected item or items (when using multi-selection) of a Feed. This is explained in detail in the [Selection page](xref:Uno.Extensions.Mvux.Advanced.Selection). diff --git a/doc/Learn/Mvux/States.md b/doc/Learn/Mvux/States.md index 2336c2d276..49da780efd 100644 --- a/doc/Learn/Mvux/States.md +++ b/doc/Learn/Mvux/States.md @@ -10,10 +10,10 @@ Like [feeds](xref:Uno.Extensions.Mvux.Feeds), states are used to manage asynchro Contrary to Feeds, states are stateful (as the name suggests) in that they keep a record of the current data value. States also allow the current value to be modified, which is useful for two-way binding scenarios. -MVUX utilizes its powerful code-generation engine to generate a bindable proxy for each Model, which holds the state information of the data, as well as a bindable proxy for entities where needed, for instance, if the entities are immutable (e.g. records - the recommended type). +MVUX utilizes its powerful code-generation engine to generate a bindable proxy for each Model, which holds the state information of the data, as well as a bindable proxy for entities where needed, for instance, if the entities are immutable (e.g. records - the recommended type). The bindable proxies use as a bridge that enables immutable entities to work with the WinUI data-binding engine. The states in the Model are monitored for data-binding changes, and in response to any change, the objects are recreated fresh, instead of their properties being changed. -States keep the current value of the data, so every new subscription to them, (such as awaiting them or binding them to an additional control, etc.), will use the data currently loaded in the state (if any). +States keep the current value of the data, so every new subscription to them, (such as awaiting them or binding them to an additional control, etc.), will use the data currently loaded in the state (if any). Like a feed, states can be reloaded, which will invoke the asynchronous operation that is used to create the state. @@ -52,7 +52,7 @@ A State can also be created from an Async Enumerable as follows: public IState MyStockCurrentValue => State.AsyncEnumerable(this, ContactsService.GetMyStockCurrentValue); ``` -Make sure the Async Enumerable methods have a `CancellationToken` parameter and are decorated with the `EnumerationCancellation` attribute. +Make sure the Async Enumerable methods have a `CancellationToken` parameter and are decorated with the `EnumerationCancellation` attribute. You can learn more about Async Enumerables in [this article](https://learn.microsoft.com/archive/msdn-magazine/2019/november/csharp-iterating-with-async-enumerables-in-csharp-8#a-tour-through-async-enumerables). #### Start with an empty state @@ -83,7 +83,7 @@ public IState MyState => State.FromFeed(this, MyFeed); #### Other ways to create states > [!TIP] -> A state can also be constructed manually by building its underlying Messages or Options. +> A state can also be constructed manually by building its underlying Messages or Options. > This is intended for advanced users and is explained [here](xref:Uno.Extensions.Reactive.State#create). ### Usage of States @@ -111,7 +111,7 @@ States are built to be cooperating with the data-binding engine. A State will au 1. Replace all child elements in the _MainPage.xaml_ with the following: ```xml - - + - + - + @@ -144,7 +144,7 @@ In this scenario, the `DataContext` is set to an instance of the `BindableSlider #### Update -To manually update the current value of a state, use its `Update` method. +To manually update the current value of a state, use its `Update` method. In this example we'll add the method `IncrementSlider` that gets the current value and increases it by one (if it doesn't exceed 100): @@ -168,14 +168,14 @@ There are additional methods that update the data of a State such as `Set` and ` ```csharp public async ValueTask SetSliderMiddle(CancellationToken ct = default) -{ +{ await SliderValue.SetAsync(50, ct); } ``` ### Subscribing to changes -The `ForEachAsync` enables executing a callback each time the value of the `IState` is updated. +The `ForEach` enables executing a callback each time the value of the `IState` is updated. This extension-method takes a single parameter which is a async callback that takes two parameters. The first parameter is of type `T?`, where `T` is type of the `IState`, and represents the new value of the state. The second parameter is a `CancellationToken` which can be used to cancel a long running action. @@ -188,7 +188,7 @@ public partial record Model public async ValueTask EnableChangeTracking() { - MyState.ForEachAsync(PerformAction); + MyState.ForEach(PerformAction); } public async ValueTask PerformAction(string item, CancellationToken ct) @@ -198,6 +198,22 @@ public partial record Model } ``` +Additionally, the `ForEach` method can be set using the Fluent API: + +```csharp +public partial record Model +{ + public IState MyState => State.Value(this, "Initial value") + .ForEach(PerformAction); + + public async ValueTask PerformAction(string item, CancellationToken ct) + { + ... + } +} + +``` + ### Commands Part of the MVUX toolbox is the automatic generation of Commands. From e581544ccabef2e65bd56565da105fb257fe833b Mon Sep 17 00:00:00 2001 From: Andres Pineda <1900897+ajpinedam@users.noreply.github.com> Date: Fri, 23 Aug 2024 09:29:18 -0400 Subject: [PATCH 09/13] chore: apply suggestions from code review Co-authored-by: Dominik Titl <78549750+morning4coffe-dev@users.noreply.github.com> --- doc/Learn/Mvux/Advanced/Selection.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/Learn/Mvux/Advanced/Selection.md b/doc/Learn/Mvux/Advanced/Selection.md index 0414b91ac6..aaefa706a3 100644 --- a/doc/Learn/Mvux/Advanced/Selection.md +++ b/doc/Learn/Mvux/Advanced/Selection.md @@ -4,7 +4,7 @@ uid: Uno.Extensions.Mvux.Advanced.Selection # Selection -MVUX has embedded support for both [single item](#single-item-selection) and [multi-item selection](#multi-item-selection). +MVUX has built-in support for both [single item](#single-item-selection) and [multi-item selection](#multi-item-selection). Any control that inherits [`Selector`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.primitives.selector) (e.g. [`ListView`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.listview), [`GridView`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.gridview), [`ComboBox`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.combobox), [`FlipView`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.flipview)), has automatic support for updating a List-State with its current selection. Binding to the `SelectedItem` property is not even required, as this works automatically. @@ -86,7 +86,7 @@ The `SelectedPerson` State is initialized with an empty value using `State Date: Mon, 26 Aug 2024 11:58:08 -0400 Subject: [PATCH 10/13] chore: apply suggestions from code review Co-authored-by: David --- src/Uno.Extensions.Reactive.Messaging/MessengerExtensions.cs | 2 +- .../Messaging/Given_Messaging.cs | 5 +++-- src/Uno.Extensions.Reactive/Core/ListState.Extensions.cs | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Uno.Extensions.Reactive.Messaging/MessengerExtensions.cs b/src/Uno.Extensions.Reactive.Messaging/MessengerExtensions.cs index 711db28468..bb9d117eed 100644 --- a/src/Uno.Extensions.Reactive.Messaging/MessengerExtensions.cs +++ b/src/Uno.Extensions.Reactive.Messaging/MessengerExtensions.cs @@ -26,7 +26,7 @@ public static IDisposable Observe(this IMessenger messenger, ISta => AttachedProperty.GetOrCreate(owner: state, key: keySelector, state: messenger, - factory: (s, ks, msg) => new Recipient, TEntity, TKey>(s, msg, ks, StateExtensions.Update)); + factory: static (s, ks, msg) => new Recipient, TEntity, TKey>(s, msg, ks, StateExtensions.Update)); /// /// Listen for on the given and updates the accordingly. diff --git a/src/Uno.Extensions.Reactive.Tests/Messaging/Given_Messaging.cs b/src/Uno.Extensions.Reactive.Tests/Messaging/Given_Messaging.cs index c4a5a3f55a..acd210fc84 100644 --- a/src/Uno.Extensions.Reactive.Tests/Messaging/Given_Messaging.cs +++ b/src/Uno.Extensions.Reactive.Tests/Messaging/Given_Messaging.cs @@ -48,8 +48,9 @@ public async Task When_Updated_Then_StateUpdated() public async Task When_Fluent_Value_Updated_Then_StateUpdated() { var messenger = new WeakReferenceMessenger(); - var state = State.Value(this, () => new MyEntity(42)) - .Observe(messenger, i => i.Key); + var state = State + .Value(this, () => new MyEntity(42)) + .Observe(messenger, i => i.Key); messenger.Send(new EntityMessage(EntityChange.Updated, new(42, 1))); diff --git a/src/Uno.Extensions.Reactive/Core/ListState.Extensions.cs b/src/Uno.Extensions.Reactive/Core/ListState.Extensions.cs index fa8e046c95..272a027e88 100644 --- a/src/Uno.Extensions.Reactive/Core/ListState.Extensions.cs +++ b/src/Uno.Extensions.Reactive/Core/ListState.Extensions.cs @@ -216,7 +216,7 @@ public static IListState ForEach(this IListState state, AsyncAction new StateForEach>(state, (list, ct) => action(list.SomeOrDefault() ?? ImmutableList.Empty, ct), $"ForEachAsync defined in {caller} at line {line}.")); + factory: static (s, ks) => new StateForEach>(state, (list, ct) => action(list.SomeOrDefault() ?? ImmutableList.Empty, ct), $"ForEach defined in {caller} at line {line}.")); return state; } From c65856ea0602af25c3ed127e0e1fa6da8a93c066 Mon Sep 17 00:00:00 2001 From: Andres Pineda Date: Mon, 26 Aug 2024 15:59:51 -0400 Subject: [PATCH 11/13] chore: mvux make factory static --- .../Core/ListState.Extensions.cs | 17 +++++---- .../Core/State.Extensions.cs | 35 ++++++++++++------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/Uno.Extensions.Reactive/Core/ListState.Extensions.cs b/src/Uno.Extensions.Reactive/Core/ListState.Extensions.cs index 272a027e88..6819ee7a91 100644 --- a/src/Uno.Extensions.Reactive/Core/ListState.Extensions.cs +++ b/src/Uno.Extensions.Reactive/Core/ListState.Extensions.cs @@ -214,9 +214,12 @@ public static IDisposable ForEachAsync(this IListState state, AsyncAction< public static IListState ForEach(this IListState state, AsyncAction> action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) where T : notnull { - _ = AttachedProperty.GetOrCreate(owner: state, - key: action, - factory: static (s, ks) => new StateForEach>(state, (list, ct) => action(list.SomeOrDefault() ?? ImmutableList.Empty, ct), $"ForEach defined in {caller} at line {line}.")); + _ = AttachedProperty.GetOrCreate( + owner: state, + key: action, + state: (caller, line), + factory: static (s, a, d) => new StateForEach>(s, (list, ct) => a(list.SomeOrDefault() ?? ImmutableList.Empty, ct), $"ForEach defined in {d.caller} at line {d.line}.")); + return state; } @@ -233,9 +236,11 @@ public static IListState ForEach(this IListState state, AsyncAction ForEach(this IListState state, AsyncAction> action, out IDisposable disposable, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) where T : notnull { - disposable = AttachedProperty.GetOrCreate(owner: state, - key: action, - factory: (s, ks) => new StateForEach>(state, (list, ct) => action(list.SomeOrDefault() ?? ImmutableList.Empty, ct), $"ForEachAsync defined in {caller} at line {line}.")); + disposable = AttachedProperty.GetOrCreate( + owner: state, + key: action, + state: (caller, line), + factory: static (s, a, d) => new StateForEach>(s, (list, ct) => a(list.SomeOrDefault() ?? ImmutableList.Empty, ct), $"ForEachAsync defined in {d.caller} at line {d.line}.")); return state; } diff --git a/src/Uno.Extensions.Reactive/Core/State.Extensions.cs b/src/Uno.Extensions.Reactive/Core/State.Extensions.cs index f810b004cb..726e2f4a94 100644 --- a/src/Uno.Extensions.Reactive/Core/State.Extensions.cs +++ b/src/Uno.Extensions.Reactive/Core/State.Extensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; @@ -212,9 +212,12 @@ public static IDisposable ForEachAsync(this IState state, AsyncAction public static IState ForEach(this IState state, AsyncAction action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) where T : notnull { - _ = AttachedProperty.GetOrCreate(owner: state, - key: action, - factory: (s, ks) => new StateForEach(state, action.SomeOrNone(), $"ForEach defined in {caller} at line {line}.")); + _ = AttachedProperty.GetOrCreate( + owner: state, + key: action, + state: (caller, line), + factory: static (s, a, d) => new StateForEach(s, a.SomeOrNone(), $"ForEach defined in {d.caller} at line {d.line}.")); + return state; } @@ -231,9 +234,11 @@ public static IState ForEach(this IState state, AsyncAction action, public static IState ForEach(this IState state, AsyncAction action, out IDisposable disposable, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) where T : notnull { - disposable = AttachedProperty.GetOrCreate(owner: state, - key: action, - factory: (s, ks) => new StateForEach(state, action.SomeOrNone(), $"ForEach defined in {caller} at line {line}.")); + disposable = AttachedProperty.GetOrCreate( + owner: state, + key: action, + state: (caller, line), + factory: static (s, a, d) => new StateForEach(s, a.SomeOrNone(), $"ForEach defined in {d.caller} at line {d.line}.")); return state; } @@ -250,9 +255,11 @@ public static IState ForEach(this IState state, AsyncAction action, public static IState ForEach(this IState state, AsyncAction action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) where T : struct { - _ = AttachedProperty.GetOrCreate(owner: state, - key: action, - factory: (s, ks) => new StateForEach(state, action.SomeOrNone(), $"ForEach defined in {caller} at line {line}.")); + _ = AttachedProperty.GetOrCreate( + owner: state, + key: action, + state: (caller, line), + factory: static (s, a, d) => new StateForEach(s, a.SomeOrNone(), $"ForEach defined in {d.caller} at line {d.line}.")); return state; } @@ -270,9 +277,11 @@ public static IState ForEach(this IState state, AsyncAction action, public static IState ForEach(this IState state, AsyncAction action, out IDisposable disposable, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) where T : struct { - disposable = AttachedProperty.GetOrCreate(owner: state, - key: action, - factory: (s, ks) => new StateForEach(state, action.SomeOrNone(), $"ForEach defined in {caller} at line {line}.")); + disposable = AttachedProperty.GetOrCreate( + owner: state, + key: action, + state: (caller, line), + factory: static (s, a, d) => new StateForEach(s, a.SomeOrNone(), $"ForEach defined in {d.caller} at line {d.line}.")); return state; } From ddf9b720cc5273d3b41bfe6fbc3db99a9e9154a1 Mon Sep 17 00:00:00 2001 From: Andres Pineda Date: Mon, 26 Aug 2024 16:00:39 -0400 Subject: [PATCH 12/13] feat(mvux): make foreachdata fluent --- .../Operators/Given_StateForEach.cs | 14 +++--- .../Core/State.Extensions.cs | 49 +++++++++++++++++-- 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs b/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs index 1ebde035db..8eca06ce53 100644 --- a/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs +++ b/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs @@ -43,13 +43,13 @@ public async Task When_ForEachAsync_Then_AcceptsNotNullAndStruct() public async Task When_ForEachDataAsync_Then_AcceptsNotNullAndStruct() { _ = default(IState)!.ForEach(async (i, ct) => this.ToString()); - default(IState)!.ForEachDataAsync(async (i, ct) => this.ToString()); - default(IState)!.ForEachDataAsync(async (i, ct) => this.ToString()); - default(IState)!.ForEachDataAsync(async (i, ct) => this.ToString()); - default(IState)!.ForEachDataAsync(async (i, ct) => this.ToString()); - default(IState)!.ForEachDataAsync(async (i, ct) => this.ToString()); - default(IState)!.ForEachDataAsync(async (i, ct) => this.ToString()); - default(IState)!.ForEachDataAsync(async (i, ct) => this.ToString()); + _ = default(IState)!.ForEachData(async (i, ct) => this.ToString()); + _ = default(IState)!.ForEachData(async (i, ct) => this.ToString()); + _ = default(IState)!.ForEachData(async (i, ct) => this.ToString()); + _ = default(IState)!.ForEachData(async (i, ct) => this.ToString()); + _ = default(IState)!.ForEachData(async (i, ct) => this.ToString()); + _ = default(IState)!.ForEachData(async (i, ct) => this.ToString()); + _ = default(IState)!.ForEachData(async (i, ct) => this.ToString()); } [TestMethod] diff --git a/src/Uno.Extensions.Reactive/Core/State.Extensions.cs b/src/Uno.Extensions.Reactive/Core/State.Extensions.cs index 726e2f4a94..9e93e4eead 100644 --- a/src/Uno.Extensions.Reactive/Core/State.Extensions.cs +++ b/src/Uno.Extensions.Reactive/Core/State.Extensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; @@ -287,6 +287,16 @@ public static IState ForEach(this IState state, AsyncAction action, } + /// + /// [DEPRECATED] Use ForEachData instead + /// + [EditorBrowsable(EditorBrowsableState.Never)] +#if DEBUG // To avoid usage in internal reactive code, but without forcing apps to update right away + [Obsolete("Use ForEachData")] +#endif + public static IDisposable ForEachDataAsync(this IState state, AsyncAction> action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) + => new StateForEach(state, action, $"ForEachDataAsync defined in {caller} at line {line}."); + /// /// Execute an async callback each time the state is being updated. /// @@ -294,10 +304,39 @@ public static IState ForEach(this IState state, AsyncAction action, /// The state to listen. /// The callback to invoke on each update of the state. /// For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this. - /// For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this. - /// A that can be used to remove the callback registration. - public static IDisposable ForEachDataAsync(this IState state, AsyncAction> action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) - => new StateForEach(state, action, $"ForEachDataAsync defined in {caller} at line {line}."); + /// For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this. + /// An that can be used to chain other operations. + public static IState ForEachData(this IState state, AsyncAction> action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) + { + _ = AttachedProperty.GetOrCreate( + owner: state, + key: action, + state: (caller, line), + factory: static (s, a, d) => new StateForEach(s, a, $"ForEachData defined in {d.caller} at line {d.line}.")); + + return state; + } + + /// + /// Execute an async callback each time the state is being updated. + /// + /// The type of the state + /// The state to listen. + /// The callback to invoke on each update of the state. + /// A that can be used to remove the callback registration. + /// For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this. + /// For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this. + /// An that can be used to chain other operations. + public static IState ForEachData(this IState state, AsyncAction> action, out IDisposable disposable, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1) + { + disposable = AttachedProperty.GetOrCreate( + owner: state, + key: action, + state: (caller, line), + factory: static (s, a, d) => new StateForEach(s, a, $"ForEachData defined in {d.caller} at line {d.line}.")); + + return state; + } /// /// [DEPRECATED] Use .ForEachAsync instead From f7184babfde5725e0612d9750288f42e5874963c Mon Sep 17 00:00:00 2001 From: Andres Pineda Date: Tue, 27 Aug 2024 09:09:00 -0400 Subject: [PATCH 13/13] chore: remove WA for test --- .../Operators/Given_StateForEach.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs b/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs index 8eca06ce53..78abc0b2df 100644 --- a/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs +++ b/src/Uno.Extensions.Reactive.Tests/Operators/Given_StateForEach.cs @@ -177,9 +177,6 @@ await state.ForEach(async (i, ct) => await state.SetAsync(42, CT); await state.SetAsync(3, CT); - //Test fails sometime because of time needed to process the exception - await Task.Delay(100); - result.Should().BeEquivalentTo(new[] { 2, 3 }); }