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
16 changes: 12 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@ ReactiveUI.Binding.SourceGenerators is an **incremental source generator** that
```
src/
├── ReactiveUI.Binding/ # Runtime library (net8.0;net9.0;net10.0;net462-net481)
│ └── Interfaces/ # ICreatesObservableForProperty, IObservedChange, etc.
│ ├── Interfaces/ # ICreatesObservableForProperty, IObservedChange, etc.
│ └── View/ # ViewLocator, DefaultViewLocator, IViewFor<T>, attributes
├── ReactiveUI.Binding.SourceGenerators/ # Source generator (netstandard2.0)
│ ├── BindingGenerator.cs # [Generator] IIncrementalGenerator entry point
Expand All @@ -171,7 +172,8 @@ src/
│ │ ├── EquatableArray.cs
│ │ ├── ClassBindingInfo.cs # Type-level: notification mechanism flags
│ │ ├── InvocationInfo.cs # Per-call-site: WhenChanged/WhenChanging
│ │ └── BindingInvocationInfo.cs # Per-call-site: BindOneWay/BindTwoWay
│ │ ├── BindingInvocationInfo.cs # Per-call-site: BindOneWay/BindTwoWay
│ │ └── ViewRegistrationInfo.cs # Per-IViewFor<T>: view dispatch mapping
│ ├── Generators/ # Per-kind fallback generators (Pipeline A)
│ │ ├── ReactiveObjectBindingGenerator.cs # IReactiveObject (affinity 24)
│ │ ├── INPCBindingGenerator.cs # INotifyPropertyChanged (affinity 21)
Expand All @@ -180,13 +182,17 @@ src/
│ │ ├── KVOBindingGenerator.cs # Apple KVO/NSObject (affinity 25)
│ │ ├── WinFormsBindingGenerator.cs # WinForms Component (affinity 23)
│ │ ├── AndroidBindingGenerator.cs # Android View (affinity 19)
│ │ └── RegistrationGenerator.cs # Consolidates all → [ModuleInitializer]
│ │ ├── RegistrationGenerator.cs # Consolidates all → [ModuleInitializer]
│ │ └── ViewLocatorDispatchGenerator.cs # IViewFor<T> → AOT view dispatch (Pipeline C)
│ ├── Invocations/ # Per-invocation generators (Pipeline B)
│ │ ├── WhenChangedInvocationGenerator.cs # After-change observation
│ │ ├── WhenChangingInvocationGenerator.cs # Before-change observation
│ │ ├── BindOneWayInvocationGenerator.cs # One-way binding
│ │ ├── BindTwoWayInvocationGenerator.cs # Two-way binding
│ │ └── WhenAnyValueInvocationGenerator.cs # WhenAnyValue compat shim
│ ├── Helpers/ # Extraction and validation helpers
│ │ ├── ViewRegistrationExtractor.cs # IViewFor<T> → ViewRegistrationInfo extraction
│ │ └── ... # ExtractorValidation, SymbolHelpers, etc.
│ └── CodeGeneration/
│ └── CodeGenerator.cs # StringBuilder-based code generation
Expand All @@ -201,12 +207,14 @@ src/
└── ReactiveUI.Binding.Tests/ # Runtime library tests
```

### Two Pipelines
### Three Pipelines

**Pipeline A (Type Detection)**: Scans classes with base lists → builds `ClassBindingInfo` POCOs with boolean flags for each notification mechanism (IReactiveObject, INPC, WPF DP, WinUI DP, KVO, WinForms, Android). Per-kind generators filter from this shared pipeline. Consolidates into a single `[ModuleInitializer]` registration.

**Pipeline B (Invocation Detection)**: Scans method invocations (`WhenChanged`, `WhenChanging`, `BindOneWay`, `BindTwoWay`, `WhenAnyValue`) → extracts lambda property paths → generates optimized per-call-site observation/binding code. Uses **CallerFilePath + CallerLineNumber dispatch**: API stubs capture caller info, generated dispatch table routes to compile-time generated methods.

**Pipeline C (View Dispatch)**: Scans classes implementing `IViewFor<T>` → extracts `ViewRegistrationInfo` POCOs (VM FQN, View FQN, constructor availability, `[ViewContract]` contract, `[SingleInstanceView]` flag) → generates `ViewDispatch.g.cs` with a type-switch dispatch function. Supports contract-based multi-view resolution (contract checks emitted before default), singleton caching via `Interlocked.CompareExchange`, and 3-tier resolution (service locator → direct construction → null). Views can be excluded with `[ExcludeFromViewRegistration]`.

### API Pattern

