diff --git a/README.md b/README.md index bbaa33a0..27483f58 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,7 @@ Platform-specific packages provide DependencyProperty observation and other plat | `WhenChanging` | Observe property changes (before value changes, requires `INotifyPropertyChanging`) | | `WhenAnyValue` | ReactiveUI compatibility shim -- same semantics as `WhenChanged` | | `WhenAny` | Multi-property observation with selector | +| `WhenAnyDynamic` | Observe a chain given as an `Expression` built at run time (reflection, not AOT-safe) | | `WhenAnyObservable` | Observe and switch between observable properties | | `BindOneWay` | One-way binding from source to target | | `BindTwoWay` | Two-way binding between source and target | @@ -178,6 +179,28 @@ IObservable fullName = vm.WhenChanged( IObservable nameChanging = vm.WhenChanging(x => x.Name); ``` +### Observing a Chain Named at Run Time + +`WhenAnyDynamic` takes the chain as a `System.Linq.Expressions.Expression` the caller built, rather than as a +lambda the compiler could read. Arities 1 to 12 are available, each with and without the distinct gate. + +```csharp +// The chain is a value, so it can be assembled from a property name, a configuration entry, or a caller. +Expression chain = ((Expression>)(x => x.Address.City)).Body; + +IObservable cityObs = vm.WhenAnyDynamic(chain, static c => (string?)c.Value); + +IObservable fullName = vm.WhenAnyDynamic( + firstNameChain, + lastNameChain, + static (first, last) => $"{first.Value} {last.Value}"); +``` + +There is nothing for a generator to resolve in an expression built at run time, so the chain is walked by +reflection. Every overload carries `[RequiresUnreferencedCode]`, which makes this the one part of the observation +surface that is not trimming- or AOT-safe and reports each call site in a `PublishAot` build. Where the chain is +known at compile time, `WhenChanged` and `WhenAny` observe the same thing with no reflection. + ### One-Way Binding ```csharp @@ -413,6 +436,36 @@ subscription rather than re-projecting the two observed sides: | Two-Way Binding (.NET 10.0) | 8.1x faster | 8.5x less | | First Binding (.NET 10.0) | 4.1x faster | 9.6x less | +#### Chains Named at Run Time (WhenAnyDynamic) + +| Method | Runtime | Mean | Allocated | +|-------------------|-----------|-------:|----------:| +| Single Chain | .NET 10.0 | 278 us | 102.7 KB | +| Two Chains | .NET 10.0 | 595 us | 190.2 KB | +| Deep Chain | .NET 10.0 | 354 us | 103.3 KB | +| First Observation | .NET 10.0 | 7.3 us | 1.1 KB | +| Single Chain | .NET 8.0 | 347 us | 102.7 KB | +| Two Chains | .NET 8.0 | 691 us | 190.1 KB | +| Deep Chain | .NET 8.0 | 419 us | 103.3 KB | +| First Observation | .NET 8.0 | 7.9 us | 1.1 KB | + +This is the one part of the surface where the two engines are level. Both walk the chain by reflection and +allocate the same objects doing it, so the numbers land on top of each other; the gain is in resolving the chain +at compile time instead: + +| Single chain, .NET 10.0 | Mean | Allocated | +|-----------------------------|-------:|----------:| +| Generated (`WhenChanged`) | 175 us | 63.4 KB | +| `WhenAnyDynamic` | 278 us | 102.7 KB | +| ReactiveUI `WhenAnyDynamic` | 299 us | 102.6 KB | + +Most of the gap to the generated path is the `IObservedChange` handed to the selector - 40 bytes per +notification, which the signature has to produce, against a generated observation that emits the value itself. + +Two chains fire twice the notifications of one, so their 190 KB is 95 bytes per notification against a single +chain's 103: combining a higher arity costs one subscription, not a per-notification charge. Every arity observes +each of its chains through the same walk and differs only in how many it combines. + ## Diagnostics The separate analyzer package reports the following diagnostics: diff --git a/src/ReactiveUI.Binding.Shared/Mixins/ReactiveUIBindingExtensions.WhenAnyDynamic.WideArity.cs b/src/ReactiveUI.Binding.Shared/Mixins/ReactiveUIBindingExtensions.WhenAnyDynamic.WideArity.cs new file mode 100644 index 00000000..727fd8b5 --- /dev/null +++ b/src/ReactiveUI.Binding.Shared/Mixins/ReactiveUIBindingExtensions.WhenAnyDynamic.WideArity.cs @@ -0,0 +1,950 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Diagnostics.CodeAnalysis; + +#if REACTIVE_SHIM +namespace ReactiveUI.Binding.Reactive; +#else +namespace ReactiveUI.Binding; +#endif + +/// Observes property chains that are only known as expressions at run time. +/// +/// Every other observation in this library is resolved at compile time, which is what keeps it free of +/// reflection. These overloads take an the caller built rather than a lambda the +/// compiler could read, so there is nothing for a generator to resolve and the chain has to be walked by +/// reflection. They are the one part of the observation surface that is not trim- or AOT-safe, and they +/// say so. +/// +/// The walking itself is the engine the runtime observation fallback already uses, so resolving a link, +/// re-subscribing a deeper one when an intermediate moves, and filtering duplicates are not written twice. +/// Each arity differs only in how many chains it combines. +/// +/// +public static partial class ReactiveUIBindingExtensions +{ + /// Why every overload here is unsafe to trim. + private const string DynamicChainRequiresUnreferencedCode = + "Evaluates expression-based member chains via reflection; members may be trimmed."; + + /// Observes 1 dynamically-typed property chain and projects it with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// Projects the observed changes into a result. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Func, TRet> selector) + where TSender : class + => sender.WhenAnyDynamic(property1, selector, true); + + /// Observes 1 dynamically-typed property chain and projects it with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// Projects the observed changes into a result. + /// Whether a chain reports only when its value changes. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Func, TRet> selector, + bool isDistinct) + where TSender : class + { + var chains = Chains(sender, selector, isDistinct, property1); + return chains[0].Select(selector); + } + + /// Observes 2 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// Projects the observed changes into a result. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Func, IObservedChange, TRet> selector) + where TSender : class + => sender.WhenAnyDynamic(property1, property2, selector, true); + + /// Observes 2 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// Projects the observed changes into a result. + /// Whether a chain reports only when its value changes. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Func, IObservedChange, TRet> selector, + bool isDistinct) + where TSender : class + { + var chains = Chains(sender, selector, isDistinct, property1, property2); + return CombineLatestObservable.Create(chains[0], chains[1], selector); + } + + /// Observes 3 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// Projects the observed changes into a result. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Func, IObservedChange, IObservedChange, TRet> selector) + where TSender : class + => sender.WhenAnyDynamic(property1, property2, property3, selector, true); + + /// Observes 3 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// Projects the observed changes into a result. + /// Whether a chain reports only when its value changes. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Func, IObservedChange, IObservedChange, TRet> selector, + bool isDistinct) + where TSender : class + { + var chains = Chains(sender, selector, isDistinct, property1, property2, property3); + return CombineLatestObservable.Create(chains[0], chains[1], chains[2], selector); + } + + /// Observes 4 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// Projects the observed changes into a result. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector) + where TSender : class + => sender.WhenAnyDynamic(property1, property2, property3, property4, selector, true); + + /// Observes 4 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// Projects the observed changes into a result. + /// Whether a chain reports only when its value changes. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector, + bool isDistinct) + where TSender : class + { + var chains = Chains(sender, selector, isDistinct, property1, property2, property3, property4); + return CombineLatestObservable.Create(chains[0], chains[1], chains[2], chains[3], selector); + } + + /// Observes 5 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// Projects the observed changes into a result. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector) + where TSender : class + => sender.WhenAnyDynamic(property1, property2, property3, property4, property5, selector, true); + + /// Observes 5 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// Projects the observed changes into a result. + /// Whether a chain reports only when its value changes. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector, + bool isDistinct) + where TSender : class + { + var chains = Chains(sender, selector, isDistinct, property1, property2, property3, property4, property5); + return CombineLatestObservable.Create(chains[0], chains[1], chains[2], chains[3], chains[4], selector); + } + + /// Observes 6 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// Projects the observed changes into a result. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector) + where TSender : class + => sender.WhenAnyDynamic(property1, property2, property3, property4, property5, property6, selector, true); + + /// Observes 6 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// Projects the observed changes into a result. + /// Whether a chain reports only when its value changes. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector, + bool isDistinct) + where TSender : class + { + var chains = Chains(sender, selector, isDistinct, property1, property2, property3, property4, property5, property6); + return CombineLatestObservable.Create(chains[0], chains[1], chains[2], chains[3], chains[4], chains[5], selector); + } + + /// Observes 7 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// An expression naming property 7. + /// Projects the observed changes into a result. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector) + where TSender : class + => sender.WhenAnyDynamic(property1, property2, property3, property4, property5, property6, property7, selector, true); + + /// Observes 7 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// An expression naming property 7. + /// Projects the observed changes into a result. + /// Whether a chain reports only when its value changes. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector, + bool isDistinct) + where TSender : class + { + var chains = Chains(sender, selector, isDistinct, property1, property2, property3, property4, property5, property6, property7); + return CombineLatestObservable.Create(chains[0], chains[1], chains[2], chains[3], chains[4], chains[5], chains[6], selector); + } + + /// Observes 8 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// An expression naming property 7. + /// An expression naming property 8. + /// Projects the observed changes into a result. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector) + where TSender : class + => sender.WhenAnyDynamic(property1, property2, property3, property4, property5, property6, property7, property8, selector, true); + + /// Observes 8 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// An expression naming property 7. + /// An expression naming property 8. + /// Projects the observed changes into a result. + /// Whether a chain reports only when its value changes. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector, + bool isDistinct) + where TSender : class + { + var chains = Chains(sender, selector, isDistinct, property1, property2, property3, property4, property5, property6, property7, property8); + return CombineLatestObservable.Create(chains[0], chains[1], chains[2], chains[3], chains[4], chains[5], chains[6], chains[7], selector); + } + + /// Observes 9 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// An expression naming property 7. + /// An expression naming property 8. + /// An expression naming property 9. + /// Projects the observed changes into a result. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Expression? property9, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector) + where TSender : class + => sender.WhenAnyDynamic(property1, property2, property3, property4, property5, property6, property7, property8, property9, selector, true); + + /// Observes 9 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// An expression naming property 7. + /// An expression naming property 8. + /// An expression naming property 9. + /// Projects the observed changes into a result. + /// Whether a chain reports only when its value changes. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Expression? property9, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector, + bool isDistinct) + where TSender : class + { + var chains = Chains(sender, selector, isDistinct, property1, property2, property3, property4, property5, property6, property7, property8, property9); + return CombineLatestObservable.Create(chains[0], chains[1], chains[2], chains[3], chains[4], chains[5], chains[6], chains[7], chains[8], selector); + } + + /// Observes 10 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// An expression naming property 7. + /// An expression naming property 8. + /// An expression naming property 9. + /// An expression naming property 10. + /// Projects the observed changes into a result. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Expression? property9, + Expression? property10, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector) + where TSender : class + => sender.WhenAnyDynamic(property1, property2, property3, property4, property5, property6, property7, property8, property9, property10, selector, true); + + /// Observes 10 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// An expression naming property 7. + /// An expression naming property 8. + /// An expression naming property 9. + /// An expression naming property 10. + /// Projects the observed changes into a result. + /// Whether a chain reports only when its value changes. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Expression? property9, + Expression? property10, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector, + bool isDistinct) + where TSender : class + { + var chains = Chains(sender, selector, isDistinct, property1, property2, property3, property4, property5, property6, property7, property8, property9, property10); + return CombineLatestObservable.Create(chains[0], chains[1], chains[2], chains[3], chains[4], chains[5], chains[6], chains[7], chains[8], chains[9], selector); + } + + /// Observes 11 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// An expression naming property 7. + /// An expression naming property 8. + /// An expression naming property 9. + /// An expression naming property 10. + /// An expression naming property 11. + /// Projects the observed changes into a result. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Expression? property9, + Expression? property10, + Expression? property11, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector) + where TSender : class + => sender.WhenAnyDynamic(property1, property2, property3, property4, property5, property6, property7, property8, property9, property10, property11, selector, true); + + /// Observes 11 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// An expression naming property 7. + /// An expression naming property 8. + /// An expression naming property 9. + /// An expression naming property 10. + /// An expression naming property 11. + /// Projects the observed changes into a result. + /// Whether a chain reports only when its value changes. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Expression? property9, + Expression? property10, + Expression? property11, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector, + bool isDistinct) + where TSender : class + { + var chains = Chains(sender, selector, isDistinct, property1, property2, property3, property4, property5, property6, property7, property8, property9, property10, property11); + return CombineLatestObservable.Create(chains[0], chains[1], chains[2], chains[3], chains[4], chains[5], chains[6], chains[7], chains[8], chains[9], chains[10], selector); + } + + /// Observes 12 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// An expression naming property 7. + /// An expression naming property 8. + /// An expression naming property 9. + /// An expression naming property 10. + /// An expression naming property 11. + /// An expression naming property 12. + /// Projects the observed changes into a result. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Expression? property9, + Expression? property10, + Expression? property11, + Expression? property12, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector) + where TSender : class + => sender.WhenAnyDynamic(property1, property2, property3, property4, property5, property6, property7, property8, property9, property10, property11, property12, selector, true); + + /// Observes 12 dynamically-typed property chains and combines them with a selector. + /// The type of the object the chains are rooted on. + /// The type of the projected result. + /// The object the chains are rooted on. + /// An expression naming property 1. + /// An expression naming property 2. + /// An expression naming property 3. + /// An expression naming property 4. + /// An expression naming property 5. + /// An expression naming property 6. + /// An expression naming property 7. + /// An expression naming property 8. + /// An expression naming property 9. + /// An expression naming property 10. + /// An expression naming property 11. + /// An expression naming property 12. + /// Projects the observed changes into a result. + /// Whether a chain reports only when its value changes. + /// An observable of the projected result. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [SuppressMessage("Design", "SST1472", Justification = "one expression per observed chain; the parameter count is the shape of this overload")] + public static IObservable WhenAnyDynamic( + this TSender sender, + Expression? property1, + Expression? property2, + Expression? property3, + Expression? property4, + Expression? property5, + Expression? property6, + Expression? property7, + Expression? property8, + Expression? property9, + Expression? property10, + Expression? property11, + Expression? property12, + Func< + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + IObservedChange, + TRet> selector, + bool isDistinct) + where TSender : class + { + var chains = Chains(sender, selector, isDistinct, property1, property2, property3, property4, property5, property6, property7, property8, property9, property10, property11, property12); + return CombineLatestObservable.Create(chains[0], chains[1], chains[2], chains[3], chains[4], chains[5], chains[6], chains[7], chains[8], chains[9], chains[10], chains[11], selector); + } + + /// Subscribes to every chain an overload was handed, after refusing a missing argument. + /// The type of the object the chains are rooted on. + /// The object the chains are rooted on. + /// The projection the caller supplied, which is required. + /// Whether a chain reports only when its value changes. + /// The expressions naming the chains. + /// One observation per chain, in the order the chains were given. + /// + /// Every arity funnels its argument checks and its subscriptions through here, so each overload is the + /// signature and nothing else. The array is built once when the observable is created rather than per + /// notification, so a higher arity costs one allocation at subscription and nothing while it reports. + /// + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + private static IObservable>[] Chains( + TSender sender, + object selector, + bool isDistinct, + params Expression?[] properties) + where TSender : class + { + ArgumentExceptionHelper.ThrowIfNull(sender); + ArgumentExceptionHelper.ThrowIfNull(selector); + + var chains = new IObservable>[properties.Length]; + for (var i = 0; i < properties.Length; i++) + { + chains[i] = ObserveDynamicChain(sender, properties[i], isDistinct); + } + + return chains; + } + + /// Subscribes to one property chain named by a run-time expression. + /// The type of the object the chain is rooted on. + /// The object the chain is rooted on. + /// An expression naming the chain. + /// Whether the chain reports only when its value changes. + /// An observable of the chain's observed changes. + /// Every arity funnels through here, so the reflection is written once. + [RequiresUnreferencedCode(DynamicChainRequiresUnreferencedCode)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static IObservable> ObserveDynamicChain( + TSender sender, + Expression? property, + bool isDistinct) + where TSender : class => + sender.SubscribeToExpressionChain(property, false, false, isDistinct); +} diff --git a/src/ReactiveUI.Binding.Shared/Mixins/ReactiveUIBindingExtensions.cs b/src/ReactiveUI.Binding.Shared/Mixins/ReactiveUIBindingExtensions.cs index d1506595..6a3fc3bc 100644 --- a/src/ReactiveUI.Binding.Shared/Mixins/ReactiveUIBindingExtensions.cs +++ b/src/ReactiveUI.Binding.Shared/Mixins/ReactiveUIBindingExtensions.cs @@ -15,7 +15,6 @@ namespace ReactiveUI.Binding; /// over these generic stubs due to having fewer optional parameters (or matching type specificity). /// Supports 1-16 property selectors for observation APIs (WhenChanged, WhenChanging, WhenAnyValue). /// -[ExcludeFromCodeCoverage] public static partial class ReactiveUIBindingExtensions { /// diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/RxUiDynamicChainBaseline.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/RxUiDynamicChainBaseline.cs new file mode 100644 index 00000000..a68901d9 --- /dev/null +++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/RxUiDynamicChainBaseline.cs @@ -0,0 +1,143 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +#if NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif +using System.Linq.Expressions; +using System.Runtime.CompilerServices; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Jobs; +using ReactiveUI; +using ReactiveUI.Builder; +using BenchmarkVm = ReactiveUI.Binding.Benchmarks.BenchmarkViewModel; + +namespace RxUiDynamicChain; + +/// +/// The same dynamic-chain scenarios as WhenAnyDynamicBenchmark, run against ReactiveUI's own engine so +/// the two are read side by side. +/// +/// +/// Declared outside the ReactiveUI.Binding namespace on purpose. Extension lookup walks the enclosing +/// namespaces from the inside out and stops at the first level offering a candidate, so a class nested under +/// ReactiveUI.Binding reaches this library's overloads and never consults ReactiveUI's. The view model +/// arrives through a type alias rather than an import for the same reason: importing its namespace would put +/// both libraries' overloads at the outermost level and make every call ambiguous. +/// +[SimpleJob(RuntimeMoniker.Net80)] +[SimpleJob(RuntimeMoniker.Net10_0)] +[MemoryDiagnoser] +[EventPipeProfiler(EventPipeProfile.GcVerbose)] +[MarkdownExporterAttribute.GitHub] +#if NET8_0_OR_GREATER +[RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] +#endif +public class RxUiDynamicChainBaseline +{ + /// Represents the number of property change events to be triggered during the benchmark tests. + private const int PropertyChangeCount = 1_000; + + /// Names the chain reaching one property. + private static readonly Expression NameChain = Chain(x => x.Name); + + /// Names the chain reaching a second property, so an arity above one has something to combine. + private static readonly Expression AgeChain = Chain(x => x.Age); + + /// Names the chain reaching through an intermediate. + private static readonly Expression ChildValueChain = Chain(x => x.Child.Value); + + /// Reads one observed change. + /// + /// Typed rather than inferred so the binding is pinned: the parameter names ReactiveUI's + /// , so a call that resolved to this library's overload of + /// the same name would not compile rather than quietly benchmarking the wrong engine. + /// + private static readonly Func, object?> ReadOne = + static c1 => c1.Value; + + /// Reads whichever of two observed changes carries a value. + private static readonly Func, IObservedChange, object?> ReadEither = + static (c1, c2) => c1.Value ?? c2.Value; + + /// The view model instance used for observation benchmarks. + private BenchmarkVm _vm = null!; + + /// Registers the observation plugins ReactiveUI resolves each link through. + [GlobalSetup] + public void Register() + { + var builder = RxAppBuilder.CreateReactiveUIBuilder(); + _ = builder.WithCoreServices(); + _ = builder.BuildApp(); + } + + /// Sets up a fresh view model before each benchmark iteration. + [IterationSetup] + public void Setup() => + _vm = new() { Name = "Initial", Age = 0, Child = new() { Value = "ChildInitial" } }; + + /// One chain: subscribe, fire N changes, dispose. + [Benchmark(Description = "Single Chain")] + public void SingleChain() + { + object? last = null; + using var sub = _vm.WhenAnyDynamic(NameChain, ReadOne) + .Subscribe(v => last = v); + + for (var i = 0; i < PropertyChangeCount; i++) + { + _vm.Name = $"Name_{i}"; + } + } + + /// Two chains combined: subscribe, fire N changes on each, dispose. + [Benchmark(Description = "Two Chains")] + public void TwoChains() + { + object? last = null; + using var sub = _vm.WhenAnyDynamic(NameChain, AgeChain, ReadEither) + .Subscribe(v => last = v); + + for (var i = 0; i < PropertyChangeCount; i++) + { + _vm.Name = $"Name_{i}"; + _vm.Age = i; + } + } + + /// A chain through an intermediate: subscribe, fire N changes on the leaf, dispose. + [Benchmark(Description = "Deep Chain")] + public void DeepChain() + { + object? last = null; + using var sub = _vm.WhenAnyDynamic(ChildValueChain, ReadOne) + .Subscribe(v => last = v); + + for (var i = 0; i < PropertyChangeCount; i++) + { + _vm.Child.Value = $"Value_{i}"; + } + } + + /// Cold start: subscribe, read the initial value, dispose. No property changes. + /// The observed value. + [Benchmark(Description = "First Observation")] + public object? FirstObservation() + { + object? result = null; + using var sub = _vm.WhenAnyDynamic(NameChain, ReadOne) + .Subscribe(v => result = v); + return result; + } + + /// Names a property chain the way a caller building one at run time hands it over. + /// The type the chain ends at. + /// The chain to name. + /// The expression body, which is what these overloads take. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Expression Chain(Expression> property) => + property.Body; +} diff --git a/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyDynamicBenchmark.cs b/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyDynamicBenchmark.cs new file mode 100644 index 00000000..7a828f5b --- /dev/null +++ b/src/benchmarks/ReactiveUI.Binding.Benchmarks/WhenAnyDynamicBenchmark.cs @@ -0,0 +1,141 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +#if NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif +using System.Linq.Expressions; +using System.Runtime.CompilerServices; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Jobs; +using ReactiveUI.Binding.Builder; + +namespace ReactiveUI.Binding.Benchmarks; + +/// +/// Reflection-walked observation benchmarks, and what the generated observation of the same chain costs. +/// The pair is the choice a caller actually makes: a chain named by an expression built at run time cannot +/// be resolved at compile time, and this says what that buys and what it costs. +/// +/// +/// No NativeAOT job: these overloads walk members by reflection and say so with +/// RequiresUnreferencedCode, so an ahead-of-time published run is not a +/// configuration they support. +/// +[SimpleJob(RuntimeMoniker.Net80)] +[SimpleJob(RuntimeMoniker.Net10_0)] +[MemoryDiagnoser] +[EventPipeProfiler(EventPipeProfile.GcVerbose)] +[MarkdownExporterAttribute.GitHub] +#if NET8_0_OR_GREATER +[RequiresUnreferencedCode("Evaluates expression-based member chains via reflection; members may be trimmed.")] +#endif +public class WhenAnyDynamicBenchmark +{ + /// Represents the number of property change events to be triggered during the benchmark tests. + private const int PropertyChangeCount = 1_000; + + /// Names the chain reaching one property. + private static readonly Expression NameChain = Chain(x => x.Name); + + /// Names the chain reaching a second property, so an arity above one has something to combine. + private static readonly Expression AgeChain = Chain(x => x.Age); + + /// Names the chain reaching through an intermediate. + private static readonly Expression ChildValueChain = Chain(x => x.Child.Value); + + /// The view model instance used for observation benchmarks. + private BenchmarkViewModel _vm = null!; + + /// Registers the observation plugins the reflection walk resolves each link through. + [GlobalSetup] + public void Register() + { + var builder = RxBindingBuilder.CreateReactiveUIBindingBuilder(); + _ = builder.WithCoreServices(); + _ = builder.BuildApp(); + } + + /// Sets up a fresh view model before each benchmark iteration. + [IterationSetup] + public void Setup() => + _vm = new() { Name = "Initial", Age = 0, Child = new() { Value = "ChildInitial" } }; + + /// One chain: subscribe, fire N changes, dispose. + [Benchmark(Description = "Single Chain")] + public void SingleChain() + { + object? last = null; + using var sub = _vm.WhenAnyDynamic(NameChain, static c1 => c1.Value) + .Subscribe(v => last = v); + + for (var i = 0; i < PropertyChangeCount; i++) + { + _vm.Name = $"Name_{i}"; + } + } + + /// Two chains combined: subscribe, fire N changes on each, dispose. + [Benchmark(Description = "Two Chains")] + public void TwoChains() + { + object? last = null; + using var sub = _vm.WhenAnyDynamic(NameChain, AgeChain, static (c1, c2) => c1.Value ?? c2.Value) + .Subscribe(v => last = v); + + for (var i = 0; i < PropertyChangeCount; i++) + { + _vm.Name = $"Name_{i}"; + _vm.Age = i; + } + } + + /// A chain through an intermediate: subscribe, fire N changes on the leaf, dispose. + [Benchmark(Description = "Deep Chain")] + public void DeepChain() + { + object? last = null; + using var sub = _vm.WhenAnyDynamic(ChildValueChain, static c1 => c1.Value) + .Subscribe(v => last = v); + + for (var i = 0; i < PropertyChangeCount; i++) + { + _vm.Child.Value = $"Value_{i}"; + } + } + + /// Cold start: subscribe, read the initial value, dispose. No property changes. + /// The observed value. + [Benchmark(Description = "First Observation")] + public object? FirstObservation() + { + object? result = null; + using var sub = _vm.WhenAnyDynamic(NameChain, static c1 => c1.Value) + .Subscribe(v => result = v); + return result; + } + + /// The same single chain resolved at compile time, which is what the reflection walk is weighed against. + [Benchmark(Description = "Single Chain (generated)", Baseline = true)] + public void SingleChainGenerated() + { + var last = string.Empty; + using var sub = _vm.WhenChanged(x => x.Name) + .Subscribe(v => last = v); + + for (var i = 0; i < PropertyChangeCount; i++) + { + _vm.Name = $"Name_{i}"; + } + } + + /// Names a property chain the way a caller building one at run time hands it over. + /// The type the chain ends at. + /// The chain to name. + /// The expression body, which is what these overloads take. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Expression Chain(Expression> property) => + property.Body; +} diff --git a/src/tests/ReactiveUI.Binding.Tests/Mixins/BindingDispatchStubTests.cs b/src/tests/ReactiveUI.Binding.Tests/Mixins/BindingDispatchStubTests.cs new file mode 100644 index 00000000..339e51ab --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/Mixins/BindingDispatchStubTests.cs @@ -0,0 +1,221 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using ReactiveUI.Binding.Tests.TestModels; + +namespace ReactiveUI.Binding.Tests.Mixins; + +/// Covers the binding overloads that exist only to be replaced by a generated one. +/// +/// Unlike the observation surface, a binding has no runtime fallback: resolving one needs the two lambdas at +/// compile time. Each overload here therefore refuses the call rather than binding something. They are reached +/// on the declaring class rather than as extension methods, because written as an extension call the generated +/// dispatch wins overload resolution and the refusal never happens. +/// +public class BindingDispatchStubTests +{ + /// The source of a binding. + private readonly DispatchStubViewModel _viewModel = new(); + + /// The target of a binding. + private readonly DispatchStubView _view = new(); + + /// The stream a BindTo call writes from. + private readonly ManualObservable _source = new(); + + /// A one-way binding with no generated overload refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindOneWay_WithNoGeneratedOverload_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindOneWay( + _viewModel, + _view, + x => x.Caption, + x => x.Caption)) + .ThrowsExactly(); + + /// A converting one-way binding with no generated overload refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindOneWay_ConvertingWithNoGeneratedOverload_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindOneWay( + _viewModel, + _view, + x => x.Caption, + x => x.Caption, + static value => value)) + .ThrowsExactly(); + + /// A two-way binding with no generated overload refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindTwoWay_WithNoGeneratedOverload_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindTwoWay( + _viewModel, + _view, + x => x.Caption, + x => x.Caption)) + .ThrowsExactly(); + + /// A converting two-way binding with no generated overload refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindTwoWay_ConvertingWithNoGeneratedOverload_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindTwoWay( + _viewModel, + _view, + x => x.Caption, + x => x.Caption, + static value => value, + static value => value)) + .ThrowsExactly(); + + /// The view-first spelling of a one-way binding refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task OneWayBind_WithNoGeneratedOverload_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.OneWayBind( + _view, + _viewModel, + x => x.Caption, + x => x.Caption)) + .ThrowsExactly(); + + /// The converting view-first spelling of a one-way binding refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task OneWayBind_ConvertingWithNoGeneratedOverload_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.OneWayBind( + _view, + _viewModel, + x => x.Caption, + x => x.Caption, + static value => value)) + .ThrowsExactly(); + + /// The view-first spelling of a two-way binding refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task Bind_WithNoGeneratedOverload_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.Bind( + _view, + _viewModel, + x => x.Caption, + x => x.Caption)) + .ThrowsExactly(); + + /// The converting view-first spelling of a two-way binding refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task Bind_ConvertingWithNoGeneratedOverload_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.Bind( + _view, + _viewModel, + x => x.Caption, + x => x.Caption, + static value => value, + static value => value)) + .ThrowsExactly(); + + /// Writing a stream into a property with no generated overload refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindTo_WithNoGeneratedOverload_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindTo( + _source, + _view, + x => x.Caption)) + .ThrowsExactly(); + + /// Writing a stream into a property with a conversion hint refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindTo_WithAConversionHint_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindTo( + _source, + _view, + x => x.Caption, + conversionHint: null)) + .ThrowsExactly(); + + /// Writing a stream into a property with a converter refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindTo_WithAConverter_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindTo( + _source, + _view, + x => x.Caption, + converterOverride: null)) + .ThrowsExactly(); + + /// Writing a stream into a property with both a hint and a converter refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindTo_WithAConversionHintAndAConverter_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindTo( + _source, + _view, + x => x.Caption, + conversionHint: null, + converterOverride: null)) + .ThrowsExactly(); + + /// A command binding with no generated overload refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindCommand_WithNoGeneratedOverload_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindCommand( + _view, + _viewModel, + x => x.Run, + x => x.Control)) + .ThrowsExactly(); + + /// A command binding taking its parameter from a stream refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindCommand_WithAParameterStream_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindCommand( + _view, + _viewModel, + x => x.Run, + x => x.Control, + _source)) + .ThrowsExactly(); + + /// A command binding taking its parameter from a property refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindCommand_WithAParameterProperty_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindCommand( + _view, + _viewModel, + x => x.Run, + x => x.Control, + x => x.Parameter)) + .ThrowsExactly(); + + /// An interaction binding handled asynchronously refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindInteraction_WithAnAsynchronousHandler_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindInteraction( + _view, + _viewModel, + x => x.Confirm, + static context => Task.CompletedTask)) + .ThrowsExactly(); + + /// An interaction binding handled by a stream refuses the call. + /// A task representing the asynchronous test operation. + [Test] + public async Task BindInteraction_WithAStreamHandler_ThrowsInvalidOperationException() => + await Assert.That(() => ReactiveUIBindingExtensions.BindInteraction( + _view, + _viewModel, + x => x.Confirm, + static IObservable (context) => new ManualObservable())) + .ThrowsExactly(); +} diff --git a/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyDynamicTests.cs b/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyDynamicTests.cs new file mode 100644 index 00000000..fe1c15f0 --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyDynamicTests.cs @@ -0,0 +1,983 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Linq.Expressions; +using System.Runtime.CompilerServices; +using ReactiveUI.Binding.Tests.TestModels; +using ReactiveUI.Binding.Tests.WhenAny; + +namespace ReactiveUI.Binding.Tests.Mixins; + +/// +/// Tests for the WhenAnyDynamic overloads, which observe property chains named by an expression the +/// caller built rather than by a lambda the compiler could read. +/// +public class WhenAnyDynamicTests +{ + /// The value every observed property starts out holding. + private const string InitialValue = "a"; + + /// The value the last observed property is moved to. + private const string MovedValue = "b"; + + /// How many chains the one-chain overloads observe. + private const int OneChain = 1; + + /// How many chains the two-chain overloads observe. + private const int TwoChains = 2; + + /// How many chains the three-chain overloads observe. + private const int ThreeChains = 3; + + /// How many chains the four-chain overloads observe. + private const int FourChains = 4; + + /// How many chains the five-chain overloads observe. + private const int FiveChains = 5; + + /// How many chains the six-chain overloads observe. + private const int SixChains = 6; + + /// How many chains the seven-chain overloads observe. + private const int SevenChains = 7; + + /// How many chains the eight-chain overloads observe. + private const int EightChains = 8; + + /// How many chains the nine-chain overloads observe. + private const int NineChains = 9; + + /// How many chains the ten-chain overloads observe. + private const int TenChains = 10; + + /// How many chains the eleven-chain overloads observe. + private const int ElevenChains = 11; + + /// How many chains the twelve-chain overloads observe. + private const int TwelveChains = 12; + + /// What a chain reports with the distinct gate on: the seed, then the move. + private static readonly string[] SeedThenMove = [InitialValue, MovedValue]; + + /// What a chain reports with the gate off: the seed, the move, and the notification repeating it. + private static readonly string[] SeedThenMoveTwice = [InitialValue, MovedValue, MovedValue]; + + /// Observing 1 chain, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity1_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + static c1 => string.Concat(c1.Value)) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(OneChain)); + + fixture.P1 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(OneChain)); + } + + /// Observing 1 chain with the distinct gate named, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity1WithDistinctSpecified_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + static c1 => string.Concat(c1.Value), + true) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(OneChain)); + + fixture.P1 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(OneChain)); + } + + /// Observing 2 chains, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity2_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + static (c1, c2) => string.Concat(c1.Value, c2.Value)) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(TwoChains)); + + fixture.P2 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(TwoChains)); + } + + /// Observing 2 chains with the distinct gate named, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity2WithDistinctSpecified_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + static (c1, c2) => string.Concat(c1.Value, c2.Value), + true) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(TwoChains)); + + fixture.P2 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(TwoChains)); + } + + /// Observing 3 chains, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity3_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + static (c1, c2, c3) => string.Concat(c1.Value, c2.Value, c3.Value)) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(ThreeChains)); + + fixture.P3 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(ThreeChains)); + } + + /// Observing 3 chains with the distinct gate named, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity3WithDistinctSpecified_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + static (c1, c2, c3) => string.Concat(c1.Value, c2.Value, c3.Value), + true) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(ThreeChains)); + + fixture.P3 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(ThreeChains)); + } + + /// Observing 4 chains, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity4_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + static (c1, c2, c3, c4) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value)) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(FourChains)); + + fixture.P4 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(FourChains)); + } + + /// Observing 4 chains with the distinct gate named, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity4WithDistinctSpecified_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + static (c1, c2, c3, c4) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value), + true) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(FourChains)); + + fixture.P4 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(FourChains)); + } + + /// Observing 5 chains, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity5_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + static (c1, c2, c3, c4, c5) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value)) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(FiveChains)); + + fixture.P5 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(FiveChains)); + } + + /// Observing 5 chains with the distinct gate named, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity5WithDistinctSpecified_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + static (c1, c2, c3, c4, c5) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value), + true) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(FiveChains)); + + fixture.P5 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(FiveChains)); + } + + /// Observing 6 chains, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity6_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + static (c1, c2, c3, c4, c5, c6) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value)) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(SixChains)); + + fixture.P6 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(SixChains)); + } + + /// Observing 6 chains with the distinct gate named, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity6WithDistinctSpecified_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + static (c1, c2, c3, c4, c5, c6) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value), + true) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(SixChains)); + + fixture.P6 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(SixChains)); + } + + /// Observing 7 chains, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity7_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + Body(x => x.P7), + static (c1, c2, c3, c4, c5, c6, c7) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value, + c7.Value)) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(SevenChains)); + + fixture.P7 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(SevenChains)); + } + + /// Observing 7 chains with the distinct gate named, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity7WithDistinctSpecified_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + Body(x => x.P7), + static (c1, c2, c3, c4, c5, c6, c7) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value, + c7.Value), + true) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(SevenChains)); + + fixture.P7 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(SevenChains)); + } + + /// Observing 8 chains, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity8_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + Body(x => x.P7), + Body(x => x.P8), + static (c1, c2, c3, c4, c5, c6, c7, c8) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value, + c7.Value, + c8.Value)) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(EightChains)); + + fixture.P8 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(EightChains)); + } + + /// Observing 8 chains with the distinct gate named, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity8WithDistinctSpecified_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + Body(x => x.P7), + Body(x => x.P8), + static (c1, c2, c3, c4, c5, c6, c7, c8) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value, + c7.Value, + c8.Value), + true) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(EightChains)); + + fixture.P8 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(EightChains)); + } + + /// Observing 9 chains, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity9_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + Body(x => x.P7), + Body(x => x.P8), + Body(x => x.P9), + static (c1, c2, c3, c4, c5, c6, c7, c8, c9) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value, + c7.Value, + c8.Value, + c9.Value)) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(NineChains)); + + fixture.P9 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(NineChains)); + } + + /// Observing 9 chains with the distinct gate named, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity9WithDistinctSpecified_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + Body(x => x.P7), + Body(x => x.P8), + Body(x => x.P9), + static (c1, c2, c3, c4, c5, c6, c7, c8, c9) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value, + c7.Value, + c8.Value, + c9.Value), + true) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(NineChains)); + + fixture.P9 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(NineChains)); + } + + /// Observing 10 chains, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity10_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + Body(x => x.P7), + Body(x => x.P8), + Body(x => x.P9), + Body(x => x.P10), + static (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value, + c7.Value, + c8.Value, + c9.Value, + c10.Value)) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(TenChains)); + + fixture.P10 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(TenChains)); + } + + /// Observing 10 chains with the distinct gate named, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity10WithDistinctSpecified_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + Body(x => x.P7), + Body(x => x.P8), + Body(x => x.P9), + Body(x => x.P10), + static (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value, + c7.Value, + c8.Value, + c9.Value, + c10.Value), + true) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(TenChains)); + + fixture.P10 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(TenChains)); + } + + /// Observing 11 chains, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity11_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + Body(x => x.P7), + Body(x => x.P8), + Body(x => x.P9), + Body(x => x.P10), + Body(x => x.P11), + static (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value, + c7.Value, + c8.Value, + c9.Value, + c10.Value, + c11.Value)) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(ElevenChains)); + + fixture.P11 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(ElevenChains)); + } + + /// Observing 11 chains with the distinct gate named, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity11WithDistinctSpecified_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + Body(x => x.P7), + Body(x => x.P8), + Body(x => x.P9), + Body(x => x.P10), + Body(x => x.P11), + static (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value, + c7.Value, + c8.Value, + c9.Value, + c10.Value, + c11.Value), + true) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(ElevenChains)); + + fixture.P11 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(ElevenChains)); + } + + /// Observing 12 chains, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity12_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + Body(x => x.P7), + Body(x => x.P8), + Body(x => x.P9), + Body(x => x.P10), + Body(x => x.P11), + Body(x => x.P12), + static (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value, + c7.Value, + c8.Value, + c9.Value, + c10.Value, + c11.Value, + c12.Value)) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(TwelveChains)); + + fixture.P12 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(TwelveChains)); + } + + /// Observing 12 chains with the distinct gate named, reporting the combined value and each change. + /// A task representing the asynchronous test operation. + [Test] + public async Task Arity12WithDistinctSpecified_ReportsTheCombinedValueAndEachChange() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture.WhenAnyDynamic( + Body(x => x.P1), + Body(x => x.P2), + Body(x => x.P3), + Body(x => x.P4), + Body(x => x.P5), + Body(x => x.P6), + Body(x => x.P7), + Body(x => x.P8), + Body(x => x.P9), + Body(x => x.P10), + Body(x => x.P11), + Body(x => x.P12), + static (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12) => string.Concat( + c1.Value, + c2.Value, + c3.Value, + c4.Value, + c5.Value, + c6.Value, + c7.Value, + c8.Value, + c9.Value, + c10.Value, + c11.Value, + c12.Value), + true) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(Initial(TwelveChains)); + + fixture.P12 = MovedValue; + + await Assert.That(seen[^1]).IsEqualTo(LastMoved(TwelveChains)); + } + + /// The distinct gate suppresses a notification that leaves the value where it was. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyDynamic_WithTheDistinctGateOn_SuppressesADuplicate() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture + .WhenAnyDynamic(Body(x => x.P1), static c1 => string.Concat(c1.Value), true) + .Subscribe(seen.Add); + + fixture.P1 = MovedValue; + fixture.P1 = MovedValue; + + await Assert.That(seen).IsEquivalentTo(SeedThenMove); + } + + /// With the gate off, a notification is reported even where the value did not move. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyDynamic_WithTheDistinctGateOff_ReportsADuplicate() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture + .WhenAnyDynamic(Body(x => x.P1), static c1 => string.Concat(c1.Value), false) + .Subscribe(seen.Add); + + fixture.P1 = MovedValue; + fixture.P1 = MovedValue; + + await Assert.That(seen).IsEquivalentTo(SeedThenMoveTwice); + } + + /// A chain through an intermediate follows the intermediate when it is replaced. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyDynamic_ThroughAnIntermediate_FollowsTheReplacement() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new DynamicChainFixture(); + var seen = new List(); + + using var subscription = fixture + .WhenAnyDynamic(Body(x => x.Child!.Name), static c1 => string.Concat(c1.Value)) + .Subscribe(seen.Add); + + fixture.Child = new DynamicChainChild { Name = MovedValue }; + + await Assert.That(seen[^1]).IsEqualTo(MovedValue); + } + + /// An object to observe is required. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyDynamic_WithNoSender_ThrowsArgumentNullException() + { + DynamicChainFixture? fixture = null; + + await Assert.That(() => fixture!.WhenAnyDynamic(Body(x => x.P1), static c1 => string.Concat(c1.Value))) + .ThrowsExactly(); + } + + /// A selector is required. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyDynamic_WithNoSelector_ThrowsArgumentNullException() + { + var fixture = new DynamicChainFixture(); + + await Assert.That(() => fixture.WhenAnyDynamic(Body(x => x.P1), null!)) + .ThrowsExactly(); + } + + /// The combined value while every observed chain still holds its initial value. + /// How many chains were observed. + /// What the selector concatenates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string Initial(int count) => string.Concat(Enumerable.Repeat(InitialValue, count)); + + /// The combined value once the last of the observed chains has moved. + /// How many chains were observed. + /// What the selector concatenates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string LastMoved(int count) => Initial(count - 1) + MovedValue; + + /// Names a property chain the way a caller builds one at run time. + /// The type the chain ends at. + /// The chain to name. + /// The expression body, which is what these overloads take. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static System.Linq.Expressions.Expression Body( + Expression> property) => + property.Body; +} diff --git a/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyObservableWideArityTests.cs b/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyObservableWideArityTests.cs new file mode 100644 index 00000000..514f29b1 --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyObservableWideArityTests.cs @@ -0,0 +1,740 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. +using ReactiveUI.Binding.Tests.TestModels; +using ReactiveUI.Binding.Tests.WhenAny; + +namespace ReactiveUI.Binding.Tests.Mixins; + +/// Reaches every arity of the runtime WhenAnyObservable overloads, which follow the observable a property holds. +/// +/// Each overload is called on the declaring class rather than as an extension method. Written as an +/// extension call the generated dispatch wins overload resolution, and the runtime overload these +/// assertions are about would never run. +/// +public class WhenAnyObservableWideArityTests +{ + /// The value each stream is driven with. + private const string EmittedValue = "a"; + + /// The first and last stream's values read together, which is what the selectors project. + private const string BothEnds = EmittedValue + EmittedValue; + + /// Following one stream reports what it emits. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_FollowingOneStream_ReportsTheEmittedValue() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(EmittedValue); + } + + /// Following two streams reports what they emit. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_FollowingTwoStreams_ReportsTheEmittedValue() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(EmittedValue); + } + + /// Following three streams reports what they emit. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_FollowingThreeStreams_ReportsTheEmittedValue() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(EmittedValue); + } + + /// Following four streams reports what they emit. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_FollowingFourStreams_ReportsTheEmittedValue() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(EmittedValue); + } + + /// Following five streams reports what they emit. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_FollowingFiveStreams_ReportsTheEmittedValue() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(EmittedValue); + } + + /// Following six streams reports what they emit. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_FollowingSixStreams_ReportsTheEmittedValue() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(EmittedValue); + } + + /// Following seven streams reports what they emit. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_FollowingSevenStreams_ReportsTheEmittedValue() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + x => x.Stream7) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(EmittedValue); + } + + /// Following eight streams reports what they emit. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_FollowingEightStreams_ReportsTheEmittedValue() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + x => x.Stream7, + x => x.Stream8) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(EmittedValue); + } + + /// Following nine streams reports what they emit. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_FollowingNineStreams_ReportsTheEmittedValue() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + x => x.Stream7, + x => x.Stream8, + x => x.Stream9) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(EmittedValue); + } + + /// Following ten streams reports what they emit. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_FollowingTenStreams_ReportsTheEmittedValue() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + x => x.Stream7, + x => x.Stream8, + x => x.Stream9, + x => x.Stream10) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(EmittedValue); + } + + /// Following eleven streams reports what they emit. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_FollowingElevenStreams_ReportsTheEmittedValue() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + x => x.Stream7, + x => x.Stream8, + x => x.Stream9, + x => x.Stream10, + x => x.Stream11) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(EmittedValue); + } + + /// Following twelve streams reports what they emit. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_FollowingTwelveStreams_ReportsTheEmittedValue() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + x => x.Stream7, + x => x.Stream8, + x => x.Stream9, + x => x.Stream10, + x => x.Stream11, + x => x.Stream12) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(EmittedValue); + } + + /// Combining two streams reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_CombiningTwoStreams_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + static (v1, v2) => v1 + v2) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Combining three streams reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_CombiningThreeStreams_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + static (v1, v2, v3) => v1 + v3) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Combining four streams reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_CombiningFourStreams_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + static (v1, v2, v3, v4) => v1 + v4) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Combining five streams reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_CombiningFiveStreams_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + static (v1, v2, v3, v4, v5) => v1 + v5) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Combining six streams reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_CombiningSixStreams_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + static (v1, v2, v3, v4, v5, v6) => v1 + v6) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Combining seven streams reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_CombiningSevenStreams_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + x => x.Stream7, + static (v1, v2, v3, v4, v5, v6, v7) => v1 + v7) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Combining eight streams reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_CombiningEightStreams_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + x => x.Stream7, + x => x.Stream8, + static (v1, v2, v3, v4, v5, v6, v7, v8) => v1 + v8) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Combining nine streams reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_CombiningNineStreams_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + x => x.Stream7, + x => x.Stream8, + x => x.Stream9, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9) => v1 + v9) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Combining ten streams reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_CombiningTenStreams_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + x => x.Stream7, + x => x.Stream8, + x => x.Stream9, + x => x.Stream10, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10) => v1 + v10) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Combining eleven streams reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_CombiningElevenStreams_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + x => x.Stream7, + x => x.Stream8, + x => x.Stream9, + x => x.Stream10, + x => x.Stream11, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11) => v1 + v11) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Combining twelve streams reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyObservable_CombiningTwelveStreams_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityObservableFixture(); + var streams = fixture.FillStreams(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyObservable( + fixture, + x => x.Stream1, + x => x.Stream2, + x => x.Stream3, + x => x.Stream4, + x => x.Stream5, + x => x.Stream6, + x => x.Stream7, + x => x.Stream8, + x => x.Stream9, + x => x.Stream10, + x => x.Stream11, + x => x.Stream12, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12) => v1 + v12) + .Subscribe(seen.Add); + + foreach (var stream in streams) + { + // Only the streams this arity observes were subscribed to, so the rest have no observer. + stream.Observer?.OnNext(EmittedValue); + } + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } +} diff --git a/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyValueWideArityTests.cs b/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyValueWideArityTests.cs new file mode 100644 index 00000000..33b61191 --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyValueWideArityTests.cs @@ -0,0 +1,854 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. +using ReactiveUI.Binding.Tests.TestModels; +using ReactiveUI.Binding.Tests.WhenAny; + +namespace ReactiveUI.Binding.Tests.Mixins; + +/// Reaches every arity of the runtime WhenAnyValue overloads, the ReactiveUI-compatible spelling of WhenChanged. +/// +/// Each overload is called on the declaring class rather than as an extension method. Written as an +/// extension call the generated dispatch wins overload resolution, and the runtime overload these +/// assertions are about would never run. +/// +public class WhenAnyValueWideArityTests +{ + /// The value every observed property starts out holding. + private const string InitialValue = "a"; + + /// The first and last observed values read together, which is what each test projects. + private const string BothEnds = InitialValue + InitialValue; + + /// Observing one property reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnOneProperty_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(InitialValue); + } + + /// Observing two properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnTwoProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2) + .Subscribe(v => seen.Add(v.Property1 + v.Property2)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing three properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnThreeProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3) + .Subscribe(v => seen.Add(v.Property1 + v.Property3)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing four properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnFourProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4) + .Subscribe(v => seen.Add(v.Property1 + v.Property4)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing five properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnFiveProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5) + .Subscribe(v => seen.Add(v.Property1 + v.Property5)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing six properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnSixProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6) + .Subscribe(v => seen.Add(v.Property1 + v.Property6)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing seven properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnSevenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7) + .Subscribe(v => seen.Add(v.Property1 + v.Property7)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing eight properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnEightProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8) + .Subscribe(v => seen.Add(v.Property1 + v.Property8)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing nine properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnNineProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9) + .Subscribe(v => seen.Add(v.Property1 + v.Property9)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing ten properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnTenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10) + .Subscribe(v => seen.Add(v.Property1 + v.Property10)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing eleven properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnElevenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11) + .Subscribe(v => seen.Add(v.Property1 + v.Property11)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing twelve properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnTwelveProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12) + .Subscribe(v => seen.Add(v.Property1 + v.Property12)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing thirteen properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnThirteenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13) + .Subscribe(v => seen.Add(v.Property1 + v.Property13)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing fourteen properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnFourteenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14) + .Subscribe(v => seen.Add(v.Property1 + v.Property14)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing fifteen properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnFifteenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + x => x.Value15) + .Subscribe(v => seen.Add(v.Property1 + v.Property15)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing sixteen properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_OnSixteenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + x => x.Value15, + x => x.Value16) + .Subscribe(v => seen.Add(v.Property1 + v.Property16)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting one observed property reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingOneProperty_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + static (string v1) => v1) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(InitialValue); + } + + /// Projecting two observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingTwoProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + static (v1, v2) => v1 + v2) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting three observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingThreeProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + static (v1, v2, v3) => v1 + v3) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting four observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingFourProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + static (v1, v2, v3, v4) => v1 + v4) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting five observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingFiveProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + static (v1, v2, v3, v4, v5) => v1 + v5) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting six observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingSixProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + static (v1, v2, v3, v4, v5, v6) => v1 + v6) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting seven observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingSevenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + static (v1, v2, v3, v4, v5, v6, v7) => v1 + v7) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting eight observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingEightProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + static (v1, v2, v3, v4, v5, v6, v7, v8) => v1 + v8) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting nine observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingNineProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9) => v1 + v9) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting ten observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingTenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10) => v1 + v10) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting eleven observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingElevenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11) => v1 + v11) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting twelve observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingTwelveProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12) => v1 + v12) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting thirteen observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingThirteenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13) => v1 + v13) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting fourteen observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingFourteenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14) => v1 + v14) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting fifteen observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingFifteenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + x => x.Value15, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15) => v1 + v15) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting sixteen observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAnyValue_ProjectingSixteenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAnyValue( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + x => x.Value15, + x => x.Value16, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) => v1 + v16) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } +} diff --git a/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyWideArityTests.cs b/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyWideArityTests.cs new file mode 100644 index 00000000..bb4ec922 --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenAnyWideArityTests.cs @@ -0,0 +1,316 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. +using ReactiveUI.Binding.Tests.TestModels; +using ReactiveUI.Binding.Tests.WhenAny; + +namespace ReactiveUI.Binding.Tests.Mixins; + +/// Reaches every arity of the runtime WhenAny overloads, which hand the selector an observed change rather than a bare value. +/// +/// Each overload is called on the declaring class rather than as an extension method. Written as an +/// extension call the generated dispatch wins overload resolution, and the runtime overload these +/// assertions are about would never run. +/// +public class WhenAnyWideArityTests +{ + /// The value every observed property starts out holding. + private const string InitialValue = "a"; + + /// The first and last observed values read together, which is what each test projects. + private const string BothEnds = InitialValue + InitialValue; + + /// Observing one property hands the selector each observed change. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAny_OnOneProperty_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAny( + fixture, + x => x.Value1, + static c1 => c1.Value) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(InitialValue); + } + + /// Observing two properties hands the selector each observed change. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAny_OnTwoProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAny( + fixture, + x => x.Value1, + x => x.Value2, + static (c1, c2) => c1.Value + c2.Value) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing three properties hands the selector each observed change. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAny_OnThreeProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAny( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + static (c1, c2, c3) => c1.Value + c3.Value) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing four properties hands the selector each observed change. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAny_OnFourProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAny( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + static (c1, c2, c3, c4) => c1.Value + c4.Value) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing five properties hands the selector each observed change. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAny_OnFiveProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAny( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + static (c1, c2, c3, c4, c5) => c1.Value + c5.Value) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing six properties hands the selector each observed change. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAny_OnSixProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAny( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + static (c1, c2, c3, c4, c5, c6) => c1.Value + c6.Value) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing seven properties hands the selector each observed change. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAny_OnSevenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAny( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + static (c1, c2, c3, c4, c5, c6, c7) => c1.Value + c7.Value) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing eight properties hands the selector each observed change. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAny_OnEightProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAny( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + static (c1, c2, c3, c4, c5, c6, c7, c8) => c1.Value + c8.Value) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing nine properties hands the selector each observed change. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAny_OnNineProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAny( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + static (c1, c2, c3, c4, c5, c6, c7, c8, c9) => c1.Value + c9.Value) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing ten properties hands the selector each observed change. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAny_OnTenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAny( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + static (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10) => c1.Value + c10.Value) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing eleven properties hands the selector each observed change. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAny_OnElevenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAny( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + static (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11) => c1.Value + c11.Value) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing twelve properties hands the selector each observed change. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenAny_OnTwelveProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenAny( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + static (c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12) => c1.Value + c12.Value) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } +} diff --git a/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenChangedWideArityTests.cs b/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenChangedWideArityTests.cs new file mode 100644 index 00000000..b6abd20b --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenChangedWideArityTests.cs @@ -0,0 +1,835 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. +using ReactiveUI.Binding.Tests.TestModels; +using ReactiveUI.Binding.Tests.WhenAny; + +namespace ReactiveUI.Binding.Tests.Mixins; + +/// Reaches every arity of the runtime WhenChanged overloads, which observe after a value changes. +/// +/// Each overload is called on the declaring class rather than as an extension method. Written as an +/// extension call the generated dispatch wins overload resolution, and the runtime overload these +/// assertions are about would never run. +/// +public class WhenChangedWideArityTests +{ + /// The value every observed property starts out holding. + private const string InitialValue = "a"; + + /// The first and last observed values read together, which is what each test projects. + private const string BothEnds = InitialValue + InitialValue; + + /// Observing one property reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnOneProperty_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(InitialValue); + } + + /// Observing two properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnTwoProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2) + .Subscribe(v => seen.Add(v.Property1 + v.Property2)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing three properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnThreeProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3) + .Subscribe(v => seen.Add(v.Property1 + v.Property3)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing four properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnFourProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4) + .Subscribe(v => seen.Add(v.Property1 + v.Property4)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing five properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnFiveProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5) + .Subscribe(v => seen.Add(v.Property1 + v.Property5)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing six properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnSixProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6) + .Subscribe(v => seen.Add(v.Property1 + v.Property6)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing seven properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnSevenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7) + .Subscribe(v => seen.Add(v.Property1 + v.Property7)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing eight properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnEightProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8) + .Subscribe(v => seen.Add(v.Property1 + v.Property8)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing nine properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnNineProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9) + .Subscribe(v => seen.Add(v.Property1 + v.Property9)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing ten properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnTenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10) + .Subscribe(v => seen.Add(v.Property1 + v.Property10)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing eleven properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnElevenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11) + .Subscribe(v => seen.Add(v.Property1 + v.Property11)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing twelve properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnTwelveProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12) + .Subscribe(v => seen.Add(v.Property1 + v.Property12)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing thirteen properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnThirteenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13) + .Subscribe(v => seen.Add(v.Property1 + v.Property13)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing fourteen properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnFourteenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14) + .Subscribe(v => seen.Add(v.Property1 + v.Property14)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing fifteen properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnFifteenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + x => x.Value15) + .Subscribe(v => seen.Add(v.Property1 + v.Property15)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Observing sixteen properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_OnSixteenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + x => x.Value15, + x => x.Value16) + .Subscribe(v => seen.Add(v.Property1 + v.Property16)); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting two observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingTwoProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + static (v1, v2) => v1 + v2) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting three observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingThreeProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + static (v1, v2, v3) => v1 + v3) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting four observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingFourProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + static (v1, v2, v3, v4) => v1 + v4) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting five observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingFiveProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + static (v1, v2, v3, v4, v5) => v1 + v5) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting six observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingSixProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + static (v1, v2, v3, v4, v5, v6) => v1 + v6) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting seven observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingSevenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + static (v1, v2, v3, v4, v5, v6, v7) => v1 + v7) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting eight observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingEightProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + static (v1, v2, v3, v4, v5, v6, v7, v8) => v1 + v8) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting nine observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingNineProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9) => v1 + v9) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting ten observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingTenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10) => v1 + v10) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting eleven observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingElevenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11) => v1 + v11) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting twelve observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingTwelveProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12) => v1 + v12) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting thirteen observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingThirteenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13) => v1 + v13) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting fourteen observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingFourteenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14) => v1 + v14) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting fifteen observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingFifteenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + x => x.Value15, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15) => v1 + v15) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } + + /// Projecting sixteen observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanged_ProjectingSixteenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanged( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + x => x.Value15, + x => x.Value16, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) => v1 + v16) + .Subscribe(seen.Add); + + await Assert.That(seen[0]).IsEqualTo(BothEnds); + } +} diff --git a/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenChangingWideArityTests.cs b/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenChangingWideArityTests.cs new file mode 100644 index 00000000..78eaed7c --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/Mixins/WhenChangingWideArityTests.cs @@ -0,0 +1,897 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. +using ReactiveUI.Binding.Tests.TestModels; +using ReactiveUI.Binding.Tests.WhenAny; + +namespace ReactiveUI.Binding.Tests.Mixins; + +/// Reaches every arity of the runtime WhenChanging overloads, which observe before a value changes. +/// +/// Each overload is called on the declaring class rather than as an extension method. Written as an +/// extension call the generated dispatch wins overload resolution, and the runtime overload these +/// assertions are about would never run. +/// +public class WhenChangingWideArityTests +{ + /// The value every observed property starts out holding. + private const string InitialValue = "a"; + + /// The first and last observed values read together, which is what each test projects. + private const string BothEnds = InitialValue + InitialValue; + + /// Observing one property reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnOneProperty_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(InitialValue); + } + + /// Observing two properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnTwoProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2) + .Subscribe(v => seen.Add(v.Property1 + v.Property2)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing three properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnThreeProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3) + .Subscribe(v => seen.Add(v.Property1 + v.Property3)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing four properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnFourProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4) + .Subscribe(v => seen.Add(v.Property1 + v.Property4)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing five properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnFiveProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5) + .Subscribe(v => seen.Add(v.Property1 + v.Property5)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing six properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnSixProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6) + .Subscribe(v => seen.Add(v.Property1 + v.Property6)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing seven properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnSevenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7) + .Subscribe(v => seen.Add(v.Property1 + v.Property7)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing eight properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnEightProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8) + .Subscribe(v => seen.Add(v.Property1 + v.Property8)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing nine properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnNineProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9) + .Subscribe(v => seen.Add(v.Property1 + v.Property9)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing ten properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnTenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10) + .Subscribe(v => seen.Add(v.Property1 + v.Property10)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing eleven properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnElevenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11) + .Subscribe(v => seen.Add(v.Property1 + v.Property11)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing twelve properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnTwelveProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12) + .Subscribe(v => seen.Add(v.Property1 + v.Property12)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing thirteen properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnThirteenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13) + .Subscribe(v => seen.Add(v.Property1 + v.Property13)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing fourteen properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnFourteenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14) + .Subscribe(v => seen.Add(v.Property1 + v.Property14)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing fifteen properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnFifteenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + x => x.Value15) + .Subscribe(v => seen.Add(v.Property1 + v.Property15)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Observing sixteen properties reports the first and last observed value. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_OnSixteenProperties_ReportsTheObservedValues() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + x => x.Value15, + x => x.Value16) + .Subscribe(v => seen.Add(v.Property1 + v.Property16)); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting two observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingTwoProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + static (v1, v2) => v1 + v2) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting three observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingThreeProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + static (v1, v2, v3) => v1 + v3) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting four observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingFourProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + static (v1, v2, v3, v4) => v1 + v4) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting five observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingFiveProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + static (v1, v2, v3, v4, v5) => v1 + v5) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting six observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingSixProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + static (v1, v2, v3, v4, v5, v6) => v1 + v6) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting seven observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingSevenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + static (v1, v2, v3, v4, v5, v6, v7) => v1 + v7) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting eight observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingEightProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + static (v1, v2, v3, v4, v5, v6, v7, v8) => v1 + v8) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting nine observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingNineProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9) => v1 + v9) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting ten observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingTenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10) => v1 + v10) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting eleven observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingElevenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11) => v1 + v11) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting twelve observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingTwelveProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12) => v1 + v12) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting thirteen observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingThirteenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13) => v1 + v13) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting fourteen observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingFourteenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14) => v1 + v14) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting fifteen observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingFifteenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + x => x.Value15, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15) => v1 + v15) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } + + /// Projecting sixteen observed properties reports what the selector returns. + /// A task representing the asynchronous test operation. + [Test] + public async Task WhenChanging_ProjectingSixteenProperties_ReportsTheSelectorResult() + { + WhenAnyTests.EnsureInitialized(); + + var fixture = new WideArityFixture(); + var seen = new List(); + + using var subscription = ReactiveUIBindingExtensions.WhenChanging( + fixture, + x => x.Value1, + x => x.Value2, + x => x.Value3, + x => x.Value4, + x => x.Value5, + x => x.Value6, + x => x.Value7, + x => x.Value8, + x => x.Value9, + x => x.Value10, + x => x.Value11, + x => x.Value12, + x => x.Value13, + x => x.Value14, + x => x.Value15, + x => x.Value16, + static (v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) => v1 + v16) + .Subscribe(seen.Add); + + fixture.Value1 = InitialValue; + + await Assert.That(seen[^1]).IsEqualTo(BothEnds); + } +} diff --git a/src/tests/ReactiveUI.Binding.Tests/TestModels/DispatchStubControl.cs b/src/tests/ReactiveUI.Binding.Tests/TestModels/DispatchStubControl.cs new file mode 100644 index 00000000..6dd78223 --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/TestModels/DispatchStubControl.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +namespace ReactiveUI.Binding.Tests.TestModels; + +/// A control a command binding names, carrying nothing beyond a property to bind against. +public class DispatchStubControl +{ + /// Gets or sets the control's text. + public string Text { get; set; } = "a"; +} diff --git a/src/tests/ReactiveUI.Binding.Tests/TestModels/DispatchStubView.cs b/src/tests/ReactiveUI.Binding.Tests/TestModels/DispatchStubView.cs new file mode 100644 index 00000000..df489d19 --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/TestModels/DispatchStubView.cs @@ -0,0 +1,31 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.ComponentModel; + +namespace ReactiveUI.Binding.Tests.TestModels; + +/// A view naming the properties the binding dispatch stubs bind against. +public class DispatchStubView : IViewFor, INotifyPropertyChanged +{ + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// + public object? ViewModel { get; set; } + + /// Gets the control a command binding names. + public DispatchStubControl Control { get; } = new(); + + /// Gets or sets the bound text. + public string Caption + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Caption))); + } + } = "a"; +} diff --git a/src/tests/ReactiveUI.Binding.Tests/TestModels/DispatchStubViewModel.cs b/src/tests/ReactiveUI.Binding.Tests/TestModels/DispatchStubViewModel.cs new file mode 100644 index 00000000..f588b877 --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/TestModels/DispatchStubViewModel.cs @@ -0,0 +1,47 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.ComponentModel; +using System.Windows.Input; + +namespace ReactiveUI.Binding.Tests.TestModels; + +/// A view model naming one property of each kind the binding dispatch stubs take. +/// +/// The command and interaction are never invoked: a stub throws before it reads either, so the +/// properties exist to satisfy the overloads' type constraints rather than to behave. +/// +public class DispatchStubViewModel : INotifyPropertyChanged +{ + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// Gets or sets the bound text. + public string Caption + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Caption))); + } + } = "a"; + + /// Gets or sets the value a command binding passes as its parameter. + public string Parameter + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Parameter))); + } + } = "a"; + + /// Gets or sets the command a command binding names. + public ICommand? Run { get; set; } + + /// Gets or sets the interaction an interaction binding names. + public IInteraction Confirm { get; set; } = null!; +} diff --git a/src/tests/ReactiveUI.Binding.Tests/TestModels/DynamicChainChild.cs b/src/tests/ReactiveUI.Binding.Tests/TestModels/DynamicChainChild.cs new file mode 100644 index 00000000..3f88a2ec --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/TestModels/DynamicChainChild.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.ComponentModel; + +namespace ReactiveUI.Binding.Tests.TestModels; + +/// An object reached through an intermediate link of an observed chain. +/// Public because the chain is walked by reflection, which reads public members. +public class DynamicChainChild : INotifyPropertyChanged +{ + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// Gets or sets the observed value. + public string Name + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Name))); + } + } = "a"; +} diff --git a/src/tests/ReactiveUI.Binding.Tests/TestModels/DynamicChainFixture.cs b/src/tests/ReactiveUI.Binding.Tests/TestModels/DynamicChainFixture.cs new file mode 100644 index 00000000..7553d7f6 --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/TestModels/DynamicChainFixture.cs @@ -0,0 +1,158 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.ComponentModel; + +namespace ReactiveUI.Binding.Tests.TestModels; + +/// An object with enough notifying properties to observe every dynamic-chain arity. +/// Public because the chain is walked by reflection, which reads public members. +public class DynamicChainFixture : INotifyPropertyChanged +{ + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// Gets or sets observed property 1. + public string P1 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(P1))); + } + } = "a"; + + /// Gets or sets observed property 2. + public string P2 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(P2))); + } + } = "a"; + + /// Gets or sets observed property 3. + public string P3 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(P3))); + } + } = "a"; + + /// Gets or sets observed property 4. + public string P4 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(P4))); + } + } = "a"; + + /// Gets or sets observed property 5. + public string P5 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(P5))); + } + } = "a"; + + /// Gets or sets observed property 6. + public string P6 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(P6))); + } + } = "a"; + + /// Gets or sets observed property 7. + public string P7 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(P7))); + } + } = "a"; + + /// Gets or sets observed property 8. + public string P8 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(P8))); + } + } = "a"; + + /// Gets or sets observed property 9. + public string P9 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(P9))); + } + } = "a"; + + /// Gets or sets observed property 10. + public string P10 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(P10))); + } + } = "a"; + + /// Gets or sets observed property 11. + public string P11 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(P11))); + } + } = "a"; + + /// Gets or sets observed property 12. + public string P12 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(P12))); + } + } = "a"; + + /// Gets or sets the intermediate link an observed chain passes through. + public DynamicChainChild? Child + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Child))); + } + } +} diff --git a/src/tests/ReactiveUI.Binding.Tests/TestModels/WideArityFixture.cs b/src/tests/ReactiveUI.Binding.Tests/TestModels/WideArityFixture.cs new file mode 100644 index 00000000..d6b1a775 --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/TestModels/WideArityFixture.cs @@ -0,0 +1,208 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. +using System.ComponentModel; + +namespace ReactiveUI.Binding.Tests.TestModels; + +/// An object with enough notifying properties to reach every wide-arity observation overload. +public class WideArityFixture : INotifyPropertyChanged, INotifyPropertyChanging +{ + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// + public event PropertyChangingEventHandler? PropertyChanging; + + /// Gets or sets observed property 1. + public string Value1 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value1))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value1))); + } + } = "a"; + + /// Gets or sets observed property 2. + public string Value2 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value2))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value2))); + } + } = "a"; + + /// Gets or sets observed property 3. + public string Value3 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value3))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value3))); + } + } = "a"; + + /// Gets or sets observed property 4. + public string Value4 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value4))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value4))); + } + } = "a"; + + /// Gets or sets observed property 5. + public string Value5 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value5))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value5))); + } + } = "a"; + + /// Gets or sets observed property 6. + public string Value6 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value6))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value6))); + } + } = "a"; + + /// Gets or sets observed property 7. + public string Value7 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value7))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value7))); + } + } = "a"; + + /// Gets or sets observed property 8. + public string Value8 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value8))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value8))); + } + } = "a"; + + /// Gets or sets observed property 9. + public string Value9 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value9))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value9))); + } + } = "a"; + + /// Gets or sets observed property 10. + public string Value10 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value10))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value10))); + } + } = "a"; + + /// Gets or sets observed property 11. + public string Value11 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value11))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value11))); + } + } = "a"; + + /// Gets or sets observed property 12. + public string Value12 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value12))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value12))); + } + } = "a"; + + /// Gets or sets observed property 13. + public string Value13 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value13))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value13))); + } + } = "a"; + + /// Gets or sets observed property 14. + public string Value14 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value14))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value14))); + } + } = "a"; + + /// Gets or sets observed property 15. + public string Value15 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value15))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value15))); + } + } = "a"; + + /// Gets or sets observed property 16. + public string Value16 + { + get => field; + set + { + PropertyChanging?.Invoke(this, new(nameof(Value16))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Value16))); + } + } = "a"; +} diff --git a/src/tests/ReactiveUI.Binding.Tests/TestModels/WideArityObservableFixture.cs b/src/tests/ReactiveUI.Binding.Tests/TestModels/WideArityObservableFixture.cs new file mode 100644 index 00000000..741f63fe --- /dev/null +++ b/src/tests/ReactiveUI.Binding.Tests/TestModels/WideArityObservableFixture.cs @@ -0,0 +1,175 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.ComponentModel; + +namespace ReactiveUI.Binding.Tests.TestModels; + +/// An object with enough observable properties to reach every wide-arity WhenAnyObservable overload. +public class WideArityObservableFixture : INotifyPropertyChanged +{ + /// How many streams the fixture carries, which is the widest arity these overloads reach. + private const int StreamCount = 12; + + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// Gets or sets observed stream 1. + public IObservable? Stream1 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Stream1))); + } + } + + /// Gets or sets observed stream 2. + public IObservable? Stream2 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Stream2))); + } + } + + /// Gets or sets observed stream 3. + public IObservable? Stream3 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Stream3))); + } + } + + /// Gets or sets observed stream 4. + public IObservable? Stream4 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Stream4))); + } + } + + /// Gets or sets observed stream 5. + public IObservable? Stream5 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Stream5))); + } + } + + /// Gets or sets observed stream 6. + public IObservable? Stream6 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Stream6))); + } + } + + /// Gets or sets observed stream 7. + public IObservable? Stream7 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Stream7))); + } + } + + /// Gets or sets observed stream 8. + public IObservable? Stream8 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Stream8))); + } + } + + /// Gets or sets observed stream 9. + public IObservable? Stream9 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Stream9))); + } + } + + /// Gets or sets observed stream 10. + public IObservable? Stream10 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Stream10))); + } + } + + /// Gets or sets observed stream 11. + public IObservable? Stream11 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Stream11))); + } + } + + /// Gets or sets observed stream 12. + public IObservable? Stream12 + { + get => field; + set + { + field = value; + PropertyChanged?.Invoke(this, new(nameof(Stream12))); + } + } + + /// Fills every stream with a manually driven observable and hands them back in order. + /// The observables the streams were filled with. + internal ManualObservable[] FillStreams() + { + var streams = new ManualObservable[StreamCount]; + for (var i = 0; i < streams.Length; i++) + { + streams[i] = new(); + } + + Stream1 = streams[0]; + Stream2 = streams[1]; + Stream3 = streams[2]; + Stream4 = streams[3]; + Stream5 = streams[4]; + Stream6 = streams[5]; + Stream7 = streams[6]; + Stream8 = streams[7]; + Stream9 = streams[8]; + Stream10 = streams[9]; + Stream11 = streams[10]; + Stream12 = streams[11]; + + return streams; + } +}