Skip to content
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
db71c82
Add EnumDescriptionConverter
BillyMartin1964 Mar 9, 2026
64a31a4
Merge branch 'EnumDescriptionConverter'
BillyMartin1964 Mar 10, 2026
ee49e0f
Update EnumDescriptionConverter to support DisplayAttribute localization
BillyMartin1964 Mar 10, 2026
2b2d254
Merge branch 'EnumDescriptionConverter'
BillyMartin1964 Mar 10, 2026
17ecc76
Update Enum converter tests to use BaseOneWayConverterTest
BillyMartin1964 Mar 10, 2026
eb5927b
Merge branch 'EnumDescriptionConverter'
BillyMartin1964 Mar 10, 2026
745cfc3
Update Enum converter tests to use ICommunityToolkitValueConverter
BillyMartin1964 Mar 10, 2026
6384d8a
Merge branch 'EnumDescriptionConverter'
BillyMartin1964 Mar 10, 2026
7cf4bf7
Add EnumDescriptionConverter
BillyMartin1964 Mar 11, 2026
94edbf7
Add EnumDescriptionConverter
BillyMartin1964 Mar 11, 2026
1ff4d6a
Refactor EnumDescriptionConverter UI elements with styling and spacin…
BillyMartin1964 Mar 11, 2026
120e642
Merge branch 'RefactorToRemoveReflection'
BillyMartin1964 Mar 11, 2026
121742e
Update EnumDescriptionGenerator.cs
TheCodeTraveler Mar 11, 2026
d0afca0
Merge branch 'main' into main
TheCodeTraveler Mar 11, 2026
f18eff0
Refactor EnumDescriptionConverter UI Elements for Improved Readability
BillyMartin1964 Mar 11, 2026
295496b
Refactor EnumDescriptionConverter UI elements for improved readability
BillyMartin1964 Mar 11, 2026
150791c
Merge branch 'main' into RefactorToRemoveReflection
BillyMartin1964 Mar 11, 2026
28304c1
Refactor EnumDescriptionGeneratorHelper to remove reflection
BillyMartin1964 Mar 11, 2026
4f21f71
Merge branch 'RefactorToRemoveReflection'
BillyMartin1964 Mar 11, 2026
6e4e5ba
Merge branch 'main' into main
BillyMartin1964 Mar 12, 2026
dd4d8f4
Merge branch 'main' into main
TheCodeTraveler Apr 1, 2026
012182e
Update samples/CommunityToolkit.Maui.Sample/ViewModels/Converters/Enu…
TheCodeTraveler Apr 1, 2026
0c6f83f
Update samples/CommunityToolkit.Maui.Sample/MauiProgram.cs
TheCodeTraveler Apr 1, 2026
5413fef
Update samples/CommunityToolkit.Maui.Sample/Pages/Converters/EnumDesc…
TheCodeTraveler Apr 1, 2026
69b243c
Update samples/CommunityToolkit.Maui.Sample/CommunityToolkit.Maui.Sam…
TheCodeTraveler Apr 1, 2026
3a012cd
Update src/CommunityToolkit.Maui/Converters/EnumDescriptionConverter.cs
TheCodeTraveler Apr 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions samples/CommunityToolkit.Maui.Sample/AppShell.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.ObjectModel;