```csharp
Expand Down
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ A C# source generator that replaces ReactiveUI's runtime expression-tree binding
- [How do I install?](#how-do-i-install)
- [Supported APIs](#supported-apis)
- [Usage Examples](#usage-examples)
- [View Locator](#view-locator)
- [Supported Notification Mechanisms](#supported-notification-mechanisms)
- [Packages](#packages)
- [Rx Library Compatibility](#rx-library-compatibility)
Expand Down Expand Up @@ -142,6 +143,8 @@ Platform-specific packages provide DependencyProperty observation and other plat
| `BindTwoWay` | Two-way binding between source and target |
| `OneWayBind` | ReactiveUI compatibility shim for one-way binding |
| `Bind` | ReactiveUI compatibility shim for two-way binding |
| `BindCommand` | Bind a command property to a UI element |
| `BindInteraction` | Bind an interaction to a handler |

All APIs support single properties, deep property chains (e.g. `x => x.Address.City`), and multi-property observation (up to 12 properties for `WhenAnyValue`/`WhenChanged`).

Expand Down Expand Up @@ -220,6 +223,56 @@ IDisposable binding = vm.BindOneWay(view, x => x.Name, x => x.NameLabel,
scheduler: RxApp.MainThreadScheduler);
```

### View Locator

The source generator automatically detects classes implementing `IViewFor<T>` and generates an AOT-safe view dispatch table. Views are resolved without reflection via a compile-time type-switch.

```csharp
// Implement IViewFor<T> and the generator registers the mapping automatically
public class LoginView : ReactiveUI.Binding.IViewFor<LoginViewModel>
{
public LoginViewModel ViewModel { get; set; }
object ReactiveUI.Binding.IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = (LoginViewModel)value;
}
}

// Resolve a view at runtime
IViewFor view = ViewLocator.Current.ResolveView(myViewModel);
```

#### View Attributes

| Attribute | Description |
|-----------|-------------|
| `[ViewContract("name")]` | Registers the view under a named contract, enabling multiple views per ViewModel. Pass the contract string to `ResolveView` to select a specific view. |
| `[SingleInstanceView]` | Caches a singleton instance of the view instead of creating a new one per resolution. Not suitable for views reused multiple times in the visual tree. |
| `[ExcludeFromViewRegistration]` | Excludes the view from automatic source-generated registration (e.g. for base classes or test doubles). |

```csharp
// Contract-based resolution: multiple views for one ViewModel
[ViewContract("compact")]
public class CompactDashboardView : IViewFor<DashboardViewModel> { /* ... */ }

public class FullDashboardView : IViewFor<DashboardViewModel> { /* ... */ }

// Resolve the compact view
var compactView = ViewLocator.Current.ResolveView(vm, "compact");

// Resolve the default view
var defaultView = ViewLocator.Current.ResolveView(vm);
```

#### Resolution Strategy

The generated dispatch follows a 3-tier resolution strategy per view:

1. **Service locator** -- checks `Splat.AppLocator.Current` for DI-registered views
2. **Direct construction** -- falls back to `new View()` if a parameterless constructor exists
3. **Singleton cache** -- for `[SingleInstanceView]`, caches and reuses a single instance (thread-safe via `Interlocked.CompareExchange`)

## Supported Notification Mechanisms

The source generator detects and generates optimised code for each platform's notification mechanism:
Expand Down Expand Up @@ -363,6 +416,8 @@ src/

The generator and analyzer both target netstandard2.0 (Roslyn requirement). The runtime library targets .NET 8.0, 9.0, 10.0, and .NET Framework 4.6.2-4.8.1. Generated output is C# 7.3 compatible to support the widest range of consumer projects.

A third pipeline scans for `IViewFor<T>` implementations and generates an AOT-safe view dispatch table (`ViewDispatch.g.cs`) with type-switch resolution, singleton caching, and contract-based selection.

## Contribute

ReactiveUI.Binding.SourceGenerators is developed under an OSI-approved open source license, making it freely usable and distributable, even for commercial use. We value the people who are involved in this project, and we'd love to have you on board, especially if you are just getting started or have never contributed to open-source before.
Expand Down
15 changes: 15 additions & 0 deletions src/ReactiveUI.Binding.SourceGenerators/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,21 @@ internal static class Constants
/// </summary>
internal const string IViewForGenericMetadataName = "ReactiveUI.Binding.IViewFor`1";

/// <summary>
/// Metadata name for the <c>ExcludeFromViewRegistrationAttribute</c> used to skip view auto-registration.
/// </summary>
internal const string ExcludeFromViewRegistrationAttributeMetadataName = "ReactiveUI.Binding.ExcludeFromViewRegistrationAttribute";

/// <summary>
/// Metadata name for the <c>SingleInstanceViewAttribute</c> used for singleton view caching.
/// </summary>
internal const string SingleInstanceViewAttributeMetadataName = "ReactiveUI.Binding.SingleInstanceViewAttribute";

/// <summary>
/// Metadata name for the <c>ViewContractAttribute</c> used for contract-based view registration.
/// </summary>
internal const string ViewContractAttributeMetadataName = "ReactiveUI.Binding.ViewContractAttribute";

/// <summary>
/// Metadata name for the <c>IBindingTypeConverter</c> interface used for custom type conversions.
/// </summary>
Expand Down
Loading
Loading