Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,10 @@ internal static void EmitInlineObservation(
/// for inner segments, so reusing the root's plugin is safe whatever the segment declares.
/// </param>
/// <param name="isBeforeChange">Whether before-change notifications are being observed.</param>
/// <remarks>
/// Missing parents propagate through intermediate stages so downstream subscriptions detach,
/// while the leaf suppresses the default value to match the runtime expression-chain fallback.
/// </remarks>
private static void EmitObservationChainInnerSegments(
StringBuilder sb,
EquatableArray<PropertyPathSegment> path,
Expand All @@ -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)
{
Expand All @@ -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
Expand All @@ -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}));
""");
}
}
Expand All @@ -779,6 +801,10 @@ private static void EmitObservationChainInnerSegments(
/// </param>
/// <param name="isBeforeChange">Whether before-change notifications are being observed.</param>
/// <param name="varName">The variable-name prefix for the emitted stages.</param>
/// <remarks>
/// Missing parents propagate through intermediate stages so downstream subscriptions detach,
/// while the leaf suppresses the default value to match the runtime expression-chain fallback.
/// </remarks>
private static void EmitDeepChainInnerSegments(
StringBuilder sb,
EquatableArray<PropertyPathSegment> path,
Expand All @@ -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)
{
Expand All @@ -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
Expand All @@ -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}));
""");
}
}
Expand All @@ -836,6 +878,7 @@ private static void EmitDeepChainInnerSegments(
/// <param name="classInfo">The root type's binding info, when known.</param>
/// <param name="plugin">The observation plugin for the root type, when one matched.</param>
/// <param name="variableName">The name of the variable the chain result is assigned to.</param>
/// <remarks>Missing parents emit defaults here so binding consumers can clear their targets.</remarks>
private static void EmitInlineDeepChain(
StringBuilder sb,
string rootVar,
Expand Down Expand Up @@ -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;
}

Expand All @@ -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}))));
""");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,15 @@ void EmitDeepChainRootSegment(
/// <param name="lambdaParam">The lambda parameter name for the parent value.</param>
/// <param name="segment">The current property path segment.</param>
/// <param name="isBeforeChange">True for WhenChanging (before-change).</param>
/// <param name="nullParentBehavior">The behavior to use while the parent segment is null.</param>
void EmitDeepChainInnerSegment(
StringBuilder sb,
string prevVar,
string curVar,
string lambdaParam,
PropertyPathSegment segment,
bool isBeforeChange);
bool isBeforeChange,
NullParentObservationBehavior nullParentBehavior);

/// <summary>Emits an inline observation variable for binding generators. Used by BindOneWay/BindTwoWay for direct observation code.</summary>
/// <param name="sb">The string builder to append to.</param>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>Specifies what a generated deep-chain observer emits while its parent segment is null.</summary>
internal enum NullParentObservationBehavior
{
/// <summary>Suppresses values until the parent becomes non-null.</summary>
SuppressEmission,

/// <summary>Emits the leaf type's default value so binding consumers can clear their target.</summary>
EmitDefault,
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}));
""");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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}));
""");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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($"""
Expand All @@ -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}));
""");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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}));
""");
}

Expand Down
Loading
Loading