using CommunityToolkit.Maui.Sample.Pages;
using CommunityToolkit.Maui.Sample.Pages.Alerts;
using CommunityToolkit.Maui.Sample.Pages.Behaviors;
Expand Down Expand Up @@ -58,6 +59,7 @@ public partial class AppShell : Shell
CreateViewModelMapping<CompareConverterPage, CompareConverterViewModel, ConvertersGalleryPage, ConvertersGalleryViewModel>(),
CreateViewModelMapping<DateTimeOffsetConverterPage, DateTimeOffsetConverterViewModel, ConvertersGalleryPage, ConvertersGalleryViewModel>(),
CreateViewModelMapping<DoubleToIntConverterPage, DoubleToIntConverterViewModel, ConvertersGalleryPage, ConvertersGalleryViewModel>(),
CreateViewModelMapping<EnumDescriptionConverterPage, EnumDescriptionConverterViewModel, ConvertersGalleryPage, ConvertersGalleryViewModel>(),
CreateViewModelMapping<EnumToBoolConverterPage, EnumToBoolConverterViewModel, ConvertersGalleryPage, ConvertersGalleryViewModel>(),
CreateViewModelMapping<EnumToIntConverterPage, EnumToIntConverterViewModel, ConvertersGalleryPage, ConvertersGalleryViewModel>(),
CreateViewModelMapping<ImageResourceConverterPage, ImageResourceConverterViewModel, ConvertersGalleryPage, ConvertersGalleryViewModel>(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@
<WindowsSdkPackageVersion>10.0.19041.56</WindowsSdkPackageVersion>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Release'
AND $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) != 'windows'">
<PropertyGroup Condition="'$(Configuration)' == 'Release'&#xD;&#xA; AND $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) != 'windows'">
<!-- Windows error NETSDK1102: Optimizing assemblies for size is not supported for the selected publish configuration. -->
<PublishAot>false</PublishAot>
<PublishTrimmed>true</PublishTrimmed>
Expand Down Expand Up @@ -87,6 +86,18 @@
<ProjectReference Include="..\..\src\CommunityToolkit.Maui.Maps\CommunityToolkit.Maui.Maps.csproj" />
</ItemGroup>

<ItemGroup>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you revert those changes?

<Compile Update="Pages\Converters\EnumDescriptionConverterPage.xaml.cs">
<DependentUpon>EnumDescriptionConverterPage.xaml</DependentUpon>
</Compile>
</ItemGroup>

<ItemGroup>
<MauiXaml Update="Pages\Converters\EnumDescriptionConverterPage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
</ItemGroup>

<PropertyGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)'))=='windows' and $(Configuration) == 'Release'">
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>
Expand Down
5 changes: 5 additions & 0 deletions samples/CommunityToolkit.Maui.Sample/MauiProgram.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Diagnostics.CodeAnalysis;

using CommunityToolkit.Maui.ApplicationModel;
using CommunityToolkit.Maui.Converters;
using CommunityToolkit.Maui.Core;
using CommunityToolkit.Maui.Markup;
using CommunityToolkit.Maui.Media;
Expand Down Expand Up @@ -27,9 +29,11 @@
using CommunityToolkit.Maui.Sample.Views.Popups;
using CommunityToolkit.Maui.Storage;
using CommunityToolkit.Maui.Views;

using Microsoft.Extensions.Http.Resilience;
using Microsoft.Extensions.Logging;
using Microsoft.Maui.LifecycleEvents;

using Polly;

#if WINDOWS10_0_17763_0_OR_GREATER
Expand Down Expand Up @@ -193,6 +197,7 @@ static void RegisterViewsAndViewModels(in IServiceCollection services)
services.AddTransientWithShellRoute<CompareConverterPage, CompareConverterViewModel>();
services.AddTransientWithShellRoute<DateTimeOffsetConverterPage, DateTimeOffsetConverterViewModel>();
services.AddTransientWithShellRoute<DoubleToIntConverterPage, DoubleToIntConverterViewModel>();
services.AddTransientWithShellRoute<EnumDescriptionConverterPage, EnumDescriptionConverterViewModel>();
services.AddTransientWithShellRoute<EnumToBoolConverterPage, EnumToBoolConverterViewModel>();
services.AddTransientWithShellRoute<EnumToIntConverterPage, EnumToIntConverterViewModel>();
services.AddTransientWithShellRoute<ImageResourceConverterPage, ImageResourceConverterViewModel>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8" ?>
<pages:BasePage
x:Class="CommunityToolkit.Maui.Sample.Pages.Converters.EnumDescriptionConverterPage"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:pages="clr-namespace:CommunityToolkit.Maui.Sample.Pages"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:vm="clr-namespace:CommunityToolkit.Maui.Sample.ViewModels.Converters"
Title="EnumDescriptionConverter"
x:DataType="vm:EnumDescriptionConverterViewModel"
x:TypeArguments="vm:EnumDescriptionConverterViewModel">

<pages:BasePage.Resources>
<ResourceDictionary>
<toolkit:EnumDescriptionConverter x:Key="EnumDescriptionConverter" />
</ResourceDictionary>
</pages:BasePage.Resources>

<pages:BasePage.Content>
<VerticalStackLayout Padding="20" Spacing="20">
<Label VerticalOptions="Center">
<Label.FormattedText>
<FormattedString>
<Span Text="The" />
<Span FontAttributes="Bold" Text=" EnumDescriptionConverter " />
<Span Text="converts Enum values into readable text so they display nicely in the UI." />
</FormattedString>
</Label.FormattedText>
</Label>
<Label Text="When an Enum value is passed in, the converter looks for attributes that provide a more user-friendly name. If one is found, it uses that value; otherwise it simply uses the Enum’s name.. " VerticalOptions="Center" />
<Label Text="This ensures that Enum values can be displayed clearly while still working correctly even when no attributes are defined. " VerticalOptions="Center" />
<Label Text="There is no description needed for one word enum members that are spelled the way you want to display them." />


<Border
Margin="0,20,0,0"
Padding="15,10"
Background="LightGray"
StrokeShape="RoundRectangle 8">
<HorizontalStackLayout Spacing="10">
<Label
FontSize="16"
Text="Without Converter:"
TextColor="DarkRed" />
<Label
FontAttributes="Bold"
FontSize="16"
Text="{Binding SelectedMode}"
TextColor="DarkRed" />
</HorizontalStackLayout>
</Border>
<Border
Padding="15,10"
Background="LightGray"
StrokeShape="RoundRectangle 8">
<HorizontalStackLayout Spacing="10">
<Label
FontSize="16"
Text="With Converter:"
TextColor="DarkRed" />
<Label
FontAttributes="Bold"
FontSize="16"
Text="{Binding SelectedMode, Converter={StaticResource EnumDescriptionConverter}}"
TextColor="DarkRed" />
</HorizontalStackLayout>
</Border>
</VerticalStackLayout>
</pages:BasePage.Content>
</pages:BasePage>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using CommunityToolkit.Maui.Sample.ViewModels.Converters;

namespace CommunityToolkit.Maui.Sample.Pages.Converters;

public partial class EnumDescriptionConverterPage : BasePage<EnumDescriptionConverterViewModel>
{
public EnumDescriptionConverterPage(EnumDescriptionConverterViewModel enumDescriptionConverterViewModel)
: base(enumDescriptionConverterViewModel)
{
InitializeComponent();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public partial class ConvertersGalleryViewModel() : BaseGalleryViewModel(
SectionModel.Create<CompareConverterViewModel>(nameof(CompareConverter), "A converter that compares two IComparable objects and returns a boolean value or one of two specified objects."),
SectionModel.Create<DateTimeOffsetConverterViewModel>(nameof(DateTimeOffsetConverter), "A converter that allows to convert from a DateTimeOffset type to a DateTime type"),
SectionModel.Create<DoubleToIntConverterViewModel>(nameof(DoubleToIntConverter), "A converter that allows users to convert an incoming double value to an int."),
SectionModel.Create<EnumDescriptionConverterViewModel>(nameof(EnumDescriptionConverter), "A converter that converts Enum values into readable text so they display nicely in the UI"),
SectionModel.Create<EnumToBoolConverterViewModel>(nameof(EnumToBoolConverter), "A converter that allows you to convert an Enum to boolean value"),
SectionModel.Create<EnumToIntConverterViewModel>(nameof(EnumToIntConverter), "A converter that allows you to convert an Enum to its underlying int value"),
SectionModel.Create<IndexToArrayItemConverterViewModel>(nameof(IndexToArrayItemConverter), "A converter that allows users to convert a int value binding to an item in an array."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Text;

using CommunityToolkit.Mvvm.ComponentModel;

namespace CommunityToolkit.Maui.Sample.ViewModels.Converters;

public partial class EnumDescriptionConverterViewModel : BaseViewModel
{
[ObservableProperty]
public partial ModeName SelectedMode { get; set; }

public EnumDescriptionConverterViewModel()
{
SelectedMode = ModeName.DarkMode;
}
}

public enum ModeName
{
// No Description needed for one word enum members that
// are spelled the way you want to display them

[Description("Light Mode")] // Can Use Description attribute
LightMode,
[Display(Name = "Dark Mode")] // Or Display attribute with Name property
DarkMode,
System
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// CommunityToolkit.Maui.Analyzers/EnumDescriptionGenerator.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using CommunityToolkit.Maui.SourceGenerators.Helpers;
using CommunityToolkit.Maui.SourceGenerators.Models;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

namespace CommunityToolkit.Maui.Analyzers;

[Generator]
public class EnumDescriptionGenerator : IIncrementalGenerator
Comment on lines +1 to +18
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This source generator file is under the SourceGenerators project but declares namespace CommunityToolkit.Maui.Analyzers; and the header comment also references CommunityToolkit.Maui.Analyzers/.... This is inconsistent with the rest of the SourceGenerators project (e.g., CommunityToolkit.Maui.SourceGenerators / .Generators) and makes the codebase harder to navigate. Consider moving it into the SourceGenerators namespace.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BillyMartin1964 - Could you please fix the namespace?

{
// Using fields instead of const to avoid naming rule violations
static readonly string displayAttributeName = "System.ComponentModel.DataAnnotations.DisplayAttribute";
static readonly string descriptionAttributeName = "System.ComponentModel.DescriptionAttribute";

static EnumDescriptionModel? CreateEnumDescriptionModel(INamedTypeSymbol? enumSymbol)
{
if (enumSymbol is null || enumSymbol.TypeKind != TypeKind.Enum || !EnumDescriptionGeneratorHelper.IsAccessibleFromNamespace(enumSymbol))
{
return null;
}

string ns = enumSymbol.ContainingNamespace.ToDisplayString();
string enumName = enumSymbol.Name;
string enumQualifiedName = enumSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);

var members = new List<EnumMemberModel>();
foreach (IFieldSymbol member in enumSymbol.GetMembers().OfType<IFieldSymbol>())
{
if (member.ConstantValue is null)
{
continue;
}

string? description = EnumDescriptionGeneratorHelper.GetDescriptionFromDescriptionAttribute(member, descriptionAttributeName);
string? displayName = null;
string? resourceType = null;
if (EnumDescriptionGeneratorHelper.TryGetDisplayInfo(member, displayAttributeName, out var dn, out var rt))
{
displayName = dn;
resourceType = rt?.ToDisplayString();
}

members.Add(new EnumMemberModel(
member.Name,
description,
displayName,
resourceType
));
}

return new EnumDescriptionModel(enumName, ns, enumQualifiedName, members);
}

public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValuesProvider<EnumDescriptionModel?> enumModels = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (node, _) => node is EnumDeclarationSyntax,
transform: static (ctx, _) => EnumDescriptionGenerator.CreateEnumDescriptionModel(ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) as INamedTypeSymbol))
.Where(static model => model is not null);

context.RegisterSourceOutput(enumModels, (spc, model) =>
{
if (model is null)
{
return;
}

string code = GenerateCode(model);
string hintName = $"{model.QualifiedName.Replace(".", "_")}.g.cs";
spc.AddSource(hintName, SourceText.From(code, Encoding.UTF8));
});
}

static string GenerateCode(EnumDescriptionModel model)
{
string ns = model.Namespace;
string enumName = model.EnumName;
string enumQualifiedName = model.QualifiedName;
// Remove 'global::' prefix if present
string cleanedQualifiedName = enumQualifiedName.StartsWith("global::", StringComparison.Ordinal)
? enumQualifiedName.Substring(8)
: enumQualifiedName;
// Replace invalid characters for identifiers
string baseClassName = cleanedQualifiedName.Replace(".", "_").Replace("+", "_");
string initializerClassName = $"{baseClassName}_DescriptionInitializer";

var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("#nullable enable");
sb.AppendLine("using CommunityToolkit.Maui.Converters;");
sb.AppendLine();

if (!string.IsNullOrEmpty(ns))
{
sb.AppendLine($"namespace {ns};");
sb.AppendLine();
}

sb.AppendLine($"internal static class {initializerClassName}");
sb.AppendLine("{");
sb.AppendLine(" [global::System.Runtime.CompilerServices.ModuleInitializer]");
sb.AppendLine(" internal static void Initialize()");
sb.AppendLine(" {");
sb.AppendLine(" var dict = new global::System.Collections.Generic.Dictionary<string, string>();");
sb.AppendLine(" var resolvers = new global::System.Collections.Generic.Dictionary<string, global::System.Func<global::System.Globalization.CultureInfo?, string>>();");

foreach (var member in model.Members)
{
string fallbackDescription = member.Description ?? member.Name;
if (member.DisplayName is not null && !string.IsNullOrWhiteSpace(member.DisplayName))
{
if (member.ResourceType is not null)
{
// Cannot safely resolve localized resource access at compile-time
sb.AppendLine($" dict[\"{member.Name}\"] = \"{EnumDescriptionGeneratorHelper.EscapeString(fallbackDescription)}\";");
continue;
}
sb.AppendLine($" dict[\"{member.Name}\"] = \"{EnumDescriptionGeneratorHelper.EscapeString(member.DisplayName)}\";");
continue;
}
sb.AppendLine($" dict[\"{member.Name}\"] = \"{EnumDescriptionGeneratorHelper.EscapeString(fallbackDescription)}\";");
Comment on lines +120 to +131
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Localized DisplayAttribute support appears incomplete: when ResourceType is set the generator currently writes the fallback enum name into dict and never adds anything to resolvers, so localized display names will never be returned at runtime. The helper already has TryGetLocalizedDisplayResolverExpression; consider using it to populate resolvers (and only falling back to dict when a resolver cannot be generated).

Suggested change
if (member.DisplayName is not null && !string.IsNullOrWhiteSpace(member.DisplayName))
{
if (member.ResourceType is not null)
{
// Cannot safely resolve localized resource access at compile-time
sb.AppendLine($" dict[\"{member.Name}\"] = \"{EnumDescriptionGeneratorHelper.EscapeString(fallbackDescription)}\";");
continue;
}
sb.AppendLine($" dict[\"{member.Name}\"] = \"{EnumDescriptionGeneratorHelper.EscapeString(member.DisplayName)}\";");
continue;
}
sb.AppendLine($" dict[\"{member.Name}\"] = \"{EnumDescriptionGeneratorHelper.EscapeString(fallbackDescription)}\";");
if (EnumDescriptionGeneratorHelper.TryGetLocalizedDisplayResolverExpression(member, out string resolverExpression))
{
sb.AppendLine($" resolvers[\"{member.Name}\"] = {resolverExpression};");
continue;
}
string displayText = !string.IsNullOrWhiteSpace(member.DisplayName)
? member.DisplayName
: fallbackDescription;
sb.AppendLine($" dict[\"{member.Name}\"] = \"{EnumDescriptionGeneratorHelper.EscapeString(displayText)}\";");

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BillyMartin1964 - What are your thoughts on this comment from CoPilot?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, @TheCodeTraveler , I'm getting lost. After making all the changes that you guys wanted, it quit working. Copilot says that the generator is only generating for the enums that are actually in the Toolkit and not the enums in the user's app.

I know you're busy, but I could really use another set of eyes. This is so close to just throw it away now.

}

sb.AppendLine();
sb.AppendLine($" EnumDescriptionRegistry.Register(typeof({enumQualifiedName}), dict);");
sb.AppendLine(" if (resolvers.Count > 0)");
sb.AppendLine(" {");
sb.AppendLine($" EnumDescriptionRegistry.Register(typeof({enumQualifiedName}), resolvers);");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine("}");

return sb.ToString();
}

}
Loading
Loading