From 55f58a500e2db42352d7d899f6ef5f91ba3da2ca Mon Sep 17 00:00:00 2001
From: Glenn Watson <5834289+glennawatson@users.noreply.github.com>
Date: Thu, 1 Oct 2026 15:09:22 +1000
Subject: [PATCH] fix: report a runtime property chain's initial value once
when a link emits on subscribe
- Ignore a link notification raised from inside its own subscribe. The chain reads the same value straight after.
- WhenAnyValueUnsafe, WhenChanged and the Unsafe bindings report one initial value through a property with no change notification.
- BindUnsafe does not write the view's value back to the view model when the binding is created.
Fixes #160
---
.../ExpressionChainSink.cs | 15 +++-
.../Mixins/PocoLinkUnsafeBindingTests.cs | 85 +++++++++++++++++++
.../TestModels/PocoChainRoot.cs | 12 +++
.../WhenAny/ExpressionChainTests.cs | 60 +++++++++++++
4 files changed, 170 insertions(+), 2 deletions(-)
create mode 100644 src/tests/ReactiveUI.Binding.Tests/Mixins/PocoLinkUnsafeBindingTests.cs
create mode 100644 src/tests/ReactiveUI.Binding.Tests/TestModels/PocoChainRoot.cs
diff --git a/src/ReactiveUI.Binding.Shared/ObservableForProperty/ExpressionChainSink.cs b/src/ReactiveUI.Binding.Shared/ObservableForProperty/ExpressionChainSink.cs
index 73d56847..e012499b 100644
--- a/src/ReactiveUI.Binding.Shared/ObservableForProperty/ExpressionChainSink.cs
+++ b/src/ReactiveUI.Binding.Shared/ObservableForProperty/ExpressionChainSink.cs
@@ -305,6 +305,13 @@ private sealed class Level : IDisposable
/// This link's index/argument array (non-null only for indexer links), cached once.
private readonly object?[]? _arguments;
+ /// Whether this level is subscribing to its link's notifications.
+ ///
+ /// Only read under the gate, so only a notification the subscribing thread raises from inside
+ /// Subscribe sees it set.
+ ///
+ private bool _attaching;
+
/// Initializes a new instance of the class.
/// The owning chain sink.
/// This watcher's position in the chain.
@@ -341,10 +348,14 @@ public void SetParent(object? parent)
// Subscribe before reading, so a change between the two is reported rather than lost. The
// caller holds the gate, so a notification that races this window queues behind it and
- // re-reports the value the kicker is about to push; the sink drops that one repeat.
+ // re-reports the value the kicker is about to push; the sink drops that one repeat. A
+ // notification raised from inside Subscribe, such as the one a POCO link emits, is ignored:
+ // the kicker reads the same value straight after.
+ _attaching = true;
_subscription.Disposable = ReactiveNotifyPropertyChangedMixins
.NotifyForProperty(parent, link, _sink._beforeChange, _sink._suppressWarnings)
.Subscribe(new Observer(this));
+ _attaching = false;
Push(ReadValue(parent), fromKicker: true);
}
@@ -359,7 +370,7 @@ private void OnNotification(IObservedChange