diff --git a/src/ReactiveUI.Binding.SourceGenerators/CodeGeneration/ObservationCodeGenerator.cs b/src/ReactiveUI.Binding.SourceGenerators/CodeGeneration/ObservationCodeGenerator.cs index 3beb6bce..eb1db12e 100644 --- a/src/ReactiveUI.Binding.SourceGenerators/CodeGeneration/ObservationCodeGenerator.cs +++ b/src/ReactiveUI.Binding.SourceGenerators/CodeGeneration/ObservationCodeGenerator.cs @@ -720,6 +720,10 @@ internal static void EmitInlineObservation( /// for inner segments, so reusing the root's plugin is safe whatever the segment declares. /// /// Whether before-change notifications are being observed. + /// + /// Missing parents propagate through intermediate stages so downstream subscriptions detach, + /// while the leaf suppresses the default value to match the runtime expression-chain fallback. + /// private static void EmitObservationChainInnerSegments( StringBuilder sb, EquatableArray path, @@ -735,9 +739,25 @@ private static void EmitObservationChainInnerSegments( var lambdaParam = $"__parent{s}"; var segType = seg.PropertyTypeFullName; + // Only the leaf suppresses. Inner segments keep pushing the null downstream so the + // stage below re-parents onto null and drops its subscription on the detached subtree. + var nullParentBehavior = s == path.Length - 1 + ? NullParentObservationBehavior.SuppressEmission + : NullParentObservationBehavior.EmitDefault; + var nullParentObservable = nullParentBehavior == NullParentObservationBehavior.EmitDefault + ? $"new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>(default({segType}))" + : $"global::ReactiveUI.Binding.Observables.EmptyObservable<{segType}>.Instance"; + if (rootPlugin is not null) { - rootPlugin.EmitDeepChainInnerSegment(sb, prevVar, curVar, lambdaParam, seg, isBeforeChange); + rootPlugin.EmitDeepChainInnerSegment( + sb, + prevVar, + curVar, + lambdaParam, + seg, + isBeforeChange, + nullParentBehavior); } else if (IsINPChanging(classInfo) && isBeforeChange) { @@ -750,7 +770,7 @@ private static void EmitObservationChainInnerSegments( (global::System.ComponentModel.INotifyPropertyChanging){lambdaParam}, "{seg.PropertyName}", (global::System.ComponentModel.INotifyPropertyChanging __o) => (({seg.DeclaringTypeFullName})__o).{seg.PropertyName}) - : (global::System.IObservable<{segType}>)new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>(default({segType})))); + : (global::System.IObservable<{segType}>){nullParentObservable})); """); } else @@ -759,8 +779,10 @@ private static void EmitObservationChainInnerSegments( .AppendLine($""" var {curVar} = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Switch( global::ReactiveUI.Binding.Observables.RxBindingExtensions.Select({prevVar}, - {lambdaParam} => (global::System.IObservable<{segType}>)new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>( - {lambdaParam} != null ? (({seg.DeclaringTypeFullName}){lambdaParam}).{seg.PropertyName} : default({segType})))); + {lambdaParam} => {lambdaParam} != null + ? (global::System.IObservable<{segType}>) + new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>((({seg.DeclaringTypeFullName}){lambdaParam}).{seg.PropertyName}) + : (global::System.IObservable<{segType}>){nullParentObservable})); """); } } @@ -779,6 +801,10 @@ private static void EmitObservationChainInnerSegments( /// /// Whether before-change notifications are being observed. /// The variable-name prefix for the emitted stages. + /// + /// Missing parents propagate through intermediate stages so downstream subscriptions detach, + /// while the leaf suppresses the default value to match the runtime expression-chain fallback. + /// private static void EmitDeepChainInnerSegments( StringBuilder sb, EquatableArray path, @@ -795,9 +821,23 @@ private static void EmitDeepChainInnerSegments( var lambdaParam = $"{varName}_p{s}"; var segType = seg.PropertyTypeFullName; + var nullParentBehavior = s == path.Length - 1 + ? NullParentObservationBehavior.SuppressEmission + : NullParentObservationBehavior.EmitDefault; + var nullParentObservable = nullParentBehavior == NullParentObservationBehavior.EmitDefault + ? $"new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>(default({segType}))" + : $"global::ReactiveUI.Binding.Observables.EmptyObservable<{segType}>.Instance"; + if (rootPlugin is not null) { - rootPlugin.EmitDeepChainInnerSegment(sb, prevObsVar, curObsVar, lambdaParam, seg, isBeforeChange); + rootPlugin.EmitDeepChainInnerSegment( + sb, + prevObsVar, + curObsVar, + lambdaParam, + seg, + isBeforeChange, + nullParentBehavior); } else if (IsINPChanging(classInfo) && isBeforeChange) { @@ -810,7 +850,7 @@ private static void EmitDeepChainInnerSegments( (global::System.ComponentModel.INotifyPropertyChanging){lambdaParam}, "{seg.PropertyName}", (global::System.ComponentModel.INotifyPropertyChanging __o) => (({seg.DeclaringTypeFullName})__o).{seg.PropertyName}) - : (global::System.IObservable<{segType}>)new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>(default({segType})))); + : (global::System.IObservable<{segType}>){nullParentObservable})); """); } else @@ -819,8 +859,10 @@ private static void EmitDeepChainInnerSegments( .AppendLine($""" var {curObsVar} = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Switch( global::ReactiveUI.Binding.Observables.RxBindingExtensions.Select({prevObsVar}, - {lambdaParam} => (global::System.IObservable<{segType}>)new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>( - {lambdaParam} != null ? (({seg.DeclaringTypeFullName}){lambdaParam}).{seg.PropertyName} : default({segType})))); + {lambdaParam} => {lambdaParam} != null + ? (global::System.IObservable<{segType}>) + new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>((({seg.DeclaringTypeFullName}){lambdaParam}).{seg.PropertyName}) + : (global::System.IObservable<{segType}>){nullParentObservable})); """); } } @@ -836,6 +878,7 @@ private static void EmitDeepChainInnerSegments( /// The root type's binding info, when known. /// The observation plugin for the root type, when one matched. /// The name of the variable the chain result is assigned to. + /// Missing parents emit defaults here so binding consumers can clear their targets. private static void EmitInlineDeepChain( StringBuilder sb, string rootVar, @@ -866,7 +909,14 @@ private static void EmitInlineDeepChain( if (plugin is not null) { - plugin.EmitDeepChainInnerSegment(sb, prevVar, curVar, lambdaParam, seg, false); + plugin.EmitDeepChainInnerSegment( + sb, + prevVar, + curVar, + lambdaParam, + seg, + isBeforeChange: false, + nullParentBehavior: NullParentObservationBehavior.EmitDefault); continue; } @@ -876,8 +926,10 @@ private static void EmitInlineDeepChain( .AppendLine($""" var {curVar} = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Switch( global::ReactiveUI.Binding.Observables.RxBindingExtensions.Select({prevVar}, - {lambdaParam} => (global::System.IObservable<{segType}>)new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>( - {lambdaParam} != null ? (({declType}){lambdaParam}).{seg.PropertyName} : default({segType})))); + {lambdaParam} => {lambdaParam} != null + ? (global::System.IObservable<{segType}>) + new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>((({declType}){lambdaParam}).{seg.PropertyName}) + : (global::System.IObservable<{segType}>)new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>(default({segType})))); """); } diff --git a/src/ReactiveUI.Binding.SourceGenerators/Plugins/IObservationPlugin.cs b/src/ReactiveUI.Binding.SourceGenerators/Plugins/IObservationPlugin.cs index dd587dde..ee1a7c08 100644 --- a/src/ReactiveUI.Binding.SourceGenerators/Plugins/IObservationPlugin.cs +++ b/src/ReactiveUI.Binding.SourceGenerators/Plugins/IObservationPlugin.cs @@ -102,13 +102,15 @@ void EmitDeepChainRootSegment( /// The lambda parameter name for the parent value. /// The current property path segment. /// True for WhenChanging (before-change). + /// The behavior to use while the parent segment is null. void EmitDeepChainInnerSegment( StringBuilder sb, string prevVar, string curVar, string lambdaParam, PropertyPathSegment segment, - bool isBeforeChange); + bool isBeforeChange, + NullParentObservationBehavior nullParentBehavior); /// Emits an inline observation variable for binding generators. Used by BindOneWay/BindTwoWay for direct observation code. /// The string builder to append to. diff --git a/src/ReactiveUI.Binding.SourceGenerators/Plugins/NullParentObservationBehavior.cs b/src/ReactiveUI.Binding.SourceGenerators/Plugins/NullParentObservationBehavior.cs new file mode 100644 index 00000000..45619e23 --- /dev/null +++ b/src/ReactiveUI.Binding.SourceGenerators/Plugins/NullParentObservationBehavior.cs @@ -0,0 +1,15 @@ +// 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.SourceGenerators.Plugins; + +/// Specifies what a generated deep-chain observer emits while its parent segment is null. +internal enum NullParentObservationBehavior +{ + /// Suppresses values until the parent becomes non-null. + SuppressEmission, + + /// Emits the leaf type's default value so binding consumers can clear their target. + EmitDefault, +} diff --git a/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/AndroidObservationPlugin.cs b/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/AndroidObservationPlugin.cs index 2b96d7e6..32912995 100644 --- a/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/AndroidObservationPlugin.cs +++ b/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/AndroidObservationPlugin.cs @@ -102,17 +102,23 @@ public void EmitDeepChainInnerSegment( string curVar, string lambdaParam, PropertyPathSegment segment, - bool isBeforeChange) + bool isBeforeChange, + NullParentObservationBehavior nullParentBehavior) { var segType = segment.PropertyTypeFullName; var declType = segment.DeclaringTypeFullName; + var nullParentObservable = nullParentBehavior == NullParentObservationBehavior.EmitDefault + ? $"new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>(default({segType}))" + : $"global::ReactiveUI.Binding.Observables.EmptyObservable<{segType}>.Instance"; _ = sb.AppendLine() .AppendLine($""" var {curVar} = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Switch( global::ReactiveUI.Binding.Observables.RxBindingExtensions.Select({prevVar}, - {lambdaParam} => (global::System.IObservable<{segType}>)new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>( - {lambdaParam} != null ? (({declType}){lambdaParam}).{segment.PropertyName} : default({segType})))); + {lambdaParam} => {lambdaParam} != null + ? (global::System.IObservable<{segType}>) + new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>((({declType}){lambdaParam}).{segment.PropertyName}) + : (global::System.IObservable<{segType}>){nullParentObservable})); """); } diff --git a/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/INPCObservationPlugin.cs b/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/INPCObservationPlugin.cs index 7e460e27..8db441e0 100644 --- a/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/INPCObservationPlugin.cs +++ b/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/INPCObservationPlugin.cs @@ -119,9 +119,13 @@ public void EmitDeepChainInnerSegment( string curVar, string lambdaParam, PropertyPathSegment segment, - bool isBeforeChange) + bool isBeforeChange, + NullParentObservationBehavior nullParentBehavior) { var segType = segment.PropertyTypeFullName; + var nullParentObservable = nullParentBehavior == NullParentObservationBehavior.EmitDefault + ? $"new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>(default({segType}))" + : $"global::ReactiveUI.Binding.Observables.EmptyObservable<{segType}>.Instance"; _ = sb.AppendLine() .AppendLine(isBeforeChange @@ -133,7 +137,7 @@ public void EmitDeepChainInnerSegment( (global::System.ComponentModel.INotifyPropertyChanging){lambdaParam}, "{segment.PropertyName}", (global::System.ComponentModel.INotifyPropertyChanging __o) => (({segment.DeclaringTypeFullName})__o).{segment.PropertyName}) - : (global::System.IObservable<{segType}>)new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>(default({segType})))); + : (global::System.IObservable<{segType}>){nullParentObservable})); """ : $""" var {curVar} = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Switch( @@ -144,7 +148,7 @@ public void EmitDeepChainInnerSegment( "{segment.PropertyName}", (global::System.ComponentModel.INotifyPropertyChanged __o) => (({segment.DeclaringTypeFullName})__o).{segment.PropertyName}, false) - : (global::System.IObservable<{segType}>)new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>(default({segType})))); + : (global::System.IObservable<{segType}>){nullParentObservable})); """); } diff --git a/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/KVOObservationPlugin.cs b/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/KVOObservationPlugin.cs index 99434563..cf4cca70 100644 --- a/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/KVOObservationPlugin.cs +++ b/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/KVOObservationPlugin.cs @@ -125,11 +125,15 @@ public void EmitDeepChainInnerSegment( string curVar, string lambdaParam, PropertyPathSegment segment, - bool isBeforeChange) + bool isBeforeChange, + NullParentObservationBehavior nullParentBehavior) { var segType = segment.PropertyTypeFullName; var declType = segment.DeclaringTypeFullName; var keyPath = ToKvoKeyPath(segment.PropertyName, segment.PropertyTypeFullName); + var nullParentObservable = nullParentBehavior == NullParentObservationBehavior.EmitDefault + ? $"new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>(default({segType}))" + : $"global::ReactiveUI.Binding.Observables.EmptyObservable<{segType}>.Instance"; _ = sb.AppendLine() .AppendLine($""" @@ -142,7 +146,7 @@ public void EmitDeepChainInnerSegment( (global::Foundation.NSObject __o) => (({declType})__o).{segment.PropertyName}, false, {BoolLiteral(isBeforeChange)}) - : (global::System.IObservable<{segType}>)new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>(default({segType})))); + : (global::System.IObservable<{segType}>){nullParentObservable})); """); } diff --git a/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/ReactiveObjectObservationPlugin.cs b/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/ReactiveObjectObservationPlugin.cs index d7938520..1d14059f 100644 --- a/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/ReactiveObjectObservationPlugin.cs +++ b/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/ReactiveObjectObservationPlugin.cs @@ -120,9 +120,13 @@ public void EmitDeepChainInnerSegment( string curVar, string lambdaParam, PropertyPathSegment segment, - bool isBeforeChange) + bool isBeforeChange, + NullParentObservationBehavior nullParentBehavior) { var segType = segment.PropertyTypeFullName; + var nullParentObservable = nullParentBehavior == NullParentObservationBehavior.EmitDefault + ? $"new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>(default({segType}))" + : $"global::ReactiveUI.Binding.Observables.EmptyObservable<{segType}>.Instance"; _ = sb.AppendLine() .AppendLine(isBeforeChange @@ -134,7 +138,7 @@ public void EmitDeepChainInnerSegment( (global::System.ComponentModel.INotifyPropertyChanging){lambdaParam}, "{segment.PropertyName}", (global::System.ComponentModel.INotifyPropertyChanging __o) => (({segment.DeclaringTypeFullName})__o).{segment.PropertyName}) - : (global::System.IObservable<{segType}>)new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>(default({segType})))); + : (global::System.IObservable<{segType}>){nullParentObservable})); """ : $""" var {curVar} = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Switch( @@ -145,7 +149,7 @@ public void EmitDeepChainInnerSegment( "{segment.PropertyName}", (global::System.ComponentModel.INotifyPropertyChanged __o) => (({segment.DeclaringTypeFullName})__o).{segment.PropertyName}, false) - : (global::System.IObservable<{segType}>)new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>(default({segType})))); + : (global::System.IObservable<{segType}>){nullParentObservable})); """); } diff --git a/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/WinFormsObservationPlugin.cs b/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/WinFormsObservationPlugin.cs index b457edee..f30fcef2 100644 --- a/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/WinFormsObservationPlugin.cs +++ b/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/WinFormsObservationPlugin.cs @@ -138,10 +138,14 @@ public void EmitDeepChainInnerSegment( string curVar, string lambdaParam, PropertyPathSegment segment, - bool isBeforeChange) + bool isBeforeChange, + NullParentObservationBehavior nullParentBehavior) { var segType = segment.PropertyTypeFullName; var declType = segment.DeclaringTypeFullName; + var nullParentObservable = nullParentBehavior == NullParentObservationBehavior.EmitDefault + ? $"new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>(default({segType}))" + : $"global::ReactiveUI.Binding.Observables.EmptyObservable<{segType}>.Instance"; if (isBeforeChange) { @@ -149,8 +153,10 @@ public void EmitDeepChainInnerSegment( .AppendLine($""" var {curVar} = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Switch( global::ReactiveUI.Binding.Observables.RxBindingExtensions.Select({prevVar}, - {lambdaParam} => (global::System.IObservable<{segType}>)new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>( - {lambdaParam} != null ? (({declType}){lambdaParam}).{segment.PropertyName} : default({segType})))); + {lambdaParam} => {lambdaParam} != null + ? (global::System.IObservable<{segType}>) + new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>((({declType}){lambdaParam}).{segment.PropertyName}) + : (global::System.IObservable<{segType}>){nullParentObservable})); """); return; } @@ -165,7 +171,7 @@ public void EmitDeepChainInnerSegment( __h => (({declType}){lambdaParam}).{segment.PropertyName}Changed -= __h, () => (({declType}){lambdaParam}).{segment.PropertyName}, false) - : (global::System.IObservable<{segType}>)new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>(default({segType})))); + : (global::System.IObservable<{segType}>){nullParentObservable})); """); } diff --git a/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/WinUIObservationPlugin.cs b/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/WinUIObservationPlugin.cs index 361a4d52..fe4a59c9 100644 --- a/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/WinUIObservationPlugin.cs +++ b/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/WinUIObservationPlugin.cs @@ -133,10 +133,14 @@ public void EmitDeepChainInnerSegment( string curVar, string lambdaParam, PropertyPathSegment segment, - bool isBeforeChange) + bool isBeforeChange, + NullParentObservationBehavior nullParentBehavior) { var segType = segment.PropertyTypeFullName; var declType = segment.DeclaringTypeFullName; + var nullParentObservable = nullParentBehavior == NullParentObservationBehavior.EmitDefault + ? $"new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>(default({segType}))" + : $"global::ReactiveUI.Binding.Observables.EmptyObservable<{segType}>.Instance"; if (isBeforeChange) { @@ -144,8 +148,10 @@ public void EmitDeepChainInnerSegment( .AppendLine($""" var {curVar} = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Switch( global::ReactiveUI.Binding.Observables.RxBindingExtensions.Select({prevVar}, - {lambdaParam} => (global::System.IObservable<{segType}>)new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>( - {lambdaParam} != null ? (({declType}){lambdaParam}).{segment.PropertyName} : default({segType})))); + {lambdaParam} => {lambdaParam} != null + ? (global::System.IObservable<{segType}>) + new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>((({declType}){lambdaParam}).{segment.PropertyName}) + : (global::System.IObservable<{segType}>){nullParentObservable})); """); return; } @@ -160,7 +166,7 @@ public void EmitDeepChainInnerSegment( {declType}.{segment.PropertyName}Property, (global::Microsoft.UI.Xaml.DependencyObject __o) => (({declType})__o).{segment.PropertyName}, false) - : (global::System.IObservable<{segType}>)new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>(default({segType})))); + : (global::System.IObservable<{segType}>){nullParentObservable})); """); } diff --git a/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/WpfObservationPlugin.cs b/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/WpfObservationPlugin.cs index 72699d0e..6db05b72 100644 --- a/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/WpfObservationPlugin.cs +++ b/src/ReactiveUI.Binding.SourceGenerators/Plugins/Observation/WpfObservationPlugin.cs @@ -146,10 +146,14 @@ public void EmitDeepChainInnerSegment( string curVar, string lambdaParam, PropertyPathSegment segment, - bool isBeforeChange) + bool isBeforeChange, + NullParentObservationBehavior nullParentBehavior) { var segType = segment.PropertyTypeFullName; var declType = segment.DeclaringTypeFullName; + var nullParentObservable = nullParentBehavior == NullParentObservationBehavior.EmitDefault + ? $"new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>(default({segType}))" + : $"global::ReactiveUI.Binding.Observables.EmptyObservable<{segType}>.Instance"; if (isBeforeChange) { @@ -158,8 +162,10 @@ public void EmitDeepChainInnerSegment( .AppendLine($""" var {curVar} = global::ReactiveUI.Binding.Observables.RxBindingExtensions.Switch( global::ReactiveUI.Binding.Observables.RxBindingExtensions.Select({prevVar}, - {lambdaParam} => (global::System.IObservable<{segType}>)new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>( - {lambdaParam} != null ? (({declType}){lambdaParam}).{segment.PropertyName} : default({segType})))); + {lambdaParam} => {lambdaParam} != null + ? (global::System.IObservable<{segType}>) + new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>((({declType}){lambdaParam}).{segment.PropertyName}) + : (global::System.IObservable<{segType}>){nullParentObservable})); """); return; } @@ -176,7 +182,7 @@ public void EmitDeepChainInnerSegment( {declType}.{segment.PropertyName}Property, typeof({declType})).RemoveValueChanged({lambdaParam}, __h), () => (({declType}){lambdaParam}).{segment.PropertyName}, false) - : (global::System.IObservable<{segType}>)new global::ReactiveUI.Binding.Observables.ReturnObservable<{segType}>(default({segType})))); + : (global::System.IObservable<{segType}>){nullParentObservable})); """); } diff --git a/src/tests/ReactiveUI.Binding.GeneratedCode.TestModels/Scenarios/WhenAnyValueScenarios.cs b/src/tests/ReactiveUI.Binding.GeneratedCode.TestModels/Scenarios/WhenAnyValueScenarios.cs index 68fe8440..f2f66bce 100644 --- a/src/tests/ReactiveUI.Binding.GeneratedCode.TestModels/Scenarios/WhenAnyValueScenarios.cs +++ b/src/tests/ReactiveUI.Binding.GeneratedCode.TestModels/Scenarios/WhenAnyValueScenarios.cs @@ -43,4 +43,25 @@ public static IObservable WithSelector_TwoProperties(WhenAnyTestFixture /// An observable of the nested City property value. public static IObservable DeepChain_AddressCity(BigViewModel vm) => vm.WhenAnyValue(x => x.Address.City); + + /// + /// Deep property chain observation on HostTestFixture.Child!.Name. + /// Uses the null-forgiving operator to test generator support for nullable intermediates. + /// + /// The host fixture to observe. + /// An observable of the nested Name property value. + public static IObservable DeepChain_ChildName(HostTestFixture host) => + host.WhenAnyValue(x => x.Child!.Name); + + /// Three-link property chain observation on HostTestFixture.Child!.Child!.Name. + /// The host fixture to observe. + /// An observable of the nested grandchild Name property value. + public static IObservable DeepChain_GrandchildName(HostTestFixture host) => + host.WhenAnyValue(x => x.Child!.Child!.Name); + + /// Deep property chain observation on HostTestFixture.Child!.Age. + /// The host fixture to observe. + /// An observable of the nested Age property value. + public static IObservable DeepChain_ChildAge(HostTestFixture host) => + host.WhenAnyValue(x => x.Child!.Age); } diff --git a/src/tests/ReactiveUI.Binding.GeneratedCode.TestModels/TestModels/TestViewModel.cs b/src/tests/ReactiveUI.Binding.GeneratedCode.TestModels/TestModels/TestViewModel.cs index e95d4263..51c132b4 100644 --- a/src/tests/ReactiveUI.Binding.GeneratedCode.TestModels/TestModels/TestViewModel.cs +++ b/src/tests/ReactiveUI.Binding.GeneratedCode.TestModels/TestModels/TestViewModel.cs @@ -51,4 +51,21 @@ public int Age PropertyChanged?.Invoke(this, new(nameof(Age))); } } + + /// Gets or sets the nested child used by three-link observation tests. + public TestViewModel? Child + { + get => field; + set + { + if (field == value) + { + return; + } + + PropertyChanging?.Invoke(this, new(nameof(Child))); + field = value; + PropertyChanged?.Invoke(this, new(nameof(Child))); + } + } } diff --git a/src/tests/ReactiveUI.Binding.GeneratedCode.Tests/WhenAnyValue/WhenAnyValueEdgeCaseTests.cs b/src/tests/ReactiveUI.Binding.GeneratedCode.Tests/WhenAnyValue/WhenAnyValueEdgeCaseTests.cs index ac0cfa17..8b419874 100644 --- a/src/tests/ReactiveUI.Binding.GeneratedCode.Tests/WhenAnyValue/WhenAnyValueEdgeCaseTests.cs +++ b/src/tests/ReactiveUI.Binding.GeneratedCode.Tests/WhenAnyValue/WhenAnyValueEdgeCaseTests.cs @@ -2,6 +2,7 @@ // 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.Fallback; using ReactiveUI.Binding.GeneratedCode.TestModels.Scenarios; using ReactiveUI.Binding.GeneratedCode.TestModels.TestModels; @@ -34,6 +35,12 @@ public class WhenAnyValueEdgeCaseTests /// The value written to a property to trigger a change notification. private const string ChangedValue = "Changed"; + /// The initial child name used in nullable deep-chain tests. + private const string Alice = "Alice"; + + /// The replacement child name used in nullable deep-chain tests. + private const string Charlie = "Charlie"; + /// Verifies that disposing the WhenAnyValue subscription stops listening for changes. /// A task representing the asynchronous test operation. [Test] @@ -134,6 +141,114 @@ public async Task DeepChain_IntermediateObjectReplacement() await Assert.That(values).Contains("Eugene"); } + /// + /// Verifies that a nullable deep chain preserves legacy ReactiveUI WhenAnyValue semantics. + /// A missing intermediate suppresses emission, and replacing it reattaches the observation. + /// + /// A task representing the asynchronous test operation. + [Test] + public async Task DeepChain_NullIntermediate_MatchesLegacyReactiveUISemantics() + { + var generatedHost = new HostTestFixture { Child = null }; + var fallbackHost = new HostTestFixture { Child = null }; + var generatedValues = new List(); + var fallbackValues = new List(); + + using var generatedSub = WhenAnyValueScenarios.DeepChain_ChildName(generatedHost) + .Subscribe(generatedValues.Add); + using var fallbackSub = RuntimeObservationFallback + .WhenAnyValue(fallbackHost, x => x.Child!.Name) + .Subscribe(fallbackValues.Add); + + generatedHost.Child = new() { Name = Alice }; + fallbackHost.Child = new() { Name = Alice }; + generatedHost.Child.Name = "Bob"; + fallbackHost.Child.Name = "Bob"; + + generatedHost.Child = null; + fallbackHost.Child = null; + + generatedHost.Child = new() { Name = Charlie }; + fallbackHost.Child = new() { Name = Charlie }; + + var generatedTrace = string.Join('|', generatedValues); + var fallbackTrace = string.Join('|', fallbackValues); + await Assert.That(generatedTrace).IsEqualTo(fallbackTrace); + await Assert.That(generatedTrace).IsEqualTo("Alice|Bob|Charlie"); + } + + /// + /// Verifies that breaking a three-link chain detaches the generated and legacy observers from + /// the orphaned subtree, while repairing the chain reattaches both implementations. + /// + /// A task representing the asynchronous test operation. + [Test] + public async Task DeepChain_ThreeLinks_DetachesOrphanedSubtreeLikeLegacyReactiveUI() + { + var generatedMiddle = new TestViewModel { Child = new() { Name = Alice } }; + var fallbackMiddle = new TestViewModel { Child = new() { Name = Alice } }; + var generatedHost = new HostTestFixture { Child = generatedMiddle }; + var fallbackHost = new HostTestFixture { Child = fallbackMiddle }; + var generatedValues = new List(); + var fallbackValues = new List(); + + using var generatedSub = WhenAnyValueScenarios.DeepChain_GrandchildName(generatedHost) + .Subscribe(generatedValues.Add); + using var fallbackSub = RuntimeObservationFallback + .WhenAnyValue(fallbackHost, x => x.Child!.Child!.Name) + .Subscribe(fallbackValues.Add); + + generatedHost.Child = null; + fallbackHost.Child = null; + + generatedMiddle.Child!.Name = "Orphan mutation"; + fallbackMiddle.Child!.Name = "Orphan mutation"; + + generatedHost.Child = new() { Child = new() { Name = Charlie } }; + fallbackHost.Child = new() { Child = new() { Name = Charlie } }; + + var generatedTrace = string.Join('|', generatedValues); + var fallbackTrace = string.Join('|', fallbackValues); + await Assert.That(generatedTrace).IsEqualTo(fallbackTrace); + await Assert.That(generatedTrace).IsEqualTo("Alice|Charlie"); + } + + /// Verifies that a null leaf is still emitted when every intermediate object exists. + /// A task representing the asynchronous test operation. + [Test] + public async Task DeepChain_NullLeaf_StillEmits() + { + var host = new HostTestFixture { Child = new() { Name = Alice } }; + var values = new List(); + + using var sub = WhenAnyValueScenarios.DeepChain_ChildName(host) + .Subscribe(values.Add); + + host.Child.Name = null!; + + await Assert.That(values.Count).IsEqualTo(ExpectedEmissionCount); + await Assert.That(values[1]).IsNull(); + } + + /// Verifies that a missing intermediate is distinct from a legitimate default-valued leaf. + /// A task representing the asynchronous test operation. + [Test] + public async Task DeepChain_DefaultValueLeaf_EmitsOnlyWhenIntermediateExists() + { + var host = new HostTestFixture { Child = null }; + var values = new List(); + + using var sub = WhenAnyValueScenarios.DeepChain_ChildAge(host) + .Subscribe(values.Add); + + await Assert.That(values.Count).IsEqualTo(0); + + host.Child = new() { Age = 0 }; + + await Assert.That(values.Count).IsEqualTo(1); + await Assert.That(values[0]).IsEqualTo(0); + } + /// Verifies that WhenAnyValue with selector re-emits when properties change. /// A task representing the asynchronous test operation. [Test] diff --git a/src/tests/ReactiveUI.Binding.GeneratedCode.Tests/WhenChanged/WhenChangedEdgeCaseTests.cs b/src/tests/ReactiveUI.Binding.GeneratedCode.Tests/WhenChanged/WhenChangedEdgeCaseTests.cs index 1a13bff0..9bfeb7d1 100644 --- a/src/tests/ReactiveUI.Binding.GeneratedCode.Tests/WhenChanged/WhenChangedEdgeCaseTests.cs +++ b/src/tests/ReactiveUI.Binding.GeneratedCode.Tests/WhenChanged/WhenChangedEdgeCaseTests.cs @@ -331,10 +331,10 @@ public async Task DeepChain_NullForgiving_EmitsWhenChildSet() await Assert.That(values).Contains("Bob"); } - /// Verifies that deep chain with null-forgiving operator emits default when the intermediate object is null. + /// Verifies that deep chain with null-forgiving operator does not emit when the intermediate object is null. /// A task representing the asynchronous test operation. [Test] - public async Task DeepChain_NullForgiving_EmitsDefaultWhenChildNull() + public async Task DeepChain_NullForgiving_DoesNotEmitWhenChildNull() { var host = new HostTestFixture { Child = null }; var values = new List(); @@ -342,8 +342,7 @@ public async Task DeepChain_NullForgiving_EmitsDefaultWhenChildNull() using var sub = WhenChangedScenarios.DeepChain_ChildName(host) .Subscribe(values.Add); - // When Child is null, should emit default (null for string) - await Assert.That(values.Count).IsGreaterThanOrEqualTo(1); + await Assert.That(values.Count).IsEqualTo(0); } /// Verifies that deep chain with null-forgiving operator re-subscribes when the intermediate object is replaced. diff --git a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/Plugins/ObservationPluginTests.cs b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/Plugins/ObservationPluginTests.cs index 713700c9..6f19d54b 100644 --- a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/Plugins/ObservationPluginTests.cs +++ b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/Plugins/ObservationPluginTests.cs @@ -177,7 +177,7 @@ public async Task WpfPlugin_EmitDeepChainInnerSegment_AfterChange_EmitsEventObse var sb = new StringBuilder(); var segment = ModelFactory.CreatePropertyPathSegment("City", StringName, AddressTypeName); - plugin.EmitDeepChainInnerSegment(sb, Obs0Local, Obs1Local, "__p1", segment, false); + plugin.EmitDeepChainInnerSegment(sb, Obs0Local, Obs1Local, "__p1", segment, false, NullParentObservationBehavior.SuppressEmission); var result = sb.ToString(); await Assert.That(result).Contains(EventObservableName); @@ -193,7 +193,7 @@ public async Task WpfPlugin_EmitDeepChainInnerSegment_BeforeChange_EmitsReturnOb var sb = new StringBuilder(); var segment = ModelFactory.CreatePropertyPathSegment("City", StringName, AddressTypeName); - plugin.EmitDeepChainInnerSegment(sb, Obs0Local, Obs1Local, "__p1", segment, true); + plugin.EmitDeepChainInnerSegment(sb, Obs0Local, Obs1Local, "__p1", segment, true, NullParentObservationBehavior.SuppressEmission); await Assert.That(sb.ToString()).Contains(ReturnObservableName); } @@ -336,7 +336,7 @@ public async Task WinFormsPlugin_EmitDeepChainInnerSegment_AfterChange_EmitsEven var sb = new StringBuilder(); var segment = ModelFactory.CreatePropertyPathSegment("Text", StringName, InnerTypeName); - plugin.EmitDeepChainInnerSegment(sb, Obs0Local, Obs1Local, "__p1", segment, false); + plugin.EmitDeepChainInnerSegment(sb, Obs0Local, Obs1Local, "__p1", segment, false, NullParentObservationBehavior.SuppressEmission); var result = sb.ToString(); await Assert.That(result).Contains(EventObservableName); @@ -352,7 +352,7 @@ public async Task WinFormsPlugin_EmitDeepChainInnerSegment_BeforeChange_EmitsRet var sb = new StringBuilder(); var segment = ModelFactory.CreatePropertyPathSegment("Text", StringName, InnerTypeName); - plugin.EmitDeepChainInnerSegment(sb, Obs0Local, Obs1Local, "__p1", segment, true); + plugin.EmitDeepChainInnerSegment(sb, Obs0Local, Obs1Local, "__p1", segment, true, NullParentObservationBehavior.SuppressEmission); await Assert.That(sb.ToString()).Contains(ReturnObservableName); } @@ -468,7 +468,7 @@ public async Task WinUIPlugin_EmitDeepChainInnerSegment_AfterChange_EmitsWinUIDP var sb = new StringBuilder(); var segment = ModelFactory.CreatePropertyPathSegment("Text", StringName, InnerTypeName); - plugin.EmitDeepChainInnerSegment(sb, Obs0Local, Obs1Local, "__p1", segment, false); + plugin.EmitDeepChainInnerSegment(sb, Obs0Local, Obs1Local, "__p1", segment, false, NullParentObservationBehavior.SuppressEmission); var result = sb.ToString(); await Assert.That(result).Contains(WinUIDPObservableLocal); @@ -484,7 +484,7 @@ public async Task WinUIPlugin_EmitDeepChainInnerSegment_BeforeChange_EmitsReturn var sb = new StringBuilder(); var segment = ModelFactory.CreatePropertyPathSegment("Text", StringName, InnerTypeName); - plugin.EmitDeepChainInnerSegment(sb, Obs0Local, Obs1Local, "__p1", segment, true); + plugin.EmitDeepChainInnerSegment(sb, Obs0Local, Obs1Local, "__p1", segment, true, NullParentObservationBehavior.SuppressEmission); await Assert.That(sb.ToString()).Contains(ReturnObservableName); } @@ -594,7 +594,7 @@ public async Task KVOPlugin_EmitDeepChainInnerSegment_AfterChange_EmitsKVOObserv var sb = new StringBuilder(); var segment = ModelFactory.CreatePropertyPathSegment("City", StringName, AddressTypeName); - plugin.EmitDeepChainInnerSegment(sb, Obs0Local, Obs1Local, "__p1", segment, false); + plugin.EmitDeepChainInnerSegment(sb, Obs0Local, Obs1Local, "__p1", segment, false, NullParentObservationBehavior.SuppressEmission); var result = sb.ToString(); await Assert.That(result).Contains(KVOObservableLocal); @@ -610,7 +610,7 @@ public async Task KVOPlugin_EmitDeepChainInnerSegment_BeforeChange_EmitsKVOObser var sb = new StringBuilder(); var segment = ModelFactory.CreatePropertyPathSegment("City", StringName, AddressTypeName); - plugin.EmitDeepChainInnerSegment(sb, Obs0Local, Obs1Local, "__p1", segment, true); + plugin.EmitDeepChainInnerSegment(sb, Obs0Local, Obs1Local, "__p1", segment, true, NullParentObservationBehavior.SuppressEmission); var result = sb.ToString(); await Assert.That(result).Contains(KVOObservableLocal); @@ -749,7 +749,7 @@ public async Task AndroidPlugin_EmitDeepChainInnerSegment_EmitsReturnObservable( var sb = new StringBuilder(); var segment = ModelFactory.CreatePropertyPathSegment("City", StringName, AddressTypeName); - plugin.EmitDeepChainInnerSegment(sb, Obs0Local, Obs1Local, "__p1", segment, false); + plugin.EmitDeepChainInnerSegment(sb, Obs0Local, Obs1Local, "__p1", segment, false, NullParentObservationBehavior.SuppressEmission); var result = sb.ToString(); await Assert.That(result).Contains(ReturnObservableName); diff --git a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WAG.MP_DC#WhenAnyDispatch.g.verified.cs b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WAG.MP_DC#WhenAnyDispatch.g.verified.cs index c48ebdfe..759d8c66 100644 --- a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WAG.MP_DC#WhenAnyDispatch.g.verified.cs +++ b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WAG.MP_DC#WhenAnyDispatch.g.verified.cs @@ -48,7 +48,7 @@ internal static partial class __ReactiveUIGeneratedBindings "Name", (global::System.ComponentModel.INotifyPropertyChanged __o) => ((global::SharedScenarios.WhenAny.MultiPropertyDeepChain.ChildModel)__o).Name, false) - : (global::System.IObservable)new global::ReactiveUI.Binding.Observables.ReturnObservable(default(string)))); + : (global::System.IObservable)global::ReactiveUI.Binding.Observables.EmptyObservable.Instance)); var __propObs0 = global::ReactiveUI.Binding.Observables.RxBindingExtensions.DistinctUntilChanged(__propObs0_s1); diff --git a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WAG.SP_DC#WhenAnyDispatch.g.verified.cs b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WAG.SP_DC#WhenAnyDispatch.g.verified.cs index 00b55946..882a6b9f 100644 --- a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WAG.SP_DC#WhenAnyDispatch.g.verified.cs +++ b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WAG.SP_DC#WhenAnyDispatch.g.verified.cs @@ -45,7 +45,7 @@ internal static partial class __ReactiveUIGeneratedBindings "Name", (global::System.ComponentModel.INotifyPropertyChanged __o) => ((global::SharedScenarios.WhenAny.DeepPropertyChain.ChildModel)__o).Name, false) - : (global::System.IObservable)new global::ReactiveUI.Binding.Observables.ReturnObservable(default(string)))); + : (global::System.IObservable)global::ReactiveUI.Binding.Observables.EmptyObservable.Instance)); var __propObs0 = global::ReactiveUI.Binding.Observables.RxBindingExtensions.DistinctUntilChanged(__propObs0_s1); diff --git a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WAOG.2O_DC_CL#WhenAnyObservableDispatch.g.verified.cs b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WAOG.2O_DC_CL#WhenAnyObservableDispatch.g.verified.cs index f3317f3f..46799d8c 100644 --- a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WAOG.2O_DC_CL#WhenAnyObservableDispatch.g.verified.cs +++ b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WAOG.2O_DC_CL#WhenAnyObservableDispatch.g.verified.cs @@ -48,7 +48,7 @@ internal static partial class __ReactiveUIGeneratedBindings "Count", (global::System.ComponentModel.INotifyPropertyChanged __o) => ((global::SharedScenarios.WhenAnyObservable.DeepObservableCombineLatest.ChildModel)__o).Count, false) - : (global::System.IObservable>)new global::ReactiveUI.Binding.Observables.ReturnObservable>(default(global::System.IObservable)))); + : (global::System.IObservable>)global::ReactiveUI.Binding.Observables.EmptyObservable>.Instance)); var __obsProperty0 = global::ReactiveUI.Binding.Observables.RxBindingExtensions.DistinctUntilChanged(__obsProperty0_s1); @@ -70,7 +70,7 @@ internal static partial class __ReactiveUIGeneratedBindings "Message", (global::System.ComponentModel.INotifyPropertyChanged __o) => ((global::SharedScenarios.WhenAnyObservable.DeepObservableCombineLatest.ChildModel)__o).Message, false) - : (global::System.IObservable>)new global::ReactiveUI.Binding.Observables.ReturnObservable>(default(global::System.IObservable)))); + : (global::System.IObservable>)global::ReactiveUI.Binding.Observables.EmptyObservable>.Instance)); var __obsProperty1 = global::ReactiveUI.Binding.Observables.RxBindingExtensions.DistinctUntilChanged(__obsProperty1_s1); diff --git a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WAOG.2O_DC_Merge#WhenAnyObservableDispatch.g.verified.cs b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WAOG.2O_DC_Merge#WhenAnyObservableDispatch.g.verified.cs index ebc75b82..2f32a360 100644 --- a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WAOG.2O_DC_Merge#WhenAnyObservableDispatch.g.verified.cs +++ b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WAOG.2O_DC_Merge#WhenAnyObservableDispatch.g.verified.cs @@ -47,7 +47,7 @@ internal static partial class __ReactiveUIGeneratedBindings "Command1", (global::System.ComponentModel.INotifyPropertyChanged __o) => ((global::SharedScenarios.WhenAnyObservable.DeepObservableMerge.ChildModel)__o).Command1, false) - : (global::System.IObservable>)new global::ReactiveUI.Binding.Observables.ReturnObservable>(default(global::System.IObservable)))); + : (global::System.IObservable>)global::ReactiveUI.Binding.Observables.EmptyObservable>.Instance)); var __obsProperty0 = global::ReactiveUI.Binding.Observables.RxBindingExtensions.DistinctUntilChanged(__obsProperty0_s1); @@ -69,7 +69,7 @@ internal static partial class __ReactiveUIGeneratedBindings "Command2", (global::System.ComponentModel.INotifyPropertyChanged __o) => ((global::SharedScenarios.WhenAnyObservable.DeepObservableMerge.ChildModel)__o).Command2, false) - : (global::System.IObservable>)new global::ReactiveUI.Binding.Observables.ReturnObservable>(default(global::System.IObservable)))); + : (global::System.IObservable>)global::ReactiveUI.Binding.Observables.EmptyObservable>.Instance)); var __obsProperty1 = global::ReactiveUI.Binding.Observables.RxBindingExtensions.DistinctUntilChanged(__obsProperty1_s1); diff --git a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WAOG.SingleObservable_DC#WhenAnyObservableDispatch.g.verified.cs b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WAOG.SingleObservable_DC#WhenAnyObservableDispatch.g.verified.cs index 284ea224..84c89866 100644 --- a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WAOG.SingleObservable_DC#WhenAnyObservableDispatch.g.verified.cs +++ b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WAOG.SingleObservable_DC#WhenAnyObservableDispatch.g.verified.cs @@ -44,7 +44,7 @@ internal static partial class __ReactiveUIGeneratedBindings "MyCommand", (global::System.ComponentModel.INotifyPropertyChanged __o) => ((global::SharedScenarios.WhenAnyObservable.DeepObservableSwitch.ChildModel)__o).MyCommand, false) - : (global::System.IObservable>)new global::ReactiveUI.Binding.Observables.ReturnObservable>(default(global::System.IObservable)))); + : (global::System.IObservable>)global::ReactiveUI.Binding.Observables.EmptyObservable>.Instance)); var __obsProperty = global::ReactiveUI.Binding.Observables.RxBindingExtensions.DistinctUntilChanged(__obsProperty_s1); diff --git a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WAVG.DPC#WhenAnyValueDispatch.g.verified.cs b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WAVG.DPC#WhenAnyValueDispatch.g.verified.cs index 054f290d..359c2727 100644 --- a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WAVG.DPC#WhenAnyValueDispatch.g.verified.cs +++ b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WAVG.DPC#WhenAnyValueDispatch.g.verified.cs @@ -50,7 +50,7 @@ internal static partial class __ReactiveUIGeneratedBindings "Name", (global::System.ComponentModel.INotifyPropertyChanged __o) => ((global::SharedScenarios.WhenAnyValue.DeepPropertyChain.ChildModel)__o).Name, false) - : (global::System.IObservable)new global::ReactiveUI.Binding.Observables.ReturnObservable(default(string)))); + : (global::System.IObservable)global::ReactiveUI.Binding.Observables.EmptyObservable.Instance)); return global::ReactiveUI.Binding.Observables.RxBindingExtensions.DistinctUntilChanged(__obs1); } diff --git a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCG.4LDC#WhenChangedDispatch.g.verified.cs b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCG.4LDC#WhenChangedDispatch.g.verified.cs index c338336d..4f9c940f 100644 --- a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCG.4LDC#WhenChangedDispatch.g.verified.cs +++ b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCG.4LDC#WhenChangedDispatch.g.verified.cs @@ -70,7 +70,7 @@ internal static partial class __ReactiveUIGeneratedBindings "Value", (global::System.ComponentModel.INotifyPropertyChanged __o) => ((global::SharedScenarios.WhenChanged.FourLevelDeepChain.Model)__o).Value, false) - : (global::System.IObservable)new global::ReactiveUI.Binding.Observables.ReturnObservable(default(string)))); + : (global::System.IObservable)global::ReactiveUI.Binding.Observables.EmptyObservable.Instance)); return global::ReactiveUI.Binding.Observables.RxBindingExtensions.DistinctUntilChanged(__obs3); } diff --git a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCG.DPC#WhenChangedDispatch.g.verified.cs b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCG.DPC#WhenChangedDispatch.g.verified.cs index 23d3d47d..be09522f 100644 --- a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCG.DPC#WhenChangedDispatch.g.verified.cs +++ b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCG.DPC#WhenChangedDispatch.g.verified.cs @@ -50,7 +50,7 @@ internal static partial class __ReactiveUIGeneratedBindings "Name", (global::System.ComponentModel.INotifyPropertyChanged __o) => ((global::SharedScenarios.WhenChanged.DeepPropertyChain.ChildModel)__o).Name, false) - : (global::System.IObservable)new global::ReactiveUI.Binding.Observables.ReturnObservable(default(string)))); + : (global::System.IObservable)global::ReactiveUI.Binding.Observables.EmptyObservable.Instance)); return global::ReactiveUI.Binding.Observables.RxBindingExtensions.DistinctUntilChanged(__obs1); } diff --git a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCG.MP_WDC#WhenChangedDispatch.g.verified.cs b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCG.MP_WDC#WhenChangedDispatch.g.verified.cs index 45ce9aef..c01547d5 100644 --- a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCG.MP_WDC#WhenChangedDispatch.g.verified.cs +++ b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCG.MP_WDC#WhenChangedDispatch.g.verified.cs @@ -56,7 +56,7 @@ internal static partial class __ReactiveUIGeneratedBindings "City", (global::System.ComponentModel.INotifyPropertyChanged __o) => ((global::SharedScenarios.WhenChanged.MultiPropertyWithDeepChains.AddressModel)__o).City, false) - : (global::System.IObservable)new global::ReactiveUI.Binding.Observables.ReturnObservable(default(string)))); + : (global::System.IObservable)global::ReactiveUI.Binding.Observables.EmptyObservable.Instance)); var __propObs0 = global::ReactiveUI.Binding.Observables.RxBindingExtensions.DistinctUntilChanged(__propObs0_s1); diff --git a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCG.NullForgivingDC#WhenChangedDispatch.g.verified.cs b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCG.NullForgivingDC#WhenChangedDispatch.g.verified.cs index 5aa1a78d..a768c7d2 100644 --- a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCG.NullForgivingDC#WhenChangedDispatch.g.verified.cs +++ b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCG.NullForgivingDC#WhenChangedDispatch.g.verified.cs @@ -50,7 +50,7 @@ internal static partial class __ReactiveUIGeneratedBindings "Name", (global::System.ComponentModel.INotifyPropertyChanged __o) => ((global::SharedScenarios.WhenChanged.NullForgivingDeepChain.ChildModel)__o).Name, false) - : (global::System.IObservable)new global::ReactiveUI.Binding.Observables.ReturnObservable(default(string)))); + : (global::System.IObservable)global::ReactiveUI.Binding.Observables.EmptyObservable.Instance)); return global::ReactiveUI.Binding.Observables.RxBindingExtensions.DistinctUntilChanged(__obs1); } diff --git a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCnG.4LDC#WhenChangingDispatch.g.verified.cs b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCnG.4LDC#WhenChangingDispatch.g.verified.cs index 163649ba..009c09d3 100644 --- a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCnG.4LDC#WhenChangingDispatch.g.verified.cs +++ b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCnG.4LDC#WhenChangingDispatch.g.verified.cs @@ -66,7 +66,7 @@ internal static partial class __ReactiveUIGeneratedBindings (global::System.ComponentModel.INotifyPropertyChanging)__parent3, "Value", (global::System.ComponentModel.INotifyPropertyChanging __o) => ((global::SharedScenarios.WhenChanging.FourLevelDeepChain.Model)__o).Value) - : (global::System.IObservable)new global::ReactiveUI.Binding.Observables.ReturnObservable(default(string)))); + : (global::System.IObservable)global::ReactiveUI.Binding.Observables.EmptyObservable.Instance)); return __obs3; } diff --git a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCnG.4LDC_CFP#WhenChangingDispatch.g.verified.cs b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCnG.4LDC_CFP#WhenChangingDispatch.g.verified.cs index 44aa7091..81681049 100644 --- a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCnG.4LDC_CFP#WhenChangingDispatch.g.verified.cs +++ b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCnG.4LDC_CFP#WhenChangingDispatch.g.verified.cs @@ -64,7 +64,7 @@ internal static partial class __ReactiveUIGeneratedBindings (global::System.ComponentModel.INotifyPropertyChanging)__parent3, "Value", (global::System.ComponentModel.INotifyPropertyChanging __o) => ((global::SharedScenarios.WhenChanging.FourLevelDeepChain.Model)__o).Value) - : (global::System.IObservable)new global::ReactiveUI.Binding.Observables.ReturnObservable(default(string)))); + : (global::System.IObservable)global::ReactiveUI.Binding.Observables.EmptyObservable.Instance)); return __obs3; } diff --git a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCnG.DPC#WhenChangingDispatch.g.verified.cs b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCnG.DPC#WhenChangingDispatch.g.verified.cs index c9a98d60..e7c105f1 100644 --- a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCnG.DPC#WhenChangingDispatch.g.verified.cs +++ b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCnG.DPC#WhenChangingDispatch.g.verified.cs @@ -48,7 +48,7 @@ internal static partial class __ReactiveUIGeneratedBindings (global::System.ComponentModel.INotifyPropertyChanging)__parent1, "Name", (global::System.ComponentModel.INotifyPropertyChanging __o) => ((global::SharedScenarios.WhenChanging.DeepPropertyChain.ChildModel)__o).Name) - : (global::System.IObservable)new global::ReactiveUI.Binding.Observables.ReturnObservable(default(string)))); + : (global::System.IObservable)global::ReactiveUI.Binding.Observables.EmptyObservable.Instance)); return __obs1; } diff --git a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCnG.DPC_CFP#WhenChangingDispatch.g.verified.cs b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCnG.DPC_CFP#WhenChangingDispatch.g.verified.cs index 2ca1f9bf..c85826cb 100644 --- a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCnG.DPC_CFP#WhenChangingDispatch.g.verified.cs +++ b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCnG.DPC_CFP#WhenChangingDispatch.g.verified.cs @@ -46,7 +46,7 @@ internal static partial class __ReactiveUIGeneratedBindings (global::System.ComponentModel.INotifyPropertyChanging)__parent1, "Name", (global::System.ComponentModel.INotifyPropertyChanging __o) => ((global::SharedScenarios.WhenChanging.DeepPropertyChain.ChildModel)__o).Name) - : (global::System.IObservable)new global::ReactiveUI.Binding.Observables.ReturnObservable(default(string)))); + : (global::System.IObservable)global::ReactiveUI.Binding.Observables.EmptyObservable.Instance)); return __obs1; } diff --git a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCnG.MP_WDC#WhenChangingDispatch.g.verified.cs b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCnG.MP_WDC#WhenChangingDispatch.g.verified.cs index 2b60b8d8..fe6b206c 100644 --- a/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCnG.MP_WDC#WhenChangingDispatch.g.verified.cs +++ b/src/tests/ReactiveUI.Binding.SourceGenerators.Tests/WCnG.MP_WDC#WhenChangingDispatch.g.verified.cs @@ -54,7 +54,7 @@ internal static partial class __ReactiveUIGeneratedBindings (global::System.ComponentModel.INotifyPropertyChanging)__propObs0_p1, "City", (global::System.ComponentModel.INotifyPropertyChanging __o) => ((global::SharedScenarios.WhenChanging.MultiPropertyWithDeepChains.AddressModel)__o).City) - : (global::System.IObservable)new global::ReactiveUI.Binding.Observables.ReturnObservable(default(string)))); + : (global::System.IObservable)global::ReactiveUI.Binding.Observables.EmptyObservable.Instance)); var __propObs0 = __propObs0_s1;