Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
using Xunit;

using CommunityToolkit.Maui.Converters;

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace CommunityToolkit.Maui.UnitTests.Converters;

public class EnumDescriptionConverterTests : BaseOneWayConverterTest<EnumDescriptionConverter>
{
enum TestEnum
{
[Display(Name = "Display Name")]
WithDisplay,
[Description("Description Text")]
WithDescription,
NoAttribute
}

enum MultiAttributeEnum
{
[Display(Name = "Display Name")]
[Description("Description Text")]
Both,
[Display(Name = "")]
[Description("Description Text")]
EmptyDisplay,
[Display(Name = " ")]
[Description("Description Text")]
WhitespaceDisplay,
[Description("")]
EmptyDescription,
}

[Fact]
public void ConvertFrom_ThrowsArgumentNullException_WhenNull()
{
var converter = (ICommunityToolkitValueConverter)new EnumDescriptionConverter();
Assert.Throws<ArgumentNullException>(() => converter.Convert(null, typeof(string), null, null));
}

[Fact]
public void ConvertFrom_FallbackToValueToString_WhenInvalidEnumValue()
{
var converter = new EnumDescriptionConverter();
// Cast an int not defined in MultiAttributeEnum
var invalid = (MultiAttributeEnum)999;
var result = converter.ConvertFrom(invalid);
Assert.Equal("999", result);
}

[Fact]
public void ConvertFrom_ReturnsDisplayName()
{
var converter = new EnumDescriptionConverter();
var result = converter.ConvertFrom(TestEnum.WithDisplay);
Assert.Equal("Display Name", result);
}

[Fact]
public void ConvertFrom_ReturnsDescriptionText()
{
var converter = new EnumDescriptionConverter();
var result = converter.ConvertFrom(TestEnum.WithDescription);
Assert.Equal("Description Text", result);
}

[Fact]
public void ConvertFrom_ReturnsEnumName_WhenNoAttribute()
{
var converter = new EnumDescriptionConverter();
var result = converter.ConvertFrom(TestEnum.NoAttribute);
Assert.Equal("NoAttribute", result);
}


[Fact]
public void ConvertFrom_DisplayTakesPrecedence_WhenBothAttributes()
{
var converter = new EnumDescriptionConverter();
var result = converter.ConvertFrom(MultiAttributeEnum.Both);
Assert.Equal("Display Name", result);
}

[Fact]
public void ConvertFrom_FallbackToDescription_WhenDisplayEmpty()
{
var converter = new EnumDescriptionConverter();
var result = converter.ConvertFrom(MultiAttributeEnum.EmptyDisplay);
Assert.Equal("Description Text", result);
}

[Fact]
public void ConvertFrom_FallbackToDescription_WhenDisplayWhitespace()
{
var converter = new EnumDescriptionConverter();
var result = converter.ConvertFrom(MultiAttributeEnum.WhitespaceDisplay);
Assert.Equal("Description Text", result);
}

[Fact]
public void ConvertFrom_FallbackToEnumName_WhenDescriptionEmpty()
{
var converter = new EnumDescriptionConverter();
var result = converter.ConvertFrom(MultiAttributeEnum.EmptyDescription);
Assert.Equal("EmptyDescription", result);
}

#region DisplayAttribute Localization & Edge Cases

public class TestResources
{
public static string LocalizedDisplay => "Localized Display";
public static int NonStringResource => 42;
}

enum LocalizedEnum
{
[Display(Name = "LocalizedDisplay", ResourceType = typeof(TestResources))]
Localized,
[Display(Name = "MissingResource", ResourceType = typeof(TestResources))]
MissingResource,
[Display(Name = null)]
NullName,
[Display(Name = " ")]
WhitespaceName,
[Display(Name = "NonStringResource", ResourceType = typeof(TestResources))]
NonStringResource,
}

[Fact]
public void ConvertFrom_ReturnsLocalizedDisplay_WhenResourceTypeSet()
{
var converter = new EnumDescriptionConverter();
var result = converter.ConvertFrom(LocalizedEnum.Localized);
Assert.Equal("Localized Display", result);
}

[Fact]
public void ConvertFrom_FallbackToName_WhenResourceMissing()
{
var converter = new EnumDescriptionConverter();
var result = converter.ConvertFrom(LocalizedEnum.MissingResource);
Assert.Equal("MissingResource", result);
}

[Fact]
public void ConvertFrom_FallbackToEnumName_WhenDisplayNameNull()
{
var converter = new EnumDescriptionConverter();
var result = converter.ConvertFrom(LocalizedEnum.NullName);
Assert.Equal("NullName", result);
}

[Fact]
public void ConvertFrom_FallbackToEnumName_WhenDisplayNameWhitespace()
{
var converter = new EnumDescriptionConverter();
var result = converter.ConvertFrom(LocalizedEnum.WhitespaceName);
Assert.Equal("WhitespaceName", result);
}

[Fact]
public void ConvertFrom_FallbackToEnumName_WhenGetNameThrows()
{
var converter = new EnumDescriptionConverter();
var result = converter.ConvertFrom(LocalizedEnum.NonStringResource);
Assert.Equal("NonStringResource", result);
}

#endregion

#region DescriptionAttribute Edge Cases

enum DescriptionEdgeEnum
{
[Description("")]
EmptyDescription,
}

[Fact]
public void ConvertFrom_FallbackToEnumName_WhenDescriptionEmptyEdge()
{
var converter = new EnumDescriptionConverter();
var result = converter.ConvertFrom(DescriptionEdgeEnum.EmptyDescription);
Assert.Equal("EmptyDescription", result);
}

#endregion
}
67 changes: 67 additions & 0 deletions src/CommunityToolkit.Maui/Converters/EnumDescriptionConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Reflection;

