Skip to content

feat: add view registration attributes and attribute-aware dispatch#17

Merged
glennawatson merged 4 commits intomainfrom
feature/iviewforbinding
Mar 10, 2026
Merged

feat: add view registration attributes and attribute-aware dispatch#17
glennawatson merged 4 commits intomainfrom
feature/iviewforbinding

Conversation

@glennawatson
Copy link
Contributor

What's New

Three new attributes give you compile-time control over how views are registered and resolved — no runtime reflection needed.

Exclude a view from auto-registration

[ExcludeFromViewRegistration]
public class DesignTimeView : IViewFor<MyViewModel> { ... }

The source generator skips this class entirely. Useful for design-time views, test doubles, or platform-specific views you register manually.

Cache a view as a singleton

[SingleInstanceView]
public class ShellView : IViewFor<ShellViewModel> { ... }

The generator emits a static field and lazy-initialization pattern so the same instance is returned on every resolution:

// Generated code (simplified)
private static ShellView __singletonView_0;

if (__singletonView_0 == null)
    __singletonView_0 = new ShellView();
return __singletonView_0;

Register a view under a contract

[ViewContract("compact")]
public class CompactView : IViewFor<DashboardViewModel> { ... }

The generated dispatch only matches when the caller requests that exact contract:

// Generated code (simplified)
if (instance is DashboardViewModel)
{
    if (contract == "compact")
        return __ResolveView_0(contract);
}

This lets you have multiple views for the same ViewModel, selected at resolution time:

viewLocator.ResolveView(vm);             // default view
viewLocator.ResolveView(vm, "compact");  // compact view

Fluent mapping builder

DefaultViewLocator now exposes a CreateMappingBuilder() factory for fluent runtime registration:

var locator = new DefaultViewLocator();
locator.CreateMappingBuilder()
    .Map<LoginViewModel, LoginView>()
    .Map<SettingsViewModel, SettingsView>("admin");

Maintainer Notes

Attributes

  • ExcludeFromViewRegistrationAttribute, SingleInstanceViewAttribute, ViewContractAttribute added to ReactiveUI.Binding/View/
  • Namespace is ReactiveUI.Binding (not ReactiveUI) — ReactiveUI will delete its copies and consume these via global using
  • Metadata name constants added to Constants.cs

