Skip to content

Commit fbbcd52

Browse files
authored
feat: add view registration attributes and attribute-aware dispatch (#17)
## View Registration Attributes - Add ExcludeFromViewRegistration, SingleInstanceView, and ViewContract attributes to ReactiveUI.Binding - Source generator reads attributes to skip excluded views, emit singleton caching, and generate contract-aware dispatch - Add CreateMappingBuilder() factory method on DefaultViewLocator ## Generator Improvements - Rewrite ViewLocatorDispatchGenerator to use raw string literals ($$""" pattern) - Deduplicate by (ViewModel FQN, Contract) pair instead of ViewModel FQN alone - Remove redundant IEquatable<Self> from all sealed record model types ## Test Coverage - 100% line and branch coverage maintained * fix: ViewLocatorDispatch contract ordering, thread safety, and defensive improvements - Group dispatch branches by ViewModel type so contract-specific checks are emitted before the default branch (prevents shadowing) - Escape contract strings with SymbolDisplay.FormatLiteral for safe codegen - Use Interlocked.CompareExchange for thread-safe singleton view caching - Fix strategyDoc for SingleInstanceView without parameterless constructor - Move [ExcludeFromViewRegistration] check inside IViewFor<T> guard to prevent EnsureNotNull crash in compilations without ReactiveUI.Binding - Add tests for grouped dispatch and contract-only dispatch (100% coverage)
1 parent 3c0fe71 commit fbbcd52

File tree

44 files changed

+1674
-385
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1674
-385
lines changed

CLAUDE.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,8 @@ ReactiveUI.Binding.SourceGenerators is an **incremental source generator** that
159159
```
160160
src/
161161
├── ReactiveUI.Binding/ # Runtime library (net8.0;net9.0;net10.0;net462-net481)
162-
│ └── Interfaces/ # ICreatesObservableForProperty, IObservedChange, etc.
162+
│ ├── Interfaces/ # ICreatesObservableForProperty, IObservedChange, etc.
163+
│ └── View/ # ViewLocator, DefaultViewLocator, IViewFor<T>, attributes
163164
164165
├── ReactiveUI.Binding.SourceGenerators/ # Source generator (netstandard2.0)
165166
│ ├── BindingGenerator.cs # [Generator] IIncrementalGenerator entry point
@@ -171,7 +172,8 @@ src/
171172
│ │ ├── EquatableArray.cs
172173
│ │ ├── ClassBindingInfo.cs # Type-level: notification mechanism flags
173174
│ │ ├── InvocationInfo.cs # Per-call-site: WhenChanged/WhenChanging
174-
│ │ └── BindingInvocationInfo.cs # Per-call-site: BindOneWay/BindTwoWay
175+
│ │ ├── BindingInvocationInfo.cs # Per-call-site: BindOneWay/BindTwoWay
176+
│ │ └── ViewRegistrationInfo.cs # Per-IViewFor<T>: view dispatch mapping
175177
│ ├── Generators/ # Per-kind fallback generators (Pipeline A)
176178
│ │ ├── ReactiveObjectBindingGenerator.cs # IReactiveObject (affinity 24)
177179
│ │ ├── INPCBindingGenerator.cs # INotifyPropertyChanged (affinity 21)
@@ -180,13 +182,17 @@ src/
180182
│ │ ├── KVOBindingGenerator.cs # Apple KVO/NSObject (affinity 25)
181183
│ │ ├── WinFormsBindingGenerator.cs # WinForms Component (affinity 23)
182184
│ │ ├── AndroidBindingGenerator.cs # Android View (affinity 19)
183-
│ │ └── RegistrationGenerator.cs # Consolidates all → [ModuleInitializer]
185+
│ │ ├── RegistrationGenerator.cs # Consolidates all → [ModuleInitializer]
186+
│ │ └── ViewLocatorDispatchGenerator.cs # IViewFor<T> → AOT view dispatch (Pipeline C)
184187
│ ├── Invocations/ # Per-invocation generators (Pipeline B)
185188
│ │ ├── WhenChangedInvocationGenerator.cs # After-change observation
186189
│ │ ├── WhenChangingInvocationGenerator.cs # Before-change observation
187190
│ │ ├── BindOneWayInvocationGenerator.cs # One-way binding
188191
│ │ ├── BindTwoWayInvocationGenerator.cs # Two-way binding
189192
│ │ └── WhenAnyValueInvocationGenerator.cs # WhenAnyValue compat shim
193+
│ ├── Helpers/ # Extraction and validation helpers
194+
│ │ ├── ViewRegistrationExtractor.cs # IViewFor<T> → ViewRegistrationInfo extraction
195+
│ │ └── ... # ExtractorValidation, SymbolHelpers, etc.
190196
│ └── CodeGeneration/
191197
│ └── CodeGenerator.cs # StringBuilder-based code generation
192198
@@ -201,12 +207,14 @@ src/
201207
└── ReactiveUI.Binding.Tests/ # Runtime library tests
202208
```
203209

204-
### Two Pipelines
210+
### Three Pipelines
205211

206212
**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.
207213

208214
**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.
209215

216+
**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]`.
217+
210218
### API Pattern
211219

212220
```csharp

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ A C# source generator that replaces ReactiveUI's runtime expression-tree binding
2626
- [How do I install?](#how-do-i-install)
2727
- [Supported APIs](#supported-apis)
2828
- [Usage Examples](#usage-examples)
29+
- [View Locator](#view-locator)
2930
- [Supported Notification Mechanisms](#supported-notification-mechanisms)
3031
- [Packages](#packages)
3132
- [Rx Library Compatibility](#rx-library-compatibility)
@@ -142,6 +143,8 @@ Platform-specific packages provide DependencyProperty observation and other plat
142143
| `BindTwoWay` | Two-way binding between source and target |
143144
| `OneWayBind` | ReactiveUI compatibility shim for one-way binding |
144145
| `Bind` | ReactiveUI compatibility shim for two-way binding |
146+
| `BindCommand` | Bind a command property to a UI element |
147+
| `BindInteraction` | Bind an interaction to a handler |
145148

146149
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`).
147150

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

226+
### View Locator
227+
228+
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.
229+
230+
```csharp
231+
// Implement IViewFor<T> and the generator registers the mapping automatically
232+
public class LoginView : ReactiveUI.Binding.IViewFor<LoginViewModel>
233+
{
234+
public LoginViewModel ViewModel { get; set; }
235+
object ReactiveUI.Binding.IViewFor.ViewModel
236+
{
237+
get => ViewModel;
238+
set => ViewModel = (LoginViewModel)value;
239+
}
240+
}
241+
242+
// Resolve a view at runtime
243+
IViewFor view = ViewLocator.Current.ResolveView(myViewModel);
244+
```
245+
246+
#### View Attributes
247+
248+
| Attribute | Description |
249+
|-----------|-------------|
250+
| `[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. |
251+
| `[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. |
252+
| `[ExcludeFromViewRegistration]` | Excludes the view from automatic source-generated registration (e.g. for base classes or test doubles). |
253+
254+
```csharp
255+
// Contract-based resolution: multiple views for one ViewModel
256+
[ViewContract("compact")]
257+
public class CompactDashboardView : IViewFor<DashboardViewModel> { /* ... */ }
258+
259+
public class FullDashboardView : IViewFor<DashboardViewModel> { /* ... */ }
260+
261+
// Resolve the compact view
262+
var compactView = ViewLocator.Current.ResolveView(vm, "compact");
263+
264+
// Resolve the default view
265+
var defaultView = ViewLocator.Current.ResolveView(vm);
266+
```
267+
268+
#### Resolution Strategy
269+
270+
The generated dispatch follows a 3-tier resolution strategy per view:
271+
272+
1. **Service locator** -- checks `Splat.AppLocator.Current` for DI-registered views
273+
2. **Direct construction** -- falls back to `new View()` if a parameterless constructor exists
274+
3. **Singleton cache** -- for `[SingleInstanceView]`, caches and reuses a single instance (thread-safe via `Interlocked.CompareExchange`)
275+
223276
## Supported Notification Mechanisms
224277

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

364417
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.
365418

419+
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.
420+
366421
## Contribute
367422

368423
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.

src/ReactiveUI.Binding.SourceGenerators/Constants.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,21 @@ internal static class Constants
135135
/// </summary>
136136
internal const string IViewForGenericMetadataName = "ReactiveUI.Binding.IViewFor`1";
137137

138+
/// <summary>
139+
/// Metadata name for the <c>ExcludeFromViewRegistrationAttribute</c> used to skip view auto-registration.
140+
/// </summary>
141+
internal const string ExcludeFromViewRegistrationAttributeMetadataName = "ReactiveUI.Binding.ExcludeFromViewRegistrationAttribute";
142+
143+
/// <summary>
144+
/// Metadata name for the <c>SingleInstanceViewAttribute</c> used for singleton view caching.
145+
/// </summary>
146+
internal const string SingleInstanceViewAttributeMetadataName = "ReactiveUI.Binding.SingleInstanceViewAttribute";
147+
148+
/// <summary>
149+
/// Metadata name for the <c>ViewContractAttribute</c> used for contract-based view registration.
150+
/// </summary>
151+
internal const string ViewContractAttributeMetadataName = "ReactiveUI.Binding.ViewContractAttribute";
152+
138153
/// <summary>
139154
/// Metadata name for the <c>IBindingTypeConverter</c> interface used for custom type conversions.
140155
/// </summary>

0 commit comments

Comments
 (0)