diff --git a/StaticViewLocator.Tests/StaticViewLocator.Tests.csproj b/StaticViewLocator.Tests/StaticViewLocator.Tests.csproj
new file mode 100644
index 0000000..081d394
--- /dev/null
+++ b/StaticViewLocator.Tests/StaticViewLocator.Tests.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net9.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StaticViewLocator.Tests/StaticViewLocatorGeneratorRuntimeTests.cs b/StaticViewLocator.Tests/StaticViewLocatorGeneratorRuntimeTests.cs
new file mode 100644
index 0000000..52f5d88
--- /dev/null
+++ b/StaticViewLocator.Tests/StaticViewLocatorGeneratorRuntimeTests.cs
@@ -0,0 +1,226 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Headless;
+using Avalonia.Headless.XUnit;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Testing;
+using StaticViewLocator;
+using Xunit;
+using PackageIdentity = Microsoft.CodeAnalysis.Testing.PackageIdentity;
+
+namespace StaticViewLocator.Tests;
+
+public class StaticViewLocatorGeneratorRuntimeTests
+{
+ private static readonly ReferenceAssemblies s_referenceAssemblies = ReferenceAssemblies.Net.Net80.AddPackages(
+ ImmutableArray.Create(
+ new PackageIdentity("Avalonia", "11.2.5")));
+
+ [AvaloniaFact]
+ public async Task CreatesRegisteredViewInstances()
+ {
+ const string source = @"
+using System;
+using Avalonia.Controls;
+using StaticViewLocator;
+
+namespace TestApp
+{
+ [StaticViewLocator]
+ public partial class ViewLocator
+ {
+ public static Control Resolve(object vm)
+ {
+ if (vm is null)
+ {
+ throw new ArgumentNullException(nameof(vm));
+ }
+
+ return s_views[vm.GetType()]();
+ }
+ }
+}
+
+namespace TestApp.ViewModels
+{
+ public class SampleViewModel
+ {
+ }
+
+ public class MissingViewModel
+ {
+ }
+}
+
+namespace TestApp.Views
+{
+ public class SampleView : UserControl
+ {
+ }
+}
+";
+
+ var compilation = await CreateCompilationAsync(source);
+ var sourceGenerator = new StaticViewLocatorGenerator().AsSourceGenerator();
+ var driver = CSharpGeneratorDriver.Create(new[] { sourceGenerator }, parseOptions: (CSharpParseOptions)compilation.SyntaxTrees.First().Options);
+ driver.RunGeneratorsAndUpdateCompilation(compilation, out var updatedCompilation, out var diagnostics);
+
+ Assert.Empty(diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error));
+
+ using var peStream = new MemoryStream();
+ var emitResult = updatedCompilation.Emit(peStream);
+ Assert.True(emitResult.Success, string.Join(Environment.NewLine, emitResult.Diagnostics));
+
+ peStream.Seek(0, SeekOrigin.Begin);
+ var assembly = Assembly.Load(peStream.ToArray());
+
+ var locatorType = assembly.GetType("TestApp.ViewLocator") ?? throw new InvalidOperationException("Generated locator type not found.");
+ var resolveMethod = locatorType.GetMethod("Resolve", BindingFlags.Public | BindingFlags.Static) ?? throw new InvalidOperationException("Resolve method not found.");
+ var sampleViewModel = CreateInstance(assembly, "TestApp.ViewModels.SampleViewModel");
+ var missingViewModel = CreateInstance(assembly, "TestApp.ViewModels.MissingViewModel");
+
+ _ = HeadlessUnitTestSession.GetOrStartForAssembly(typeof(StaticViewLocatorGeneratorRuntimeTests).Assembly);
+
+ var sampleControl = (Control)resolveMethod.Invoke(null, new[] { sampleViewModel })!;
+ var missingControl = (Control)resolveMethod.Invoke(null, new[] { missingViewModel })!;
+
+ Assert.Equal("TestApp.Views.SampleView", sampleControl.GetType().FullName);
+ Assert.Equal("Avalonia.Controls.TextBlock", missingControl.GetType().FullName);
+ }
+
+ private static async Task CreateCompilationAsync(string source)
+ {
+ var parseOptions = new CSharpParseOptions();
+ var syntaxTree = CSharpSyntaxTree.ParseText(source, parseOptions);
+ var references = await s_referenceAssemblies.ResolveAsync(LanguageNames.CSharp, default);
+
+ return CSharpCompilation.Create(
+ assemblyName: "TestAssembly",
+ syntaxTrees: new[] { syntaxTree },
+ references: references,
+ options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+ }
+
+ [AvaloniaFact]
+ public async Task ResolvesMultipleViewModelsAndRespectsOrdering()
+ {
+ const string source = @"
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Controls;
+using StaticViewLocator;
+
+namespace Portal
+{
+ [StaticViewLocator]
+ public partial class PortalViewLocator
+ {
+ public static Control Locate(object vm)
+ {
+ if (vm is null)
+ {
+ throw new ArgumentNullException(nameof(vm));
+ }
+
+ return s_views[vm.GetType()]();
+ }
+ }
+}
+
+namespace Portal.ViewModels
+{
+ public abstract class ViewModelBase
+ {
+ }
+
+ public class HomeViewModel : ViewModelBase
+ {
+ }
+
+ public class ReportsViewModel : ViewModelBase
+ {
+ }
+
+ public class SettingsViewModel : ViewModelBase
+ {
+ }
+
+ public abstract class WorkspaceViewModel : ViewModelBase
+ {
+ }
+}
+
+namespace Portal.Views
+{
+ public class HomeView : UserControl
+ {
+ }
+
+ public class ReportsView : UserControl
+ {
+ }
+}
+";
+
+ var compilation = await CreateCompilationAsync(source);
+ var sourceGenerator = new StaticViewLocatorGenerator().AsSourceGenerator();
+ var driver = CSharpGeneratorDriver.Create(new[] { sourceGenerator }, parseOptions: (CSharpParseOptions)compilation.SyntaxTrees.First().Options);
+ driver.RunGeneratorsAndUpdateCompilation(compilation, out var updatedCompilation, out var diagnostics);
+
+ Assert.Empty(diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error));
+
+ using var peStream = new MemoryStream();
+ var emitResult = updatedCompilation.Emit(peStream);
+ Assert.True(emitResult.Success, string.Join(Environment.NewLine, emitResult.Diagnostics));
+
+ peStream.Seek(0, SeekOrigin.Begin);
+ var assembly = Assembly.Load(peStream.ToArray());
+
+ var locatorType = assembly.GetType("Portal.PortalViewLocator") ?? throw new InvalidOperationException("Generated locator type not found.");
+ var locateMethod = locatorType.GetMethod("Locate", BindingFlags.Public | BindingFlags.Static) ?? throw new InvalidOperationException("Locate method not found.");
+ var dictionaryField = locatorType.GetField("s_views", BindingFlags.NonPublic | BindingFlags.Static) ?? throw new InvalidOperationException("Dictionary field not found.");
+ var viewsMap = (Dictionary>)dictionaryField.GetValue(null)!;
+
+ var expectedOrder = new[]
+ {
+ "Portal.ViewModels.HomeViewModel",
+ "Portal.ViewModels.ReportsViewModel",
+ "Portal.ViewModels.SettingsViewModel",
+ };
+
+ Assert.Equal(expectedOrder.Length, viewsMap.Count);
+ Assert.Equal(expectedOrder, viewsMap.Keys.Select(key => key.FullName).ToArray());
+
+ var homeViewModel = CreateInstance(assembly, "Portal.ViewModels.HomeViewModel");
+ var reportsViewModel = CreateInstance(assembly, "Portal.ViewModels.ReportsViewModel");
+ var settingsViewModel = CreateInstance(assembly, "Portal.ViewModels.SettingsViewModel");
+
+ var homeControl = (Control)locateMethod.Invoke(null, new[] { homeViewModel })!;
+ var reportsControl = (Control)locateMethod.Invoke(null, new[] { reportsViewModel })!;
+ var settingsControl = (Control)locateMethod.Invoke(null, new[] { settingsViewModel })!;
+
+ Assert.Equal("Portal.Views.HomeView", homeControl.GetType().FullName);
+ Assert.Equal("Portal.Views.ReportsView", reportsControl.GetType().FullName);
+
+ var fallback = Assert.IsType(settingsControl);
+ Assert.Equal("Not Found: Portal.Views.SettingsView", fallback.Text);
+ Assert.DoesNotContain(viewsMap.Keys, key => key.FullName?.Contains("WorkspaceViewModel", StringComparison.Ordinal) == true);
+ }
+
+ private static object CreateInstance(Assembly assembly, string typeName)
+ {
+ var type = assembly.GetType(typeName, throwOnError: true) ??
+ throw new InvalidOperationException($"Unable to locate type '{typeName}'.");
+
+ return Activator.CreateInstance(type) ??
+ throw new InvalidOperationException($"Unable to instantiate type '{typeName}'.");
+ }
+}
diff --git a/StaticViewLocator.Tests/StaticViewLocatorGeneratorSnapshotTests.cs b/StaticViewLocator.Tests/StaticViewLocatorGeneratorSnapshotTests.cs
new file mode 100644
index 0000000..f0032a0
--- /dev/null
+++ b/StaticViewLocator.Tests/StaticViewLocatorGeneratorSnapshotTests.cs
@@ -0,0 +1,198 @@
+using System.Threading.Tasks;
+using StaticViewLocator.Tests.TestHelpers;
+using Xunit;
+
+namespace StaticViewLocator.Tests;
+
+public class StaticViewLocatorGeneratorSnapshotTests
+{
+ [Fact]
+ public async Task GeneratesAttributeAndLocatorSources()
+ {
+ const string input = @"
+using Avalonia.Controls;
+using StaticViewLocator;
+
+namespace TestApp.ViewModels
+{
+ public abstract class ViewModelBase
+ {
+ }
+
+ public class MainWindowViewModel : ViewModelBase
+ {
+ }
+
+ public class TestViewModel : ViewModelBase
+ {
+ }
+
+ public abstract class IgnoredViewModel : ViewModelBase
+ {
+ }
+}
+
+namespace TestApp.Views
+{
+ public class TestView : UserControl
+ {
+ }
+}
+
+namespace TestApp
+{
+ [StaticViewLocator]
+ public partial class ViewLocator
+ {
+ }
+}
+";
+
+const string expectedAttribute = """
+//
+using System;
+
+namespace StaticViewLocator;
+
+[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
+public sealed class StaticViewLocatorAttribute : Attribute
+{
+}
+
+""";
+
+const string expectedLocator = """
+//
+#nullable enable
+using System;
+using System.Collections.Generic;
+using Avalonia.Controls;
+
+namespace TestApp;
+
+public partial class ViewLocator
+{
+ private static Dictionary> s_views = new()
+ {
+ [typeof(TestApp.ViewModels.MainWindowViewModel)] = () => new TextBlock() { Text = "Not Found: TestApp.Views.MainWindowView" },
+ [typeof(TestApp.ViewModels.TestViewModel)] = () => new TestApp.Views.TestView(),
+ };
+}
+
+""";
+
+ await StaticViewLocatorGeneratorVerifier.VerifyGeneratedSourcesAsync(
+ input,
+ ("StaticViewLocatorAttribute.cs", expectedAttribute),
+ ("ViewLocator_StaticViewLocator.cs", expectedLocator));
+ }
+
+ [Fact]
+ public async Task GeneratesMappingsForMultipleLocators()
+ {
+ const string input = @"
+using Avalonia.Controls;
+using StaticViewLocator;
+
+namespace App.Modules.Admin
+{
+ [StaticViewLocator]
+ public partial class AdminViewLocator
+ {
+ }
+
+ public class AdminDashboardViewModel
+ {
+ }
+
+ public class AdminDashboardView : UserControl
+ {
+ }
+}
+
+namespace App.Modules.Client
+{
+ [StaticViewLocator]
+ public partial class ClientViewLocator
+ {
+ }
+
+ public class ClientDashboardViewModel
+ {
+ }
+
+ public class ClientDashboardView : UserControl
+ {
+ }
+}
+
+namespace App.Modules.Shared
+{
+ public class ActivityLogViewModel
+ {
+ }
+}
+";
+
+const string expectedAttribute = """
+//
+using System;
+
+namespace StaticViewLocator;
+
+[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
+public sealed class StaticViewLocatorAttribute : Attribute
+{
+}
+
+""";
+
+const string expectedAdminLocator = """
+//
+#nullable enable
+using System;
+using System.Collections.Generic;
+using Avalonia.Controls;
+
+namespace App.Modules.Admin;
+
+public partial class AdminViewLocator
+{
+ private static Dictionary> s_views = new()
+ {
+ [typeof(App.Modules.Admin.AdminDashboardViewModel)] = () => new App.Modules.Admin.AdminDashboardView(),
+ [typeof(App.Modules.Client.ClientDashboardViewModel)] = () => new App.Modules.Client.ClientDashboardView(),
+ [typeof(App.Modules.Shared.ActivityLogViewModel)] = () => new TextBlock() { Text = "Not Found: App.Modules.Shared.ActivityLogView" },
+ };
+}
+
+""";
+
+const string expectedClientLocator = """
+//
+#nullable enable
+using System;
+using System.Collections.Generic;
+using Avalonia.Controls;
+
+namespace App.Modules.Client;
+
+public partial class ClientViewLocator
+{
+ private static Dictionary> s_views = new()
+ {
+ [typeof(App.Modules.Admin.AdminDashboardViewModel)] = () => new App.Modules.Admin.AdminDashboardView(),
+ [typeof(App.Modules.Client.ClientDashboardViewModel)] = () => new App.Modules.Client.ClientDashboardView(),
+ [typeof(App.Modules.Shared.ActivityLogViewModel)] = () => new TextBlock() { Text = "Not Found: App.Modules.Shared.ActivityLogView" },
+ };
+}
+
+""";
+
+ await StaticViewLocatorGeneratorVerifier.VerifyGeneratedSourcesAsync(
+ input,
+ ("StaticViewLocatorAttribute.cs", expectedAttribute),
+ ("AdminViewLocator_StaticViewLocator.cs", expectedAdminLocator),
+ ("ClientViewLocator_StaticViewLocator.cs", expectedClientLocator));
+ }
+}
diff --git a/StaticViewLocator.Tests/TestHelpers/StaticViewLocatorGeneratorVerifier.cs b/StaticViewLocator.Tests/TestHelpers/StaticViewLocatorGeneratorVerifier.cs
new file mode 100644
index 0000000..38e7e2a
--- /dev/null
+++ b/StaticViewLocator.Tests/TestHelpers/StaticViewLocatorGeneratorVerifier.cs
@@ -0,0 +1,40 @@
+using System.Collections.Immutable;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Testing;
+using Microsoft.CodeAnalysis.Testing;
+using Microsoft.CodeAnalysis.Testing.Verifiers;
+using Microsoft.CodeAnalysis.Text;
+using StaticViewLocator;
+using PackageIdentity = Microsoft.CodeAnalysis.Testing.PackageIdentity;
+
+namespace StaticViewLocator.Tests.TestHelpers;
+
+internal static class StaticViewLocatorGeneratorVerifier
+{
+ private static readonly ReferenceAssemblies s_referenceAssemblies = ReferenceAssemblies.Net.Net80.AddPackages(
+ ImmutableArray.Create(
+ new PackageIdentity("Avalonia", "11.2.5")));
+
+ public static async Task VerifyGeneratedSourcesAsync(string source, params (string hintName, string source)[] generatedSources)
+ {
+ var test = new Test();
+ test.TestState.Sources.Add(source);
+
+ foreach (var (hintName, generatedSource) in generatedSources)
+ {
+ test.TestState.GeneratedSources.Add((typeof(StaticViewLocatorGenerator), hintName, SourceText.From(generatedSource, Encoding.UTF8)));
+ }
+
+ await test.RunAsync();
+ }
+
+ private sealed class Test : CSharpSourceGeneratorTest
+ {
+ public Test()
+ {
+ ReferenceAssemblies = s_referenceAssemblies;
+ }
+ }
+}
diff --git a/StaticViewLocator.sln b/StaticViewLocator.sln
index 987224b..f69e20d 100644
--- a/StaticViewLocator.sln
+++ b/StaticViewLocator.sln
@@ -1,22 +1,59 @@
-
+
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StaticViewLocator", "StaticViewLocator\StaticViewLocator.csproj", "{44612354-2231-41AA-805A-C9EEF6DB4CF9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StaticViewLocatorDemo", "StaticViewLocatorDemo\StaticViewLocatorDemo.csproj", "{92F9405F-926D-45E7-973C-3370D0F49348}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StaticViewLocator.Tests", "StaticViewLocator.Tests\StaticViewLocator.Tests.csproj", "{B654A784-6F67-4B9C-A138-0D93766D0CBD}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{44612354-2231-41AA-805A-C9EEF6DB4CF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{44612354-2231-41AA-805A-C9EEF6DB4CF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {44612354-2231-41AA-805A-C9EEF6DB4CF9}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {44612354-2231-41AA-805A-C9EEF6DB4CF9}.Debug|x64.Build.0 = Debug|Any CPU
+ {44612354-2231-41AA-805A-C9EEF6DB4CF9}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {44612354-2231-41AA-805A-C9EEF6DB4CF9}.Debug|x86.Build.0 = Debug|Any CPU
{44612354-2231-41AA-805A-C9EEF6DB4CF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{44612354-2231-41AA-805A-C9EEF6DB4CF9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {44612354-2231-41AA-805A-C9EEF6DB4CF9}.Release|x64.ActiveCfg = Release|Any CPU
+ {44612354-2231-41AA-805A-C9EEF6DB4CF9}.Release|x64.Build.0 = Release|Any CPU
+ {44612354-2231-41AA-805A-C9EEF6DB4CF9}.Release|x86.ActiveCfg = Release|Any CPU
+ {44612354-2231-41AA-805A-C9EEF6DB4CF9}.Release|x86.Build.0 = Release|Any CPU
{92F9405F-926D-45E7-973C-3370D0F49348}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{92F9405F-926D-45E7-973C-3370D0F49348}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {92F9405F-926D-45E7-973C-3370D0F49348}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {92F9405F-926D-45E7-973C-3370D0F49348}.Debug|x64.Build.0 = Debug|Any CPU
+ {92F9405F-926D-45E7-973C-3370D0F49348}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {92F9405F-926D-45E7-973C-3370D0F49348}.Debug|x86.Build.0 = Debug|Any CPU
{92F9405F-926D-45E7-973C-3370D0F49348}.Release|Any CPU.ActiveCfg = Release|Any CPU
{92F9405F-926D-45E7-973C-3370D0F49348}.Release|Any CPU.Build.0 = Release|Any CPU
+ {92F9405F-926D-45E7-973C-3370D0F49348}.Release|x64.ActiveCfg = Release|Any CPU
+ {92F9405F-926D-45E7-973C-3370D0F49348}.Release|x64.Build.0 = Release|Any CPU
+ {92F9405F-926D-45E7-973C-3370D0F49348}.Release|x86.ActiveCfg = Release|Any CPU
+ {92F9405F-926D-45E7-973C-3370D0F49348}.Release|x86.Build.0 = Release|Any CPU
+ {B654A784-6F67-4B9C-A138-0D93766D0CBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B654A784-6F67-4B9C-A138-0D93766D0CBD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B654A784-6F67-4B9C-A138-0D93766D0CBD}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B654A784-6F67-4B9C-A138-0D93766D0CBD}.Debug|x64.Build.0 = Debug|Any CPU
+ {B654A784-6F67-4B9C-A138-0D93766D0CBD}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B654A784-6F67-4B9C-A138-0D93766D0CBD}.Debug|x86.Build.0 = Debug|Any CPU
+ {B654A784-6F67-4B9C-A138-0D93766D0CBD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B654A784-6F67-4B9C-A138-0D93766D0CBD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B654A784-6F67-4B9C-A138-0D93766D0CBD}.Release|x64.ActiveCfg = Release|Any CPU
+ {B654A784-6F67-4B9C-A138-0D93766D0CBD}.Release|x64.Build.0 = Release|Any CPU
+ {B654A784-6F67-4B9C-A138-0D93766D0CBD}.Release|x86.ActiveCfg = Release|Any CPU
+ {B654A784-6F67-4B9C-A138-0D93766D0CBD}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
diff --git a/StaticViewLocator/StaticViewLocatorGenerator.cs b/StaticViewLocator/StaticViewLocatorGenerator.cs
index f3522b0..bfd7997 100644
--- a/StaticViewLocator/StaticViewLocatorGenerator.cs
+++ b/StaticViewLocator/StaticViewLocatorGenerator.cs
@@ -1,5 +1,6 @@
+using System;
using System.Collections.Generic;
-using System.Linq;
+using System.Collections.Immutable;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
@@ -8,164 +9,150 @@
namespace StaticViewLocator;
-[Generator]
-public class StaticViewLocatorGenerator : ISourceGenerator
+[Generator(LanguageNames.CSharp)]
+public sealed class StaticViewLocatorGenerator : IIncrementalGenerator
{
- private const string StaticViewLocatorAttributeDisplayString = "StaticViewLocator.StaticViewLocatorAttribute";
-
- private const string ViewModelSuffix = "ViewModel";
-
- private const string ViewSuffix = "View";
-
- private const string AttributeText =
- """
- //
- using System;
-
- namespace StaticViewLocator;
-
- [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
- public sealed class StaticViewLocatorAttribute : Attribute
- {
- }
-
- """;
-
- public void Initialize(GeneratorInitializationContext context)
- {
- // System.Diagnostics.Debugger.Launch();
- context.RegisterForPostInitialization((i) => i.AddSource("StaticViewLocatorAttribute.cs", SourceText.From(AttributeText, Encoding.UTF8)));
-
- context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
- }
-
- public void Execute(GeneratorExecutionContext context)
- {
- if (context.SyntaxContextReceiver is not SyntaxReceiver receiver)
- {
- return;
- }
-
- var attributeSymbol = context.Compilation.GetTypeByMetadataName(StaticViewLocatorAttributeDisplayString);
- if (attributeSymbol is null)
- {
- return;
- }
-
- foreach (var namedTypeSymbol in receiver.NamedTypeSymbolLocators)
- {
- var namedTypeSymbolViewModels = receiver.NamedTypeSymbolViewModels.ToList();
- namedTypeSymbolViewModels.Sort((x, y) => x.ToDisplayString().CompareTo(y.ToDisplayString()));
-
- var classSource = ProcessClass(context.Compilation, namedTypeSymbol, namedTypeSymbolViewModels);
- if (classSource is not null)
- {
- context.AddSource($"{namedTypeSymbol.Name}_StaticViewLocator.cs", SourceText.From(classSource, Encoding.UTF8));
- }
- }
- }
-
- private static string? ProcessClass(Compilation compilation, INamedTypeSymbol namedTypeSymbolLocator, List namedTypeSymbolViewModels)
- {
- if (!namedTypeSymbolLocator.ContainingSymbol.Equals(namedTypeSymbolLocator.ContainingNamespace, SymbolEqualityComparer.Default))
- {
- return null;
- }
-
- string namespaceNameLocator = namedTypeSymbolLocator.ContainingNamespace.ToDisplayString();
-
- var format = new SymbolDisplayFormat(
- typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes,
- genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters | SymbolDisplayGenericsOptions.IncludeTypeConstraints | SymbolDisplayGenericsOptions.IncludeVariance);
-
- string classNameLocator = namedTypeSymbolLocator.ToDisplayString(format);
-
- var source = new StringBuilder(
- $$"""
- //
- #nullable enable
- using System;
- using System.Collections.Generic;
- using Avalonia.Controls;
-
- namespace {{namespaceNameLocator}};
-
- public partial class {{classNameLocator}}
- {
- """);
-
- source.Append(
- """
-
- private static Dictionary> s_views = new()
- {
-
- """);
-
- var userControlViewSymbol = compilation.GetTypeByMetadataName("Avalonia.Controls.UserControl");
-
- foreach (var namedTypeSymbolViewModel in namedTypeSymbolViewModels)
- {
- string namespaceNameViewModel = namedTypeSymbolViewModel.ContainingNamespace.ToDisplayString();
- string classNameViewModel = $"{namespaceNameViewModel}.{namedTypeSymbolViewModel.ToDisplayString(format)}";
- string classNameView = classNameViewModel.Replace(ViewModelSuffix, ViewSuffix);
-
- var classNameViewSymbol = compilation.GetTypeByMetadataName(classNameView);
- if (classNameViewSymbol is null || classNameViewSymbol.BaseType?.Equals(userControlViewSymbol, SymbolEqualityComparer.Default) != true)
- {
- source.AppendLine(
- $$"""
- [typeof({{classNameViewModel}})] = () => new TextBlock() { Text = {{("\"Not Found: " + classNameView + "\"")}} },
- """);
- }
- else
- {
- source.AppendLine(
- $$"""
- [typeof({{classNameViewModel}})] = () => new {{classNameView}}(),
- """);
- }
- }
-
- source.Append(
- """
- };
- }
-
- """);
-
- return source.ToString();
- }
-
- private class SyntaxReceiver : ISyntaxContextReceiver
- {
- public List NamedTypeSymbolLocators { get; } = new();
-
- public List NamedTypeSymbolViewModels { get; } = new();
-
- public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
- {
- if (context.Node is ClassDeclarationSyntax classDeclarationSyntax)
- {
- var namedTypeSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax);
- if (namedTypeSymbol is null)
- {
- return;
- }
-
- var attributes = namedTypeSymbol.GetAttributes();
- if (attributes.Any(ad => ad?.AttributeClass?.ToDisplayString() == StaticViewLocatorAttributeDisplayString))
- {
- NamedTypeSymbolLocators.Add(namedTypeSymbol);
- }
- else if (namedTypeSymbol.Name.EndsWith(ViewModelSuffix))
- {
- if (!namedTypeSymbol.IsAbstract)
- {
- NamedTypeSymbolViewModels.Add(namedTypeSymbol);
- }
- }
- }
- }
- }
+ private const string StaticViewLocatorAttributeDisplayString = "StaticViewLocator.StaticViewLocatorAttribute";
+ private const string ViewModelSuffix = "ViewModel";
+ private const string ViewSuffix = "View";
+
+ private const string AttributeText =
+ """
+ //
+ using System;
+
+ namespace StaticViewLocator;
+
+ [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
+ public sealed class StaticViewLocatorAttribute : Attribute
+ {
+ }
+
+ """;
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ context.RegisterPostInitializationOutput(static ctx =>
+ ctx.AddSource("StaticViewLocatorAttribute.cs", SourceText.From(AttributeText, Encoding.UTF8)));
+
+ var viewModelsProvider = context.SyntaxProvider
+ .CreateSyntaxProvider(
+ static (node, _) => node is ClassDeclarationSyntax classDeclaration &&
+ classDeclaration.Identifier.ValueText.EndsWith(ViewModelSuffix, StringComparison.Ordinal),
+ static (generatorContext, cancellationToken) =>
+ {
+ var classDeclaration = (ClassDeclarationSyntax)generatorContext.Node;
+ if (generatorContext.SemanticModel.GetDeclaredSymbol(classDeclaration, cancellationToken) is not INamedTypeSymbol symbol)
+ {
+ return null;
+ }
+
+ return symbol.IsAbstract ? null : symbol;
+ })
+ .Where(static symbol => symbol is not null)
+ .Select(static (symbol, _) => symbol!)
+ .Collect();
+
+ var locatorsProvider = context.SyntaxProvider.ForAttributeWithMetadataName(
+ StaticViewLocatorAttributeDisplayString,
+ static (node, _) => node is ClassDeclarationSyntax,
+ static (attributeContext, _) => (INamedTypeSymbol)attributeContext.TargetSymbol);
+
+ var inputs = locatorsProvider
+ .Combine(context.CompilationProvider)
+ .Combine(viewModelsProvider);
+
+ context.RegisterSourceOutput(inputs, static (sourceProductionContext, tuple) =>
+ {
+ var ((locatorSymbol, compilation), viewModelSymbols) = tuple;
+
+ var classSource = ProcessClass(compilation, locatorSymbol, viewModelSymbols);
+ if (classSource is not null)
+ {
+ sourceProductionContext.AddSource(
+ $"{locatorSymbol.Name}_StaticViewLocator.cs",
+ SourceText.From(classSource, Encoding.UTF8));
+ }
+ });
+ }
+
+ private static string? ProcessClass(Compilation compilation, INamedTypeSymbol locatorSymbol, ImmutableArray viewModelSymbols)
+ {
+ if (!locatorSymbol.ContainingSymbol.Equals(locatorSymbol.ContainingNamespace, SymbolEqualityComparer.Default))
+ {
+ return null;
+ }
+
+ var namespaceNameLocator = locatorSymbol.ContainingNamespace.ToDisplayString();
+
+ var format = new SymbolDisplayFormat(
+ typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes,
+ genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters |
+ SymbolDisplayGenericsOptions.IncludeTypeConstraints |
+ SymbolDisplayGenericsOptions.IncludeVariance);
+
+ var classNameLocator = locatorSymbol.ToDisplayString(format);
+
+ var relevantViewModels = new List();
+ var seen = new HashSet(SymbolEqualityComparer.Default);
+
+ foreach (var symbol in viewModelSymbols)
+ {
+ if (!symbol.Name.EndsWith(ViewModelSuffix, StringComparison.Ordinal) || symbol.IsAbstract)
+ {
+ continue;
+ }
+
+ if (seen.Add(symbol))
+ {
+ relevantViewModels.Add(symbol);
+ }
+ }
+
+ relevantViewModels.Sort(static (left, right) =>
+ string.Compare(left.ToDisplayString(), right.ToDisplayString(), StringComparison.Ordinal));
+
+ var source = new StringBuilder(
+ $$"""
+ //
+ #nullable enable
+ using System;
+ using System.Collections.Generic;
+ using Avalonia.Controls;
+
+ namespace {{namespaceNameLocator}};
+
+ public partial class {{classNameLocator}}
+ {
+ """);
+
+ source.AppendLine();
+ source.AppendLine("\tprivate static Dictionary> s_views = new()");
+ source.AppendLine("\t{");
+
+ var userControlViewSymbol = compilation.GetTypeByMetadataName("Avalonia.Controls.UserControl");
+
+ foreach (var viewModelSymbol in relevantViewModels)
+ {
+ var namespaceNameViewModel = viewModelSymbol.ContainingNamespace.ToDisplayString();
+ var classNameViewModel = $"{namespaceNameViewModel}.{viewModelSymbol.ToDisplayString(format)}";
+ var classNameView = classNameViewModel.Replace(ViewModelSuffix, ViewSuffix);
+
+ var viewSymbol = compilation.GetTypeByMetadataName(classNameView);
+ if (viewSymbol is null || viewSymbol.BaseType?.Equals(userControlViewSymbol, SymbolEqualityComparer.Default) != true)
+ {
+ source.AppendLine(
+ $"\t\t[typeof({classNameViewModel})] = () => new TextBlock() {{ Text = \"Not Found: {classNameView}\" }},");
+ }
+ else
+ {
+ source.AppendLine($"\t\t[typeof({classNameViewModel})] = () => new {classNameView}(),");
+ }
+ }
+
+ source.AppendLine("\t};");
+ source.AppendLine("}");
+
+ return source.ToString();
+ }
}
diff --git a/StaticViewLocatorDemo/ViewModels/DashboardViewModel.cs b/StaticViewLocatorDemo/ViewModels/DashboardViewModel.cs
new file mode 100644
index 0000000..6854f15
--- /dev/null
+++ b/StaticViewLocatorDemo/ViewModels/DashboardViewModel.cs
@@ -0,0 +1,23 @@
+using System.Collections.Generic;
+
+namespace StaticViewLocatorDemo.ViewModels;
+
+public class DashboardViewModel : ViewModelBase
+{
+ public DashboardViewModel()
+ {
+ Metrics = new List
+ {
+ new("Active Users", "people", 1280, "Users currently signed in across all platforms."),
+ new("Conversion", "%", 3.7, "Percentage of visitors completing the onboarding journey."),
+ new("Support Tickets", "open", 5, "Items waiting in the support queue."),
+ new("Build Duration", "min", 7.4, "Average CI build time over the last 24 hours."),
+ };
+ }
+
+ public override string Title => "Dashboard";
+
+ public string Summary => "A snapshot of the most important application metrics.";
+
+ public IReadOnlyList Metrics { get; }
+}
diff --git a/StaticViewLocatorDemo/ViewModels/MainWindowViewModel.cs b/StaticViewLocatorDemo/ViewModels/MainWindowViewModel.cs
index 65c0939..bb2c688 100644
--- a/StaticViewLocatorDemo/ViewModels/MainWindowViewModel.cs
+++ b/StaticViewLocatorDemo/ViewModels/MainWindowViewModel.cs
@@ -1,6 +1,30 @@
-namespace StaticViewLocatorDemo.ViewModels;
+using System.Collections.Generic;
+using ReactiveUI;
+
+namespace StaticViewLocatorDemo.ViewModels;
public class MainWindowViewModel : ViewModelBase
{
- public TestViewModel TestViewModel { get; } = new ();
+ private ViewModelBase _selectedPage;
+
+ public MainWindowViewModel()
+ {
+ Pages = new ViewModelBase[]
+ {
+ new DashboardViewModel(),
+ new TestViewModel(),
+ new SettingsViewModel(),
+ new ReportsViewModel(),
+ };
+
+ _selectedPage = Pages[0];
+ }
+
+ public IReadOnlyList Pages { get; }
+
+ public ViewModelBase SelectedPage
+ {
+ get => _selectedPage;
+ set => this.RaiseAndSetIfChanged(ref _selectedPage, value);
+ }
}
diff --git a/StaticViewLocatorDemo/ViewModels/MetricViewModel.cs b/StaticViewLocatorDemo/ViewModels/MetricViewModel.cs
new file mode 100644
index 0000000..eaa90d0
--- /dev/null
+++ b/StaticViewLocatorDemo/ViewModels/MetricViewModel.cs
@@ -0,0 +1,22 @@
+namespace StaticViewLocatorDemo.ViewModels;
+
+public class MetricViewModel : ViewModelBase
+{
+ public MetricViewModel(string name, string unit, double value, string description)
+ {
+ Name = name;
+ Unit = unit;
+ Value = value;
+ Description = description;
+ }
+
+ public string Name { get; }
+
+ public string Unit { get; }
+
+ public double Value { get; }
+
+ public string Description { get; }
+
+ public override string Title => Name;
+}
diff --git a/StaticViewLocatorDemo/ViewModels/ReportViewModel.cs b/StaticViewLocatorDemo/ViewModels/ReportViewModel.cs
new file mode 100644
index 0000000..d502312
--- /dev/null
+++ b/StaticViewLocatorDemo/ViewModels/ReportViewModel.cs
@@ -0,0 +1,22 @@
+using System;
+
+namespace StaticViewLocatorDemo.ViewModels;
+
+public class ReportViewModel : ViewModelBase
+{
+ public ReportViewModel(string title, string summary, DateTime lastGenerated, string status)
+ {
+ Title = title;
+ Summary = summary;
+ LastGenerated = lastGenerated;
+ Status = status;
+ }
+
+ public override string Title { get; }
+
+ public string Summary { get; }
+
+ public DateTime LastGenerated { get; }
+
+ public string Status { get; }
+}
diff --git a/StaticViewLocatorDemo/ViewModels/ReportsViewModel.cs b/StaticViewLocatorDemo/ViewModels/ReportsViewModel.cs
new file mode 100644
index 0000000..95297c0
--- /dev/null
+++ b/StaticViewLocatorDemo/ViewModels/ReportsViewModel.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+
+namespace StaticViewLocatorDemo.ViewModels;
+
+public class ReportsViewModel : ViewModelBase
+{
+ public ReportsViewModel()
+ {
+ Reports = new List
+ {
+ new("Usage", "Daily active user counts split by platform.", DateTime.Now.AddMinutes(-32), "Up to date"),
+ new("Infrastructure", "Server health with CPU and memory trends.", DateTime.Now.AddHours(-3), "Requires review"),
+ new("Commerce", "Payments accepted and refunds processed in the last week.", DateTime.Now.AddDays(-1), "Regenerating"),
+ };
+ }
+
+ public override string Title => "Reports";
+
+ public IReadOnlyList Reports { get; }
+}
diff --git a/StaticViewLocatorDemo/ViewModels/SettingsViewModel.cs b/StaticViewLocatorDemo/ViewModels/SettingsViewModel.cs
new file mode 100644
index 0000000..fe4b120
--- /dev/null
+++ b/StaticViewLocatorDemo/ViewModels/SettingsViewModel.cs
@@ -0,0 +1,30 @@
+using ReactiveUI;
+
+namespace StaticViewLocatorDemo.ViewModels;
+
+public class SettingsViewModel : ViewModelBase
+{
+ private bool _useDarkTheme = true;
+ private bool _enableAnimations = true;
+ private bool _receiveReleaseNotifications = true;
+
+ public override string Title => "Settings";
+
+ public bool UseDarkTheme
+ {
+ get => _useDarkTheme;
+ set => this.RaiseAndSetIfChanged(ref _useDarkTheme, value);
+ }
+
+ public bool EnableAnimations
+ {
+ get => _enableAnimations;
+ set => this.RaiseAndSetIfChanged(ref _enableAnimations, value);
+ }
+
+ public bool ReceiveReleaseNotifications
+ {
+ get => _receiveReleaseNotifications;
+ set => this.RaiseAndSetIfChanged(ref _receiveReleaseNotifications, value);
+ }
+}
diff --git a/StaticViewLocatorDemo/ViewModels/TestViewModel.cs b/StaticViewLocatorDemo/ViewModels/TestViewModel.cs
index f89c6eb..98807a0 100644
--- a/StaticViewLocatorDemo/ViewModels/TestViewModel.cs
+++ b/StaticViewLocatorDemo/ViewModels/TestViewModel.cs
@@ -2,5 +2,9 @@ namespace StaticViewLocatorDemo.ViewModels;
public class TestViewModel : ViewModelBase
{
+ public override string Title => "Welcome";
+
public string Greeting => "Welcome to Avalonia!";
+
+ public string Description => "This page is produced entirely via the static view locator.";
}
diff --git a/StaticViewLocatorDemo/ViewModels/ViewModelBase.cs b/StaticViewLocatorDemo/ViewModels/ViewModelBase.cs
index 0dc5e00..9b98710 100644
--- a/StaticViewLocatorDemo/ViewModels/ViewModelBase.cs
+++ b/StaticViewLocatorDemo/ViewModels/ViewModelBase.cs
@@ -1,7 +1,9 @@
-using ReactiveUI;
+using System;
+using ReactiveUI;
namespace StaticViewLocatorDemo.ViewModels;
public class ViewModelBase : ReactiveObject
{
+ public virtual string Title => GetType().Name.Replace("ViewModel", string.Empty, StringComparison.Ordinal);
}
diff --git a/StaticViewLocatorDemo/Views/DashboardView.axaml b/StaticViewLocatorDemo/Views/DashboardView.axaml
new file mode 100644
index 0000000..60b42ec
--- /dev/null
+++ b/StaticViewLocatorDemo/Views/DashboardView.axaml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StaticViewLocatorDemo/Views/DashboardView.axaml.cs b/StaticViewLocatorDemo/Views/DashboardView.axaml.cs
new file mode 100644
index 0000000..82d1167
--- /dev/null
+++ b/StaticViewLocatorDemo/Views/DashboardView.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace StaticViewLocatorDemo.Views;
+
+public partial class DashboardView : UserControl
+{
+ public DashboardView()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/StaticViewLocatorDemo/Views/MainWindow.axaml b/StaticViewLocatorDemo/Views/MainWindow.axaml
index c7e552c..14ce15e 100644
--- a/StaticViewLocatorDemo/Views/MainWindow.axaml
+++ b/StaticViewLocatorDemo/Views/MainWindow.axaml
@@ -3,16 +3,28 @@
xmlns:vm="using:StaticViewLocatorDemo.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
- mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+ mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="560"
x:Class="StaticViewLocatorDemo.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Icon="/Assets/avalonia-logo.ico"
- Title="StaticViewLocatorDemo">
+ Title="Static View Locator Demo">
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StaticViewLocatorDemo/Views/MetricView.axaml b/StaticViewLocatorDemo/Views/MetricView.axaml
new file mode 100644
index 0000000..f7ec510
--- /dev/null
+++ b/StaticViewLocatorDemo/Views/MetricView.axaml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StaticViewLocatorDemo/Views/MetricView.axaml.cs b/StaticViewLocatorDemo/Views/MetricView.axaml.cs
new file mode 100644
index 0000000..a666ac8
--- /dev/null
+++ b/StaticViewLocatorDemo/Views/MetricView.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace StaticViewLocatorDemo.Views;
+
+public partial class MetricView : UserControl
+{
+ public MetricView()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/StaticViewLocatorDemo/Views/ReportView.axaml b/StaticViewLocatorDemo/Views/ReportView.axaml
new file mode 100644
index 0000000..3de45b1
--- /dev/null
+++ b/StaticViewLocatorDemo/Views/ReportView.axaml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StaticViewLocatorDemo/Views/ReportView.axaml.cs b/StaticViewLocatorDemo/Views/ReportView.axaml.cs
new file mode 100644
index 0000000..69146d9
--- /dev/null
+++ b/StaticViewLocatorDemo/Views/ReportView.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace StaticViewLocatorDemo.Views;
+
+public partial class ReportView : UserControl
+{
+ public ReportView()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/StaticViewLocatorDemo/Views/ReportsView.axaml b/StaticViewLocatorDemo/Views/ReportsView.axaml
new file mode 100644
index 0000000..bf84c36
--- /dev/null
+++ b/StaticViewLocatorDemo/Views/ReportsView.axaml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StaticViewLocatorDemo/Views/ReportsView.axaml.cs b/StaticViewLocatorDemo/Views/ReportsView.axaml.cs
new file mode 100644
index 0000000..70bb28c
--- /dev/null
+++ b/StaticViewLocatorDemo/Views/ReportsView.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace StaticViewLocatorDemo.Views;
+
+public partial class ReportsView : UserControl
+{
+ public ReportsView()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/StaticViewLocatorDemo/Views/SettingsView.axaml b/StaticViewLocatorDemo/Views/SettingsView.axaml
new file mode 100644
index 0000000..7883d5c
--- /dev/null
+++ b/StaticViewLocatorDemo/Views/SettingsView.axaml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
diff --git a/StaticViewLocatorDemo/Views/SettingsView.axaml.cs b/StaticViewLocatorDemo/Views/SettingsView.axaml.cs
new file mode 100644
index 0000000..46562b7
--- /dev/null
+++ b/StaticViewLocatorDemo/Views/SettingsView.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace StaticViewLocatorDemo.Views;
+
+public partial class SettingsView : UserControl
+{
+ public SettingsView()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/StaticViewLocatorDemo/Views/TestView.axaml b/StaticViewLocatorDemo/Views/TestView.axaml
index 22de57c..d697fe4 100644
--- a/StaticViewLocatorDemo/Views/TestView.axaml
+++ b/StaticViewLocatorDemo/Views/TestView.axaml
@@ -2,7 +2,14 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:vm="using:StaticViewLocatorDemo.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
- x:Class="StaticViewLocatorDemo.Views.TestView">
- Welcome to reflection free world!
+ x:Class="StaticViewLocatorDemo.Views.TestView"
+ x:DataType="vm:TestViewModel">
+
+
+
+
+
+