Source Generator Changes

  • ViewRegistrationExtractor reads all three attributes during IViewFor<T> extraction; excluded views return null before reaching the pipeline
  • ViewRegistrationInfo record gains Contract (string?) and IsSingleInstance (bool) properties
  • ViewLocatorDispatchGenerator deduplicates by (ViewModel FQN, Contract) pair, emits contract-guarded dispatch branches, and emits singleton cache fields + lazy init for [SingleInstanceView] views
  • Generator rewritten to use $$""" raw string literals matching project conventions

Cleanup

  • Removed redundant : IEquatable<Self> from all 10 sealed record model types (records implement this automatically)
  • Defensive null guards in extractor refactored to use InvalidOperationExceptionHelper.EnsureNotNull (attribute types co-exist with IViewFor<T> in the same assembly, so null is unreachable)

Coverage

  • 100% line and branch coverage maintained

## 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
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds compile-time attributes and generator support to control view auto-registration/resolution (exclude, singleton caching, and contract-based dispatch), plus a fluent runtime mapping builder on DefaultViewLocator.

Changes:

  • Introduces ExcludeFromViewRegistrationAttribute, SingleInstanceViewAttribute, and ViewContractAttribute in ReactiveUI.Binding.
  • Updates the source generator pipeline to extract these attributes and emit contract-aware dispatch + optional singleton caching fields.
  • Adds DefaultViewLocator.CreateMappingBuilder() and expands unit/snapshot coverage; removes redundant IEquatable<T> from model records.

Reviewed changes

Copilot reviewed 36 out of 36 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/ReactiveUI.Binding/View/ViewContractAttribute.cs Adds contract attribute for view resolution specificity.
src/ReactiveUI.Binding/View/SingleInstanceViewAttribute.cs Adds marker attribute for singleton view caching generation.
src/ReactiveUI.Binding/View/ExcludeFromViewRegistrationAttribute.cs Adds marker attribute to skip view auto-registration.
src/ReactiveUI.Binding/View/DefaultViewLocator.cs Exposes CreateMappingBuilder() factory for fluent runtime mappings.
src/ReactiveUI.Binding.SourceGenerators/Constants.cs Adds metadata name constants for new attributes.
src/ReactiveUI.Binding.SourceGenerators/Helpers/ViewRegistrationExtractor.cs Extracts contract/singleton/exclusion metadata into ViewRegistrationInfo.
src/ReactiveUI.Binding.SourceGenerators/Generators/ViewLocatorDispatchGenerator.cs Emits contract-aware dispatch branches and singleton cache fields.
src/ReactiveUI.Binding.SourceGenerators/Models/ViewRegistrationInfo.cs Extends model with Contract and IsSingleInstance.
src/ReactiveUI.Binding.SourceGenerators/Models/WhenAnyObservableInvocationInfo.cs Removes redundant IEquatable<> from record.
src/ReactiveUI.Binding.SourceGenerators/Models/PropertyPathSegment.cs Removes redundant IEquatable<> from record.
src/ReactiveUI.Binding.SourceGenerators/Models/ObservableTypeInfo.cs Removes redundant IEquatable<> from record.
src/ReactiveUI.Binding.SourceGenerators/Models/ObservablePropertyInfo.cs Removes redundant IEquatable<> from record.
src/ReactiveUI.Binding.SourceGenerators/Models/InvocationInfo.cs Removes redundant IEquatable<> from record.
src/ReactiveUI.Binding.SourceGenerators/Models/ClassBindingInfo.cs Removes redundant IEquatable<> from record.
src/ReactiveUI.Binding.SourceGenerators/Models/BindingInvocationInfo.cs Removes redundant IEquatable<> from record.
src/ReactiveUI.Binding.SourceGenerators/Models/BindInteractionInvocationInfo.cs Removes redundant IEquatable<> from record.
src/ReactiveUI.Binding.SourceGenerators/Models/BindCommandInvocationInfo.cs Removes redundant IEquatable<> from record.
src/tests/ReactiveUI.Binding.Tests/View/ViewAttributeTests.cs Adds basic tests for new attribute types.
src/tests/ReactiveUI.Binding.Tests/View/DefaultViewLocatorTests.cs Tests CreateMappingBuilder() registers mappings on the locator instance.
src/tests/ReactiveUI.Binding.SourceGenerators.Tests/ViewLocatorDispatchGeneratorTests.cs Adds snapshot tests for exclusion, singleton, and contract dispatch generation.
src/tests/ReactiveUI.Binding.SourceGenerators.Tests/VDG.ViewWithoutParameterlessConstructor#ViewDispatch.g.verified.cs Updates snapshots for new generator output formatting/behavior.
src/tests/ReactiveUI.Binding.SourceGenerators.Tests/VDG.ViewWithPrivateConstructor#ViewDispatch.g.verified.cs Updates snapshots for new generator output formatting/behavior.
src/tests/ReactiveUI.Binding.SourceGenerators.Tests/VDG.ViewContractGeneratesContractDispatch#ViewDispatch.g.verified.cs Adds snapshot for contract-aware dispatch output.
src/tests/ReactiveUI.Binding.SourceGenerators.Tests/VDG.ViewContractGeneratesContractDispatch#GeneratedBindingsAttributes.g.verified.cs Snapshot for generated bindings attributes file.
src/tests/ReactiveUI.Binding.SourceGenerators.Tests/VDG.ViewContractGeneratesContractDispatch#GeneratedBinderRegistration.g.verified.cs Snapshot for generated binder registration file.
src/tests/ReactiveUI.Binding.SourceGenerators.Tests/VDG.SingleViewForImplementation#ViewDispatch.g.verified.cs Updates snapshot for generator formatting changes.
src/tests/ReactiveUI.Binding.SourceGenerators.Tests/VDG.SingleInstanceViewWithoutParameterlessCtor#ViewDispatch.g.verified.cs Adds snapshot for singleton attribute without direct construction case.
src/tests/ReactiveUI.Binding.SourceGenerators.Tests/VDG.SingleInstanceViewWithoutParameterlessCtor#GeneratedBindingsAttributes.g.verified.cs Snapshot for generated bindings attributes file.
src/tests/ReactiveUI.Binding.SourceGenerators.Tests/VDG.SingleInstanceViewWithoutParameterlessCtor#GeneratedBinderRegistration.g.verified.cs Snapshot for generated binder registration file.
src/tests/ReactiveUI.Binding.SourceGenerators.Tests/VDG.SingleInstanceViewGeneratesSingletonCache#ViewDispatch.g.verified.cs Adds snapshot for singleton-cache output.
src/tests/ReactiveUI.Binding.SourceGenerators.Tests/VDG.SingleInstanceViewGeneratesSingletonCache#GeneratedBindingsAttributes.g.verified.cs Snapshot for generated bindings attributes file.
src/tests/ReactiveUI.Binding.SourceGenerators.Tests/VDG.SingleInstanceViewGeneratesSingletonCache#GeneratedBinderRegistration.g.verified.cs Snapshot for generated binder registration file.
src/tests/ReactiveUI.Binding.SourceGenerators.Tests/VDG.MultipleViewForImplementations#ViewDispatch.g.verified.cs Updates snapshot for generator formatting changes.
src/tests/ReactiveUI.Binding.SourceGenerators.Tests/VDG.ExclAttr#GeneratedBindingsAttributes.g.verified.cs Snapshot for exclusion-only generation case.
src/tests/ReactiveUI.Binding.SourceGenerators.Tests/VDG.ExclAttr#GeneratedBinderRegistration.g.verified.cs Snapshot for exclusion-only generation case.
src/tests/ReactiveUI.Binding.SourceGenerators.Tests/VDG.DuplicateViewModelsAreDeduplicated#ViewDispatch.g.verified.cs Updates snapshot for new deduplication key and formatting changes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@codecov
Copy link

codecov bot commented Mar 10, 2026

Codecov Report

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

Additional details and impacted files
@@            Coverage Diff            @@
##              main       #17   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          125       126    +1     
  Lines         2116      2119    +3     
  Branches       395       395           
=========================================
+ Hits          2116      2119    +3     

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

…ive 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)
@glennawatson glennawatson enabled auto-merge (squash) March 10, 2026 11:06
glennawatson and others added 2 commits March 10, 2026 22:22
…DE.md

- Add View Locator section with usage examples, attribute table, and
  resolution strategy to README
- Add BindCommand and BindInteraction to supported APIs table
- Document Pipeline C (View Dispatch) in CLAUDE.md
- Update project structure with View/, ViewLocatorDispatchGenerator,
  ViewRegistrationExtractor, and ViewRegistrationInfo
@glennawatson glennawatson merged commit fbbcd52 into main Mar 10, 2026
6 checks passed
@glennawatson glennawatson deleted the feature/iviewforbinding branch March 10, 2026 13:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants