Skip to content

Instantly share code, notes, and snippets.

@peter-mghendi
Last active February 16, 2025 20:36
Show Gist options
  • Select an option

  • Save peter-mghendi/6201b0ca642a1b24726002e0892438f6 to your computer and use it in GitHub Desktop.

Select an option

Save peter-mghendi/6201b0ca642a1b24726002e0892438f6 to your computer and use it in GitHub Desktop.
Source-generated ViewLocator for AvaloniaUI.
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:themes="clr-namespace:Material.Styles.Themes;assembly=Material.Styles"
xmlns:dialogs="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
xmlns:infrastructure="clr-namespace:Gringotts.Infrastructure"
x:Class="Gringotts.App"
RequestedThemeVariant="Default">
<Application.DataTemplates>
<infrastructure:SourceGeneratedViewLocator/>
</Application.DataTemplates>
<Application.Styles>
<themes:MaterialTheme BaseTheme="Dark" PrimaryColor="Teal" SecondaryColor="Cyan" />
<dialogs:DialogHostStyles />
</Application.Styles>
</Application>
using Gringotts.Bidirectional.DataBinding;
using Gringotts.Bidirectional.Infrastructure;
using Gringotts.Bidirectional.Routing;
using Gringotts.Views;
namespace Gringotts.ViewModels;
[ViewModelFor<MainView>]
public partial class MainViewModel(Navigator navigator) : ViewModelBase
{
public Navigator Navigator { get; } = navigator;
}
using Avalonia.Controls;
using Gringotts.Bidirectional.DataBinding;
using Gringotts.Bidirectional.Infrastructure;
namespace Gringotts.Infrastructure;
public partial class SourceGeneratedViewLocator : ViewLocator<ViewModelBase>
{
protected override partial Control Create(ViewModelBase vm);
}
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Gringotts.Bidirectional.DataBinding;
namespace Gringotts.Bidirectional.Infrastructure;
public abstract class ViewLocator<TViewModel> : IDataTemplate where TViewModel : ViewModelBase
{
protected abstract Control Create(TViewModel vm);
protected virtual Control CreateError(string message) => new TextBlock { Text = message };
public Control Build(object? data) => data switch
{
TViewModel vm => Create(vm),
_ => CreateError("Invalid input")
};
public bool Match(object? data) => data is TViewModel;
}
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using Gringotts.Lattice.Internal;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using Mapping = (string ViewModel, string View);
namespace Gringotts.Lattice.Generators;
[Generator]
public class ViewLocatorGenerator : IIncrementalGenerator
{
private const string Output = "SourceGeneratedViewLocator.g.cs";
private const string ViewModelFor = nameof(ViewModelFor);
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var attrs = static (ClassDeclarationSyntax @class) =>
from list in @class.AttributeLists
from attr in list.Attributes
where attr.Name is GenericNameSyntax { Identifier.ValueText: ViewModelFor }
select attr;
var provider = context.SyntaxProvider.CreateSyntaxProvider(
predicate: (node, _) => node is ClassDeclarationSyntax @class && attrs(@class).Any(),
transform: (ctx, _) =>
{
var @class = (ClassDeclarationSyntax)ctx.Node;
var semantics = ctx.SemanticModel;
var attribute = attrs(@class).First();
var generic = ((GenericNameSyntax)attribute.Name).TypeArgumentList.Arguments.First();
var model = semantics.GetDeclaredSymbol(@class);
var view = semantics.GetTypeInfo(generic).Type;
return (ViewModel: model!.ToDisplayString(), View: view!.ToDisplayString());
}
).Collect();
context.RegisterSourceOutput(provider, Execute);
}
private static void Execute(SourceProductionContext context, ImmutableArray<Mapping> pairs)
{
if (ValidateMappings(pairs) is { } diagnostic)
{
// Fail gracefully; report error and generate an empty dictionary to avoid cascading errors in console.
context.ReportDiagnostic(diagnostic);
context.AddSource(Output, SourceText.From(BuildSource([]), Encoding.UTF8));
return;
}
var mapping = from pair in pairs select $"[typeof({pair.ViewModel})] = () => new {pair.View}()";
context.AddSource(Output, SourceText.From(BuildSource(mapping), Encoding.UTF8));
}
private static Diagnostic? ValidateMappings(ImmutableArray<Mapping> pairs)
{
var query = from pair in pairs
group pair by pair.View
into refs
where refs.Any()
select (View: refs.Key, References: from @ref in refs select @ref.ViewModel);
return query.ToList() switch
{
[var (view, models), _] when string.Join(", ", models) is [_, ..] references =>
Diagnostic.Create(Diagnostics.ViewLocator.DuplicateView, Location.None, view, references),
_ => null
};
}
private static string BuildSource(IEnumerable<string> lines) => new StringBuilder(
$$"""
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using Avalonia.Controls;
namespace Gringotts.Infrastructure;
[global::System.CodeDom.Compiler.GeneratedCode(tool: "Gringotts.Lattice", version: "1.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public partial class SourceGeneratedViewLocator
{
private static readonly Lazy<FrozenDictionary<Type, Func<Control>>> ViewFactories = new(CreateViewFactories);
private static FrozenDictionary<Type, Func<Control>> CreateViewFactories()
{
return new Dictionary<Type, Func<Control>>
{
{{string.Join(",\n\t\t\t", lines)}}
}.ToFrozenDictionary();
}
protected override partial Control Create(Gringotts.Bidirectional.DataBinding.ViewModelBase vm)
{
if (!ViewFactories.Value.TryGetValue(vm.GetType(), out var factory))
{
return CreateError($"View not found for {vm.GetType().FullName}");
}
try
{
return factory();
}
catch (Exception ex)
{
return CreateError($"Error creating view: {ex.Message}");
}
}
}
"""
).ToString();
}
using CommunityToolkit.Mvvm.ComponentModel;
namespace Gringotts.Bidirectional.DataBinding;
public abstract class ViewModelBase : ObservableObject;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment