A C# source generator that automatically implements static view locator for Avalonia without using reflection.
Add NuGet package reference to project.
<PackageReference Include="StaticViewLocator" Version="0.3.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;
}
}The generator emits:
s_views: resolved mappings fromTypetoFunc<Control>s_missingViews: unresolved mappings used for"Not Found: ..."fallback text- helper methods for exact type lookup, generic type-definition lookup, base-class fallback, and interface fallback
By default, the generated lookup order is:
- exact runtime type
- generic type definition for generic runtime types
- base type chain
- implemented interfaces in reverse order
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",
};
}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;or,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;or,separators and extends the default view base type list.StaticViewLocatorNamespaceReplacementRulesuses;or,separators withfrom=topairs and is applied sequentially to the view-model namespace when deriving the target view namespace. The default includesViewModels=Views.StaticViewLocatorTypeNameReplacementRulesuses;or,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;or,separators and is applied to interface view-model names before looking up the target view. The default includesI.
These properties are exported as CompilerVisibleProperty by the package, so analyzers can read them without extra project configuration.
- Exact type mapping
- 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
- Candidate discovery still starts from types whose names end with
ViewModel. - 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. - If you provide custom replacement rules, they take precedence over the built-in defaults.
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.
StaticViewLocator is licensed under the MIT license. See LICENSE file for details.