diff --git a/src/Uno.Extensions.Core/Option.T.cs b/src/Uno.Extensions.Core/Option.T.cs index 9fb9378f74..f2d05acd5f 100644 --- a/src/Uno.Extensions.Core/Option.T.cs +++ b/src/Uno.Extensions.Core/Option.T.cs @@ -166,6 +166,9 @@ public override string ToString() { OptionType.Undefined => $"Undefined<{typeof(T).Name}>", OptionType.None => $"None<{typeof(T).Name}>", - _ => $"Some({_value switch { string => _value, IEnumerable enumerable => string.Join(",", enumerable.Cast()), _ => _value }})", + _ when _value is null => "Some(--null--)", + _ when _value is string str => $"Some({str})", + _ when _value is IEnumerable enumerable => $"Some({string.Join(",", enumerable.Cast())})", + _ => $"Some({_value})", }; } diff --git a/src/Uno.Extensions.Reactive.UI/Utils/Dispatching/DispatcherQueueProvider.cs b/src/Uno.Extensions.Reactive.UI/Utils/Dispatching/DispatcherQueueProvider.cs index 821325330a..93823b80cc 100644 --- a/src/Uno.Extensions.Reactive.UI/Utils/Dispatching/DispatcherQueueProvider.cs +++ b/src/Uno.Extensions.Reactive.UI/Utils/Dispatching/DispatcherQueueProvider.cs @@ -37,7 +37,7 @@ public Dispatcher(DispatcherQueue queue) /// public bool TryEnqueue(Action action) - => _queue.TryEnqueue(() => action()); + => _queue.TryEnqueue(Unsafe.As(action)); /// public async ValueTask ExecuteAsync(AsyncFunc action, CancellationToken ct) diff --git a/src/Uno.Extensions.Reactive/Core/Internal/FeedSubscription.cs b/src/Uno.Extensions.Reactive/Core/Internal/FeedSubscription.cs index 7c372b8f8d..3d2887b20b 100644 --- a/src/Uno.Extensions.Reactive/Core/Internal/FeedSubscription.cs +++ b/src/Uno.Extensions.Reactive/Core/Internal/FeedSubscription.cs @@ -10,6 +10,7 @@ using Uno.Extensions.Reactive.Logging; using Uno.Extensions.Reactive.Operators; using Uno.Extensions.Reactive.Utils; +using Uno.Extensions.Reactive.Utils.Logging; namespace Uno.Extensions.Reactive.Core; @@ -35,7 +36,7 @@ public FeedSubscription(ISignal> feed, SourceContext rootContext) isInitialSyncValuesSkippingAllowed: true); } - string ISourceContextOwner.Name => $"Sub on '{_feed}' for ctx '{_context.Parent!.Owner.Name}'."; + string ISourceContextOwner.Name => $"Sub on '{LogHelper.GetIdentifier(_feed)}' for ctx '{_context.Parent!.Owner.Name}'."; IDispatcher? ISourceContextOwner.Dispatcher => null; @@ -64,7 +65,7 @@ public async IAsyncEnumerable> GetMessages(SourceContext subscriberCo { // We make sure that even if we are replaying a previous message, the changes collection contains all keys. isFirstMessage = false; - yield return Message.Initial.OverrideBy(msg); + yield return Message.Initial.OverrideBy(msg); } else { diff --git a/src/Uno.Extensions.Reactive/Presentation/Bindings/BindableViewModelBase.cs b/src/Uno.Extensions.Reactive/Presentation/Bindings/BindableViewModelBase.cs index 163ce9321f..42b589b0c0 100644 --- a/src/Uno.Extensions.Reactive/Presentation/Bindings/BindableViewModelBase.cs +++ b/src/Uno.Extensions.Reactive/Presentation/Bindings/BindableViewModelBase.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.Linq; +using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -12,6 +14,7 @@ using Uno.Extensions.Reactive.Events; using Uno.Extensions.Reactive.Logging; using Uno.Extensions.Reactive.Utils; +using Uno.Extensions.Reactive.Utils.Logging; namespace Uno.Extensions.Reactive.Bindings; @@ -127,36 +130,48 @@ async void ViewModelToView(Action updated) { try { - var initialValue = stateImpl.Current.Current.Data.SomeOrDefault(GetDefaultValueForBindings()); + var defaultValue = GetDefaultValueForBindings(); + var value = stateImpl.Current.Current.Data.SomeOrDefault(defaultValue); // We run the update sync in setup, no matter the thread - updated(initialValue); + updated(value); + var ct = stateImpl.Context.Token; var source = FeedUIHelper.GetSource(stateImpl, stateImpl.Context); var dispatcher = await _dispatcher.GetFirstResolved(ct).ConfigureAwait(false); - - dispatcher.TryEnqueue(async () => + var updateScheduled = 0; + + // Note: We use for each here to deduplicate updates in case of fast updates of the source. + // This also ensure to not wait to for the UI thread before fetching MoveNext the source. + _ = source + .ForEachAsync(OnMessage, ct) + .ContinueWith( + enumeration => + { + this.Log().Error( + enumeration.Exception!, + $"Synchronization from ViewModel to View of '{propertyName}' failed." + + "(This is a final error, changes made in the VM are no longer propagated to the View.)"); + }, + TaskContinuationOptions.OnlyOnFaulted); + void OnMessage(Message msg) { - try + if (msg.Changes.Contains(MessageAxis.Data) && !(msg.Current.Get(BindingSource)?.Equals((this, propertyName)) ?? false)) { - // Note: No needs to use .WithCancellation() here as we are enumerating the stateImp which is going to be disposed anyway. - await foreach (var msg in source.WithCancellation(ct).ConfigureAwait(true)) + value = msg.Current.Data.SomeOrDefault(defaultValue); + if (Interlocked.CompareExchange(ref updateScheduled, 1, 0) is 0) { - if (msg.Changes.Contains(MessageAxis.Data) && !(msg.Current.Get(BindingSource)?.Equals((this, propertyName)) ?? false)) - { - updated(msg.Current.Data.SomeOrDefault(GetDefaultValueForBindings())); - _propertyChanged.Raise(new PropertyChangedEventArgs(propertyName)); - } + dispatcher.TryEnqueue(UpdateValue); } } - catch (Exception error) - { - this.Log().Error( - error, - $"Synchronization from ViewModel to View of '{propertyName}' failed." - + "(This is a final error, changes made in the VM are no longer propagated to the View.)"); - } - }); + } + + void UpdateValue() + { + updateScheduled = 0; + updated(value); + _propertyChanged.Raise(new PropertyChangedEventArgs(propertyName)); + } } catch (Exception error) { diff --git a/src/Uno.Extensions.Reactive/Utils/Logging/LogHelper.cs b/src/Uno.Extensions.Reactive/Utils/Logging/LogHelper.cs new file mode 100644 index 0000000000..cd1f3de627 --- /dev/null +++ b/src/Uno.Extensions.Reactive/Utils/Logging/LogHelper.cs @@ -0,0 +1,29 @@ +using System; +using System.Linq; +using System.Threading; +using Uno.Extensions.Reactive; + +namespace Uno.Extensions.Reactive.Utils.Logging; + +internal static class LogHelper +{ + public static string GetIdentifier(object obj) + => obj switch + { + null => "--null--", +#if DEBUG + _ => $"{GetTypeName(obj)}-{obj.GetHashCode():X8}", +#else + _ => obj?.ToString() +#endif + }; + + public static string GetTypeName(object obj) + => obj.GetType() switch + { + { IsGenericType: true } type => $"{type.Name}<{string.Join(", ", type.GenericTypeArguments.Select(GetTypeName))}>", + { IsArray: true } type => $"{GetTypeName(type.GetElementType()!)}[]", + { IsValueType: true } type => type.ToString(), + { } type => type.Name + }; +}