Repository navigation
Add NotifyPropertyChangeBehavior - #282
Merged
Merged
Conversation
…ropertyChanged Implements #270: a single behavior that raises PropertyChanging before the setter runs and PropertyChanged after a successful set. Assigning an unchanged value (compared with Equals, no getter invocation: Native AOT-friendly) raises neither event; by default the setter still runs, with opt-in short-circuiting via the shortCircuitUnchanged ctor parameter. The behavior owns event subscription end-to-end (generated interface event accessors are empty) and holds subscriber lists and last-written values in instance fields, so each stunt gets independent state via AddBehavior<T> factories or ICloneable cloning. Thread-safe via a private lock; handlers are invoked outside the lock.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #270.
Design
Adds a single
NotifyPropertyChangeBehaviorthat raisesINotifyPropertyChanging.PropertyChangingbefore a property setter runs andINotifyPropertyChanged.PropertyChangedafter a successful set.Interception
The behavior intercepts
set_*accessors plus theadd_/remove_accessors of both notify events. Generated interface event accessors are empty (MemberScaffold.Eventemits an empty body when there is no default instance), so the behavior owns subscriber storage and event dispatch end-to-end — subscription included.flowchart TD A["set_Name('Ada')"] --> B{value changed?} B -->|No| C{shortCircuitUnchanged?} C -->|Yes| D["return (setter skipped)"] C -->|No| E["invoke next (setter runs, no events)"] B -->|Yes| F["raise PropertyChanging"] F --> G["invoke next (setter runs)"] G --> H{success?} H -->|Yes| I["record value; raise PropertyChanged"] H -->|No| J["no record, no PropertyChanged"]Change detection without reflection
The incoming value is compared against the last value written through the pipeline using
Equals— the property getter is never invoked, keeping the behavior Native AOT-friendly. The first write to a property always notifies.Known limitation: for class stunts whose base implementation mutates the property outside the pipeline, the tracked value can go stale. Documented, not solved — solving it would need pipeline/generator changes.
Unchanged-value semantics
By default (
shortCircuitUnchanged: false) the behavior only adds notifications — assigning an unchanged value raises neither event but the setter still runs, so the behavior introduces no behavioral change. Passingtrueopts into skipping the setter entirely for unchanged values.Note:
AddBehavior<TBehavior>requires a real parameterless constructor (new()constraint; optional parameters don't satisfy it), so the behavior exposes bothNotifyPropertyChangeBehavior()andNotifyPropertyChangeBehavior(bool shortCircuitUnchanged).Per-stunt state
flowchart LR B["StuntBuilder"] --> F["factory per stunt"] F --> P1["stunt 1 : own behavior instance"] F --> P2["stunt 2 : own behavior instance"]Subscriber lists and last-written values live in instance fields.
AddBehavior<NotifyPropertyChangeBehavior>()(#281) gives every built stunt its own instance; the behavior also implementsICloneable(the clone carries overshortCircuitUnchanged), so plain instance registrations are cloned per stunt (#277). State lifetime == behavior instance lifetime == pipeline/stunt lifetime.Thread-safety
All state mutations and snapshots happen under a private lock; handlers are invoked outside the lock on a snapshot, matching standard .NET event semantics: a throwing
PropertyChanginghandler prevents the set; a throwingPropertyChangedhandler propagates after the value was recorded.Registration
Tests
Scenario tests in
src/Stunts.UnitTests/Scenarios/NotifyPropertyChange.cscover: event order and property names, setter-run counting, default vs short-circuit unchanged semantics, throwing setters (noChanged, value not recorded), per-stunt state isolation viaStuntBuilder, and unsubscribe.