namespace CommunityToolkit.Maui.Converters;

/// <summary>
/// Converts an <see cref="Enum"/> value to its display string using <see cref="DisplayAttribute"/> or <see cref="DescriptionAttribute"/>.
/// </summary>
[AcceptEmptyServiceProvider]
public partial class EnumDescriptionConverter : BaseConverterOneWay<Enum, string>
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.

Suggested change
public partial class EnumDescriptionConverter : BaseConverterOneWay<Enum, string>
public partial class EnumDescriptionConverter : BaseConverterOneWay<Enum, string?>

It would make sense for this converter to return a nullable string, BaseConverterOneWay<Enum, string?> and return null when an enum does not contain a Description attribute.

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.

I originally had a null return, but the copilots reviewer said we should return the key if the user was localizing. What do you think?

{
/// <inheritdoc/>
public override string DefaultConvertReturnValue { get; set; } = string.Empty;
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.

Suggested change
public override string DefaultConvertReturnValue { get; set; } = string.Empty;
public override string? DefaultConvertReturnValue { get; set; }

It would make sense for this converter to return a nullable string and return null when an enum does not contain a Description attribute.


/// <summary>
/// Converts an <see cref="Enum"/> value to its display string.
/// </summary>
/// <param name="value">The enum value to convert.</param>
/// <param name="culture">The culture to use for the conversion (not used).</param>
/// <returns>
/// The value of the <see cref="DisplayAttribute.Name"/> if defined;
/// otherwise the value of the <see cref="DescriptionAttribute.Description"/> if defined;
/// otherwise the enum name as a string.
/// </returns>
public override string ConvertFrom(Enum value, CultureInfo? culture = null)
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.

Suggested change
public override string ConvertFrom(Enum value, CultureInfo? culture = null)
public override string? ConvertFrom(Enum value, CultureInfo? culture = null)

It would make sense for this converter to return a nullable string and return null when an enum does not contain a Description attribute.

{
ArgumentNullException.ThrowIfNull(value);
var fieldInfo = value.GetType().GetField(value.ToString());

// Check for DisplayAttribute first (common in data annotations)
var displayAttr = fieldInfo?.GetCustomAttribute<DisplayAttribute>();
if (displayAttr is not null)
{
string? displayName = null;
try
{
displayName = displayAttr.GetName();
}
catch
{
// If GetName throws, fallback to Name
displayName = null;
}
if (!string.IsNullOrWhiteSpace(displayName))
{
return displayName;
}
if (!string.IsNullOrWhiteSpace(displayAttr.Name))
{
return displayAttr.Name;
}
}

// Fallback to DescriptionAttribute
var descriptionAttr = fieldInfo?.GetCustomAttribute<DescriptionAttribute>();
if (descriptionAttr is not null && !string.IsNullOrWhiteSpace(descriptionAttr.Description))
{
return descriptionAttr.Description;
}

return value.ToString(); // Fallback to enum name if no attribute found
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.

Suggested change
return value.ToString(); // Fallback to enum name if no attribute found
return value.ToString(); // Fallback to enum name if no attribute found

It would make sense for this converter to return a nullable string and return null when an enum does not contain a Description attribute.

}

// ConvertBackTo is sealed in BaseConverterOneWay and cannot be overridden.
}
Loading