StaticViewLocator 0.6.0
See the version list below for details.
dotnet add package StaticViewLocator --version 0.6.0
NuGet\Install-Package StaticViewLocator -Version 0.6.0
<PackageReference Include="StaticViewLocator" Version="0.6.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="StaticViewLocator" Version="0.6.0" />
<PackageReference Include="StaticViewLocator"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add StaticViewLocator --version 0.6.0
#r "nuget: StaticViewLocator, 0.6.0"
#:package StaticViewLocator@0.6.0
#addin nuget:?package=StaticViewLocator&version=0.6.0
#tool nuget:?package=StaticViewLocator&version=0.6.0
StaticViewLocator
A C# source generator that automatically implements static view locator for Avalonia without using reflection.
Usage
Add NuGet package reference to project.
<PackageReference Include="StaticViewLocator" Version="0.6.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
Annotate a view locator class with [StaticViewLocator], make it partial, and let the generator provide the lookup tables and fallback helpers.
[StaticViewLocator]
public partial class ViewLocator : IDataTemplate
{
public Control? Build(object? data)
{
if (data is null)
{
return null;
}
var type = data.GetType();
var func = TryGetFactory(type) ?? TryGetFactoryFromInterfaces(type);
if (func is not null)
{
return func.Invoke();
}
var missingView = TryGetMissingView(type) ?? TryGetMissingViewFromInterfaces(type);
if (missingView is not null)
{
return new TextBlock { Text = missingView };
}
throw new Exception($"Unable to create view for type: {type}");
}
public bool Match(object? data)
{
return data is ViewModelBase;
}
}
Explicit mappings
Use [StaticViewMapping] on the locator when a view and view model do not follow the configured naming rules. Explicit mappings override convention-based discovery and also support model types whose names do not end in ViewModel.
[StaticViewLocator]
[StaticViewMapping(typeof(LoginViewModel), typeof(LogInView))]
[StaticViewMapping(typeof(DashboardModel), typeof(DashboardScreen))]
public partial class ViewLocator : IDataTemplate
{
}
Generic MVVM contract mappings
Use ViewModelMappingContracts when a framework already expresses the view/view-model relationship through a one-argument generic interface or base class. The generator scans concrete Avalonia views at compile time, follows their interface and base-type ancestry, and maps the contract's generic argument to the concrete view.
public interface IViewFor<TViewModel>
{
}
public abstract class FrameworkView<TViewModel> : UserControl, IViewFor<TViewModel>
{
}
public sealed class DashboardScreen : FrameworkView<DashboardModel>
{
}
[StaticViewLocator(
ViewModelMappingContracts = new[] { typeof(IViewFor<>) })]
public partial class ViewLocator
{
}
This generates DashboardModel -> DashboardScreen even though neither type follows the default *ViewModel -> *View naming convention. Multiple contracts can be supplied in the array.
Contract discovery has these constraints:
- each configured contract must be an open generic type with exactly one type parameter;
- invalid configured contracts are ignored and produce the
SVL0006error; - the contract may be an interface or a base class, including one inherited indirectly;
- discovered views must be concrete, non-generic types from the current compilation;
- discovered views must derive from
UserControl,Window, or a type configured throughStaticViewLocatorAdditionalViewBaseTypes; - discovered views must be accessible to the locator and expose an accessible constructor callable without arguments;
- contract discovery happens at compile time and adds no runtime assembly scanning;
- ambiguous configured-contract mappings are omitted, produce
SVL0003, and must be resolved with an explicit[StaticViewMapping]override; automatic ReactiveUI ambiguities also produceSVL0003.
Mapping sources are applied in this order, from highest to lowest priority:
[StaticViewMapping]explicit overrideViewModelMappingContractsinference- automatic
ReactiveUI.IViewFor<TViewModel>inference whenGenerateIViewLocator = true - configured namespace and type-name conventions
Exact factory generation
Set GenerateViewFactoryMethods = true to generate this private partial-class helper:
private static bool TryCreateViewExact(Type viewModelType, out Control? view)
It performs only an exact dictionary lookup and invokes the statically generated constructor delegate. It does not walk base types or interfaces and does not construct closed generic types at runtime. This is useful when an MVVM framework needs to create a view and then apply its own view-model assignment or lifecycle rules.
If the annotated partial class already declares a compatible TryCreateViewExact(Type, out Control?) returning bool, the generator reuses it instead of emitting a duplicate helper. The source helper may be static or instance-based because generated adapter calls are instance methods.
When a locator supplies its own Build method, set GenerateRuntimeTypeFallbackMethods = false to omit the legacy BaseType, GetInterfaces(), and generic-type-definition fallback helpers. If the generator must emit the legacy Build, those helpers are always emitted because that implementation depends on them. The generated IDataTemplate.Build path uses exact static lookup and does not emit these runtime type-walking helpers unless a source-declared Build(object?) requires them and the option remains enabled.
Generated ReactiveUI IViewLocator and Avalonia IDataTemplate
The generator can optionally generate the complete ReactiveUI IViewLocator and Avalonia IDataTemplate adapter. This removes the manual Build, Match, and four ResolveView methods from the locator class.
using ReactiveUI;
using StaticViewLocator;
[StaticViewLocator(
GenerateIViewLocator = true,
GenerateIDataTemplate = true,
GenerateRuntimeTypeFallbackMethods = false,
DataTemplateMatchTypes = new[] { typeof(ViewModelBase) })]
public partial class ViewLocator
{
}
GenerateIViewLocator = true requires the consumer project to reference ReactiveUI. It automatically discovers concrete Avalonia views implementing ReactiveUI.IViewFor<TViewModel>, so ViewModelMappingContracts = new[] { typeof(IViewFor<>) } and GenerateViewFactoryMethods = true are not required for this mode. User-configured ViewModelMappingContracts take precedence over this automatic ReactiveUI inference, while [StaticViewMapping] remains the final override. The generated locator implements all four current ReactiveUI resolution overloads. Runtime-instance resolution assigns IViewFor.ViewModel; non-null contracts return null because the generated map is currently unkeyed.
ReactiveUI package families
ReactiveUI 24 is available in two mutually exclusive distribution families. Choose one complete family for an application and keep every ReactiveUI and Avalonia package on that same row:
| Mode | ReactiveUI packages | Concrete implementation namespaces |
|---|---|---|
| Primitives | ReactiveUI, ReactiveUI.Avalonia |
ReactiveUI, ReactiveUI.Avalonia |
| System.Reactive | ReactiveUI.Reactive, ReactiveUI.Avalonia.Reactive |
ReactiveUI.Reactive, ReactiveUI.Avalonia.Reactive |
Do not reference packages from both rows in the same application. StaticViewLocator supports either mode without a generator option because its generated adapter uses only the distribution-neutral contracts ReactiveUI.IViewFor, ReactiveUI.IViewFor<TViewModel>, and ReactiveUI.IViewLocator. Only application code that uses concrete types such as ReactiveObject, ReactiveCommand, ViewModelViewHost, or UseReactiveUI needs the namespace from the selected distribution.
GenerateIDataTemplate = true adds IDataTemplate, Control? Build(object?), and Match(object?). DataTemplateMatchTypes provides a fast application-specific match predicate. If it is empty, the generated Match checks the statically generated view and missing-view maps using the same exact-type semantics as generated Build.
The generated Build pipeline is:
BuildInvalidView(param)fornullinput.BuildResolvedView(param)for the normal statically mapped view.BuildFallbackView(param)for application-specific fallback cases.BuildMissingView(param, viewModelType)for the final not-found control.
For non-sealed locator classes, default hook implementations are generated as protected virtual, allowing normal subclass overrides. For sealed locator classes the generated defaults are private, because virtual members are illegal on sealed types. In both cases, a hook with the corresponding by-value object?-based signature declared directly in the annotated partial class suppresses generation of that default hook; unrelated or ref/in/out overloads do not suppress it. A custom hook return type must be implicitly convertible to Control?. This allows application-specific behavior without replacing the public generated Build method. For example, an application-specific context fallback can be implemented as:
public interface IContextHost
{
object? Context { get; }
}
[StaticViewLocator(
GenerateIViewLocator = true,
GenerateIDataTemplate = true,
GenerateRuntimeTypeFallbackMethods = false,
DataTemplateMatchTypes = new[] { typeof(ViewModelBase), typeof(IContextHost) })]
public partial class ViewLocator
{
protected virtual Control? BuildFallbackView(object? param)
{
if (param is not IContextHost { Context: ViewModelBase })
{
return null;
}
var contentControl = new ContentControl
{
DataContext = param,
};
contentControl.Bind(
ContentControl.ContentProperty,
new Binding(nameof(IContextHost.Context)));
return contentControl;
}
}
The generator assembly itself does not reference ReactiveUI. Distribution-neutral ReactiveUI contract types are referenced only in generated consumer source when GenerateIViewLocator is enabled.
Source-declared public adapter methods are reused only when they implement the corresponding framework contract: public instance accessibility, by-value parameters, the expected return type, and the ReactiveUI where TViewModel : class constraint. A same-signature member that cannot implement the contract produces SVL0002 rather than silently suppressing required generated code.
The solution builds the same complete AXAML sample against both distributions. StaticViewLocatorReactiveUIDemo covers the primitives packages, while StaticViewLocatorReactiveUIDemo.Reactive links the same C# and AXAML source and covers the System.Reactive packages. The shared UI displays the same navigation state through two side-by-side paths: an Avalonia ContentControl using the generated IDataTemplate, and a ReactiveUI ViewModelViewHost whose ViewLocator is the generated locator. It also demonstrates the context-wrapper fallback without runtime view scanning. A small local ReactiveViewHost wrapper keeps the AXAML distribution-neutral because the concrete ReactiveUI control namespace differs between package families.
Generator diagnostics
| ID | Severity | Meaning |
|---|---|---|
SVL0001 |
Error | A requested adapter contract type is not referenced. |
SVL0002 |
Error | A source member collides with a generated adapter member but has an incompatible contract. |
SVL0003 |
Error | More than one inferred view maps to the same view model. |
SVL0004 |
Error | A mapped view is inaccessible, abstract, or has no accessible constructor callable without arguments. |
SVL0005 |
Error | The annotated locator is nested, static, file-local, or not partial. |
SVL0006 |
Error | A configured mapping contract is not an open generic interface or class with exactly one type parameter. |
Attribute options
| Option | Default | Behavior |
|---|---|---|
GenerateViewFactoryMethods |
false |
Emits TryCreateViewExact(Type, out Control?) for framework adapters and custom locators unless a compatible source helper already exists. |
GenerateRuntimeTypeFallbackMethods |
true |
Emits base/interface/open-generic runtime fallback helpers when a legacy or source-declared Build path needs them; generated IDataTemplate.Build does not require them. |
GenerateIViewLocator |
false |
Generates ReactiveUI IViewLocator, all four ResolveView overloads, and automatic IViewFor<TViewModel> compile-time mappings. Requires a ReactiveUI reference in the consumer project. |
GenerateIDataTemplate |
false |
Generates Avalonia IDataTemplate, Build, Match, and customizable build hooks. |
ViewModelMappingContracts |
empty | Infers mappings from configured open generic contracts with one type parameter. |
DataTemplateMatchTypes |
empty | Types accepted by generated Match; when empty, generated maps are checked using exact runtime type. |
[StaticViewMapping(typeof(TViewModel), typeof(TView))] is repeatable and is the final override for a mapped view model. It also admits model types whose names do not end in ViewModel.
The generator emits:
s_views: resolved mappings fromTypetoFunc<Control>s_missingViews: unresolved mappings used for"Not Found: ..."fallback text- optional exact factory creation through
TryCreateViewExact - optional generated ReactiveUI
IViewLocator - optional generated Avalonia
IDataTemplate - runtime helpers for generic type-definition, base-class, and interface fallback only when required or enabled for a source-declared/legacy
Buildpath
By default, the legacy generated lookup order is:
- exact runtime type
- generic type definition for generic runtime types
- base type chain
- implemented interfaces in reverse order
The generated IViewLocator and generated IDataTemplate.Build paths intentionally use exact static lookup. This keeps their resolution predictable and avoids the runtime type walking used by the legacy Build implementation.
Source generator will generate mappings using convention-based transforms. By default:
- namespace
ViewModelsbecomesViews - type suffix
ViewModelbecomesView - generic arity markers are removed from the target view name
- interface prefix
Iis stripped before resolving the target view name
This allows patterns like:
MyApp.ViewModels.SettingsViewModel -> MyApp.Views.SettingsViewMyApp.ViewModels.WidgetViewModel<T> -> MyApp.Views.WidgetViewMyApp.ViewModels.IDetailsViewModel -> MyApp.Views.DetailsView
public partial class ViewLocator
{
private static Dictionary<Type, Func<Control>> s_views = new()
{
[typeof(StaticViewLocatorDemo.ViewModels.TestViewModel)] = () => new StaticViewLocatorDemo.Views.TestView(),
};
private static Dictionary<Type, string> s_missingViews = new()
{
[typeof(StaticViewLocatorDemo.ViewModels.MainWindowViewModel)] = "Not Found: StaticViewLocatorDemo.Views.MainWindowView",
};
}
MSBuild configuration
You can scope which view model namespaces are considered and opt into additional behaviors.
<PropertyGroup>
<StaticViewLocatorViewModelNamespacePrefixes>MyApp.ViewModels,MyApp.Modules</StaticViewLocatorViewModelNamespacePrefixes>
<StaticViewLocatorIncludeInternalViewModels>false</StaticViewLocatorIncludeInternalViewModels>
<StaticViewLocatorIncludeReferencedAssemblies>false</StaticViewLocatorIncludeReferencedAssemblies>
<StaticViewLocatorAdditionalViewBaseTypes>MyApp.Controls.ToolWindowBase</StaticViewLocatorAdditionalViewBaseTypes>
<StaticViewLocatorNamespaceReplacementRules>ViewModels=Views</StaticViewLocatorNamespaceReplacementRules>
<StaticViewLocatorTypeNameReplacementRules>ViewModel=View,Vm=Page</StaticViewLocatorTypeNameReplacementRules>
<StaticViewLocatorStripGenericArityFromViewName>true</StaticViewLocatorStripGenericArityFromViewName>
<StaticViewLocatorInterfacePrefixesToStrip>I</StaticViewLocatorInterfacePrefixesToStrip>
</PropertyGroup>
Defaults and behavior:
StaticViewLocatorViewModelNamespacePrefixesuses comma separators and defaults to all namespaces.StaticViewLocatorIncludeReferencedAssembliesdefaults tofalse. Whentrue, view models from referenced assemblies are included.StaticViewLocatorIncludeInternalViewModelsdefaults tofalse. Whentrue, internal view models from referenced assemblies are included only if the referenced assembly exposes them viaInternalsVisibleTo.StaticViewLocatorAdditionalViewBaseTypesuses comma separators and extends the default view base type list.StaticViewLocatorNamespaceReplacementRulesuses comma separators withfrom=topairs and is applied sequentially to the view-model namespace when deriving the target view namespace. The default includesViewModels=Views.StaticViewLocatorTypeNameReplacementRulesuses comma separators withfrom=topairs and is applied sequentially to the view-model type name when deriving the target view name. The default includesViewModel=View.StaticViewLocatorStripGenericArityFromViewNamedefaults totrue. When enabled, generic arity markers like`1are removed from the derived target view name, soWidgetViewModel<T>can map toWidgetView.StaticViewLocatorInterfacePrefixesToStripuses comma separators and is applied to interface view-model names before looking up the target view. The default includesI.
Use commas for multi-value MSBuild properties. Semicolons are not supported because Roslyn treats ; as an EditorConfig comment marker when exporting CompilerVisibleProperty values, truncating everything after the first semicolon (dotnet/roslyn#51692).
These properties are exported as CompilerVisibleProperty by the package, so analyzers can read them without extra project configuration.
Generator architecture
The incremental pipeline remains split into the original source view-model discovery, analyzer-option ingestion, optional referenced-assembly discovery, locator discovery, and per-locator emission stages. Mapping normalization applies conventions and configured contracts first, then automatic ReactiveUI discovery, then explicit overrides. Adapter analysis is isolated in StaticViewLocatorGenerator.Adapters.cs; it validates framework symbols and source-member compatibility before emitting IDataTemplate or IViewLocator members. Every test-helper generation run compiles the resulting syntax trees, so snapshot assertions cannot pass while generated code is invalid.
Supported resolution features
- Exact type mapping
- Explicit view/view-model mapping overrides
- Compile-time mapping inference from generic MVVM contracts
- Optional exact factory generation for framework adapters
- Optional generated ReactiveUI
IViewLocator - Optional generated Avalonia
IDataTemplate - Customizable generated
Buildpipeline hooks - Optional omission of runtime type-walking helpers in custom locators
- Open generic mapping, for example
WidgetViewModel<T> -> WidgetView - Base-class fallback
- Interface fallback
- Configurable namespace replacement rules
- Configurable type-name replacement rules
- Configurable interface prefix stripping
- Configurable additional allowed view base types
- Optional referenced-assembly scanning
- Optional internal view-model inclusion
Notes
- Convention candidate discovery starts from types whose names end with
ViewModel. Explicit and generic-contract mappings can add other model types. - Missing views do not block fallback resolution. The generator keeps unresolved targets in
s_missingViews, so a derived type can still fall back to a base-class or interface mapping before returning a"Not Found"placeholder in the legacy runtime-fallback path. - If you provide custom replacement rules, they take precedence over the built-in defaults.
- Replacement targets may be empty. For example,
ViewModel=removes theViewModelsuffix without adding a replacement suffix. - Exact factory generation is intentionally separate from runtime fallback. Framework adapters should prefer exact creation when the framework supplies the concrete view-model type.
Default view base types:
Avalonia.Controls.UserControlAvalonia.Controls.Window
Accessibility rules:
- View models in the current compilation are always eligible (subject to namespace prefixes).
- Referenced assembly view models must be public unless
StaticViewLocatorIncludeInternalViewModelsis enabled andInternalsVisibleTois configured. - Locator classes may be public or internal, including in the global namespace, but must be top-level, non-static, non-file-local, and partial.
License
StaticViewLocator is licensed under the MIT license. See LICENSE file for details.
Learn more about Target Frameworks and .NET Standard.
This package has no dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories (3)
Showing the top 3 popular GitHub repositories that depend on StaticViewLocator:
| Repository | Stars |
|---|---|
|
wieslawsoltes/Dock
A docking layout system.
|
|
|
wieslawsoltes/Core2D
A multi-platform data driven 2D diagram editor.
|
|
|
NeilMacMullen/kusto-loco
C# KQL query engine with flexible I/O layers and visualization
|