Last active
February 16, 2025 20:36
-
-
Save peter-mghendi/6201b0ca642a1b24726002e0892438f6 to your computer and use it in GitHub Desktop.
Source-generated ViewLocator for AvaloniaUI.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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