Skip to content

feat: add BindCommand and BindInteraction source-generated bindings#13

Merged
glennawatson merged 6 commits into
mainfrom
feature/bind-command-interaction
Mar 8, 2026
Merged

feat: add BindCommand and BindInteraction source-generated bindings#13
glennawatson merged 6 commits into
mainfrom
feature/bind-command-interaction

Conversation

@glennawatson

Copy link
Copy Markdown
Contributor

What's Changed

This PR adds compile-time source generation for BindCommand and BindInteraction, completing the core ReactiveUI binding API surface. Both APIs follow the same CallerFilePath/CallerLineNumber dispatch pattern used by the existing WhenChanged, WhenAnyValue, and Bind APIs.

BindCommand

Wire an ICommand on your ViewModel to a control with a single call — the source generator handles event subscription, command property assignment, and parameter binding at compile time.

// Basic — subscribes to the control's default event (e.g. Click) and invokes the command
this.BindCommand(ViewModel, vm => vm.SaveCommand, v => v.SaveButton)
    .DisposeWith(disposables);

// Custom event — bind to a specific event instead of the default
this.BindCommand(ViewModel, vm => vm.SaveCommand, v => v.SaveButton, "MouseDown")
    .DisposeWith(disposables);

// With command parameter from a property expression
this.BindCommand(ViewModel, vm => vm.DeleteCommand, v => v.DeleteButton, vm => vm.SelectedItem)
    .DisposeWith(disposables);

// With command parameter from an observable
this.BindCommand(ViewModel, vm => vm.DeleteCommand, v => v.DeleteButton, SelectedItems.CountChanged)
    .DisposeWith(disposables);

How it works under the hood:

The generator inspects the target control type at compile time and selects the best binding strategy:

  1. Command property binding — If the control has a settable ICommand Command property (WPF ButtonBase, MAUI Button, etc.), the generator assigns the command directly and optionally sets CommandParameter. This is the most efficient path.

  2. Event + Enabled binding — If the control has a default event (like Click) and a settable bool Enabled property (WinForms Button, etc.), the generator subscribes to the event, invokes ICommand.Execute(), and synchronizes Enabled with ICommand.CanExecute().

  3. Event-only binding — If the control has a default event but no Enabled property, the generator subscribes to the event and invokes the command.

  4. Runtime fallback — If no event or command property is detected at compile time (custom controls, third-party libraries), the generated code delegates to CommandBinderService, which resolves an ICreatesCommandBinding plugin at runtime. This preserves compatibility with platform-specific controls that the generator can't introspect.

BindInteraction

Type-safe interaction handler registration, generated at compile time:

// Register a handler using a Task-returning delegate
this.BindInteraction(ViewModel, vm => vm.ConfirmDelete, async context =>
{
    var result = await ShowDialog("Are you sure?");
    context.SetOutput(result);
})
.DisposeWith(disposables);

// Register a handler using an Observable-returning delegate
this.BindInteraction(ViewModel, vm => vm.ShowError, context =>
{
    ShowNotification(context.Input);
    context.SetOutput(Unit.Default);
    return Observable.Return(Unit.Default);
})
.DisposeWith(disposables);

The generator resolves the Interaction<TInput, TOutput> property type at compile time and wires the handler with full generic type safety — no runtime type casting.

Runtime Library Additions

  • Interaction<TInput, TOutput> — the interaction type with Handle() method and handler registration
  • InteractionContext<TInput, TOutput> — carries input to handlers, collects output via SetOutput()
  • ICreatesCommandBinding — plugin interface for runtime command binding resolution
  • CommandBinderService — resolves the best ICreatesCommandBinding for a given control type
  • SerialDisposable — replaces previous subscription on re-binding (used in generated code)

Source Generator Internals

Restructured Helpers

Broke apart the monolithic MetadataExtractor into focused, single-responsibility helpers under Helpers/:

Helper Responsibility
ObservationExtractor WhenChanged, WhenChanging, WhenAnyValue, WhenAny extraction
WhenAnyObservableExtractor WhenAnyObservable extraction
BindingExtractor Bind, BindOneWay, BindTwoWay, OneWayBind extraction
CommandExtractor BindCommand extraction with control introspection
InteractionExtractor BindInteraction extraction
TypeDetectionExtractor Pipeline A type detection (INPC, ReactiveObject, etc.)
ExtractorValidation Shared guard clauses and validation methods
SyntaxHelpers Lambda parsing and property path extraction
SymbolHelpers Type introspection utilities
EventHelpers Event/delegate type resolution
InvalidOperationExceptionHelper Defensive guards for impossible null states

New Code Generators

  • BindCommandCodeGenerator — generates command binding dispatch with plugin selection
  • BindInteractionCodeGenerator — generates interaction handler wiring

Observation generators moved

Moved platform-specific observation generators to Generators/Observation/ subfolder for clearer organization.

Eliminated phantom IL branches

Extracted inline pattern matching from invocation generator predicates into named RoslynHelpers methods, eliminating 18 phantom uncovered branches that were artifacts of the C# compiler's IL generation.


How We Tested

  • 100% line, branch, and method coverage across all 4 production assemblies (5585 lines, 2108 branches, 677 methods)
  • 6981 tests passing across net8.0, net9.0, and net10.0
  • Added NSubstitute dependency for mocking Roslyn interfaces (IMethodSymbol, IParameterSymbol, INamedTypeSymbol) in unit tests
  • New unit test classes for extracted helpers: ExtractorValidationTests, CommandExtractorHelperTests, SymbolHelpersTests, SyntaxHelpersTests, EventHelpersTests, TypeDetectionExtractorTests, RoslynHelpersTests, AnalyzerHelpersTests
  • Snapshot tests for all BindCommand scenarios (basic, custom event, command property, expression/observable parameter, deep paths, Enabled sync, CFP fallback)
  • Snapshot tests for all BindInteraction scenarios (task handler, observable handler, deep paths, non-INPC viewmodel, CFP fallback)
  • Runtime execution tests verifying generated code compiles and executes correctly

## New Features

Add compile-time source generation for BindCommand and BindInteraction,
completing the core ReactiveUI binding API surface.

**BindCommand** generates optimized command wiring at compile time:
- Detects ICommand/CommandParameter properties on target controls
  (WPF, WinForms, MAUI buttons) and generates direct property assignment
- Falls back to runtime CommandBinderService plugin system when the
  target control lacks standard command properties, preserving
  compatibility with custom or platform-specific controls
- Supports custom event binding via toEvent parameter
- Supports command parameter binding via expression or observable
- Handles Enabled property synchronization for WinForms-style controls
- Deep property path support for nested command properties

**BindInteraction** generates compile-time interaction handler wiring:
- Type-safe handler registration with TInput/TOutput generics
- Proper IDisposable lifecycle management
- CallerFilePath/CallerLineNumber dispatch (same pattern as other APIs)

**Runtime library additions:**
- Interaction<TInput, TOutput> with IInteraction interface
- InteractionContext<TInput, TOutput> for handler communication
- ICreatesCommandBinding interface for runtime command binding plugins
- CommandBinderService for fallback command binding resolution
- SerialDisposable for lifecycle management in generated code

## Maintainers

**Source generator restructuring:**
- Extracted MetadataExtractor into focused helpers under Helpers/:
  ObservationExtractor, WhenAnyObservableExtractor, BindingExtractor,
  CommandExtractor, InteractionExtractor, TypeDetectionExtractor,
  ExtractorValidation, SyntaxHelpers, SymbolHelpers, EventHelpers
- Moved observation generators to Generators/Observation/ subfolder
- Eliminated phantom IL branches by extracting inline pattern matching
  into named RoslynHelpers predicates
- Removed impossible null guards (replaced with null-forgiving operator)
- InvalidOperationExceptionHelper for defensive guards in extractors

**Testing:**
- Added NSubstitute dependency for mocking Roslyn interfaces
- 100% line, branch, and method coverage across all 4 assemblies
  (5585 lines, 2108 branches, 677 methods)
- 6981 tests passing across net8.0/net9.0/net10.0
- New helper unit tests: ExtractorValidationTests, CommandExtractorHelperTests,
  SymbolHelpersTests, SyntaxHelpersTests, EventHelpersTests,
  TypeDetectionExtractorTests, RoslynHelpersTests, AnalyzerHelpersTests
- Snapshot tests for all BindCommand/BindInteraction scenarios
- Runtime execution tests for BindCommand and BindInteraction
@glennawatson glennawatson changed the title Add BindCommand and BindInteraction source-generated bindings feat: add BindCommand and BindInteraction source-generated bindings Mar 8, 2026
@glennawatson glennawatson requested a review from Copilot March 8, 2026 09:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@glennawatson glennawatson requested a review from Copilot March 8, 2026 09:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Add WinForms-specific command binding tests that exercise the source
generator's output against real System.Windows.Forms.Button and
ToolStripButton controls. Tests run only on Windows CI where WinForms
types are available.

- Reference source generator as analyzer in WinForms test project
- WinFormsCommandScenarios: BindCommand calls with Button, ToolStripButton,
  expression params, observable params, and custom MouseUp event
- WinFormsCommandBindingTests: 12 tests covering click execution,
  CanExecute, disposal, command changes, parameter passing
- Fix placeholder test to run on all platforms (was excluded on Windows)
… binding tests

Remove the ReactiveUI.Binding.WinForms.Tests project which required Windows
to run. Replace with cross-platform runtime tests using structural test doubles
(WinFormsLikeButton, WpfLikeButton) that exercise all three binding plugins:
EventEnabled, CommandProperty, and DefaultEvent — running on all platforms.

Adds 14 new tests covering EventEnabled (Click+Enabled sync, CanExecute,
dispose, expression/observable params) and CommandProperty (Command/
CommandParameter property setting, reactive updates, command changes).
@glennawatson glennawatson enabled auto-merge (squash) March 8, 2026 22:15
@glennawatson glennawatson merged commit 4c5e858 into main Mar 8, 2026
4 checks passed
@glennawatson glennawatson deleted the feature/bind-command-interaction branch March 8, 2026 22:24
@codecov

codecov Bot commented Mar 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (8bb90f7) to head (c5c0498).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff             @@
##             main       #13      +/-   ##
===========================================
+ Coverage   97.82%   100.00%   +2.17%     
===========================================
  Files         164       118      -46     
  Lines        5556      1949    -3607     
  Branches      995       364     -631     
===========================================
- Hits         5435      1949    -3486     
+ Misses         30         0      -30     
+ Partials       91         0      -91     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

Copy link
Copy Markdown

This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@github-actions github-actions Bot locked as resolved and limited conversation to collaborators Mar 23, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants