Skip to content

Add NotifyPropertyChangeBehavior - #282

Merged
kzu merged 4 commits into
mainfrom
notify-property-change-270
Oct 7, 2026
Merged

kzu merged 4 commits into
mainfrom
notify-property-change-270

Conversation

@kzu

@kzu kzu commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Fixes #270.

Design

Adds a single NotifyPropertyChangeBehavior that raises INotifyPropertyChanging.PropertyChanging before a property setter runs and INotifyPropertyChanged.PropertyChanged after a successful set.

Interception

The behavior intercepts set_* accessors plus the add_/remove_ accessors of both notify events. Generated interface event accessors are empty (MemberScaffold.Event emits 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"]
Loading

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. Passing true opts 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 both NotifyPropertyChangeBehavior() and NotifyPropertyChangeBehavior(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"]
Loading

Subscriber lists and last-written values live in instance fields. AddBehavior<NotifyPropertyChangeBehavior>() (#281) gives every built stunt its own instance; the behavior also implements ICloneable (the clone carries over shortCircuitUnchanged), 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 PropertyChanging handler prevents the set; a throwing PropertyChanged handler propagates after the value was recorded.

Registration

var builder = Stunt.Builder();
builder.AddBehavior<NotifyPropertyChangeBehavior>(); // per-stunt instance
builder.AddBehavior(new DefaultValueBehavior());

IPerson person = builder.Build<IPerson, INotifyPropertyChanged, INotifyPropertyChanging>();

// Opt into setter short-circuiting:
builder.AddBehavior(() => new NotifyPropertyChangeBehavior(shortCircuitUnchanged: true));

Tests

Scenario tests in src/Stunts.UnitTests/Scenarios/NotifyPropertyChange.cs cover: event order and property names, setter-run counting, default vs short-circuit unchanged semantics, throwing setters (no Changed, value not recorded), per-stunt state isolation via StuntBuilder, and unsubscribe.

…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.
@kzu kzu added the enhancement New feature or request label Oct 7, 2026
@kzu
kzu merged commit c5da845 into main Oct 7, 2026
6 checks passed
@kzu
kzu deleted the notify-property-change-270 branch October 7, 2026 23:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add property change notification behaviors

1 participant