1

While on my quest for a (more) modern solution to using a single attribute on an enum in order to provide data for an entry's display name and its tooltip, too, I came across several questions and answers here on SO.

However, it was this particular answer that a) pointed me into the right direction by using ResourceType = typeof(<YourResourcesFileType, e. g. "Resources"> within the attribute and an instance of ResourceManager within the extension method and b) encouraged me to write this separate question and answer, too.

Now, how to write a good, preferably generic, approach utilizing newer C# language features to populate e. g. a dropdown with data while providing a tooltip for each entry by only using a single attribute per enum entry? Is that even possible?

The answer is: YES by using the DisplayAttribute and leveraging its Name and Description properties.

Since I developed it for a WPF application, I was bound by MVVM and aimed for clean, readable code as much as possible. But as with all code: there's always room for improvement. So, please feel free to provide feedback, if you think we can improve this even further.

Community
  • 1
  • 1
Yoda
  • 539
  • 1
  • 9
  • 18
  • This looks like it would fit well as an entry in the Documentation section of SO – Lithium Oct 27 '16 at 13:39
  • @Yoda, why do you need attribute if everything stored in resources? enum field name (e.g. `QuestionType.Answer`) is a good key for Resource. `_Entry` or `_ToolTip` is a convention, which can be added to a key (`QuestionType.Answer_ToolTip`) and passed to converter as parameter. [my variation on Enum localization - EnumLocalizationConverter](https://github.com/AlexanderSharykin/CardIdleRemastered/blob/master/SourceCode/CardIdleRemastered/Converters/EnumLocalizationConverter.cs) (work for any Enum) – ASh Oct 27 '16 at 13:47
  • @Lithium can you point me to some guide showing how to accomplish this? – Yoda Oct 27 '16 at 14:46
  • @ASh because a) from what I understand, your `ConvertBack()` method requires to iterate over the whole enum which is pretty expensive on bigger `enum`s, b) it seems doing it your way removes the ability to provide both, an entry's name _and_ it's tooltip and c) it seems to me like your solution wouldn't work if several `enum`s were used (which I need to) and the scheme would differ from `EnumType.Field`. – Yoda Oct 27 '16 at 14:56
  • @Yoda Try the [Documentation Tour](http://stackoverflow.com/tour/documentation), the Documentation portion of SO is still in Beta, but it is very similar to SO, only that instead of Q/A it is focused on being a source of documentation and examples of implementation like your answer is. – Lithium Oct 27 '16 at 14:59
  • @Yoda, a) it need performance measurement if there are doubts b) it perfectly works for `Name`, `ToolTip` *and whatever else*, if use converterParameter c) perfectly works for many enum, scheme `Namespace.EnumName.FieldName_AdditionalParameter` should provide unique key for any enum field – ASh Oct 28 '16 at 06:26
  • and another important thing: what about existing enums where nobody can add their custom attributes? e.g. .Net `DayOfWeek`, which often should be visible for user in applications? – ASh Oct 28 '16 at 10:37

1 Answers1

0

Note: Code is formatted to fit SO's design, not necessarily the typical C# line (break) design as I dislike horizontal scrolling.

The resources file, Resources.resx: Resources.resx example

Note #1: You must set the Access Modifier (upper right corner in the screenshot above) to Public in order to get access from other assemblies (which is required in my case).

Note #2: To leverage Visual Studio's concept of multi-language support, a localized resources file must be named Resources.xx-XX.resx, e. g. Resources.de-DE.resx.

The enum, QuestionType.cs:

namespace Yoda.Data.Interfaces.Enums
{
  using System.ComponentModel.DataAnnotations;
  using Yoda.Data.Interfaces.Properties;

  public enum QuestionType
  {
    [Display(Name = "Question_Type_Unknown_Entry",
             ResourceType = typeof(Resources),
             Description = "Question_Type_Unknown_ToolTip")]
    Unknown = 0,

    [Display(Name = "Question_Type_Question_Entry",
             ResourceType = typeof(Resources),
             Description = "Question_Type_Question_ToolTip")]
    Question = 1,

    [Display(Name = "Question_Type_Answer_Entry",
             ResourceType = typeof(Resources),
             Description = "Question_Type_Answer_ToolTip")]
    Answer = 2
  }
}

The extension method(s) in EnumExtensions.cs:

namespace Yoda.Frontend.Extensions
{
  using System;
  using System.ComponentModel.DataAnnotations;
  using System.Globalization;
  using System.Linq;
  using System.Resources;

  public static class EnumExtensions
  {
    // This method can be made private if you don't use it elsewhere
    public static TAttribute GetEnumAttribute<TAttribute>(this Enum enumValue)
      where TAttribute : Attribute
    {
      var memberInfo = enumValue.GetType().GetMember(enumValue.ToString());

      return memberInfo[0].GetCustomAttributes(typeof(TAttribute), false)
                          .OfType<TAttribute>()
                          .FirstOrDefault();
    }

    public static string ToDescription(this Enum enumValue)
    {
      var displayAttribute = enumValue.GetEnumAttribute<DisplayAttribute>();

      return displayAttribute == null
        ? enumValue.ToString().Replace("_", " ")
        : new ResourceManager(displayAttribute.ResourceType)
                .GetString(displayAttribute.Description, CultureInfo.CurrentUICulture);
    }

    public static string ToName(this Enum enumValue)
    {
      var displayAttribute = enumValue.GetEnumAttribute<DisplayAttribute>();

      return displayAttribute == null
        ? enumValue.ToString().Replace("_", " ")
        : new ResourceManager(displayAttribute.ResourceType)
                .GetString(displayAttribute.Name, CultureInfo.CurrentUICulture);
    }
  }

  // Your other enum extension methods go here...
}

An usage example in WPF requiring 2 converters (instead of one with parameter) to comply with SoC would look like the following:

The Name property converter, QuestionTypeNameConverter.cs:

namespace Yoda.Frontend.Converters
{
  using System;
  using System.Globalization;
  using System.Windows.Data;

  using Yoda.Data.Interfaces.Enums;
  using Yoda.Frontend.Extensions;

  // Second converter would be named QuestionTypeDescriptionConverter 
  public class QuestionTypeNameConverter : IValueConverter
  {
    public object Convert(object value, Type targetType, object parameter,
                          CultureInfo culture)
    {
      // Second converter would call .ToDescription() instead
      return (value as QuestionType? ?? QuestionType.Unknown).ToName();
    }

    public object ConvertBack(object value, Type targetType, object parameter,
                              CultureInfo culture)
    {
      return value;
    }
  }
}

Note: For your reading pleasure, I'm limiting my example to only show one converter class, but I've included comments to describe needed changes for the second one.

And finally the usage in MainView.xaml:

<Window x:Class="Yoda.Frontend.MainView" x:Name="MainWindow">
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        // ... more xmlns & other basic stuff

        xmlns:c="clr-namespace:CAP.GUI.Converters"
  <Window.Resources>
    <c:QuestionTypeDescriptionConverter x:Key="QuestionTypeDescription" />
    <c:QuestionTypeNameConverter        x:Key="QuestionTypeName" />
  </Window.Resources>

  // Your window layout goes here...

  <ComboBox ItemsSource="{Binding QuestionTypes, Mode=OneWay}" Margin="3" Name="QuestionType"
            SelectedItem="{Binding SelectedItem.ValidQuestionType,
                                   Converter={StaticResource QuestionTypeName}}"
            ToolTip="{Binding SelectedItem.ValidQuestionType,
                              Converter={StaticResource QuestionTypeDescription}}">
    <ComboBox.ItemTemplate>
      <DataTemplate>
        <TextBlock Text="{Binding Converter={StaticResource QuestionTypeName}}"
                   ToolTip="{Binding Converter={StaticResource QuestionTypeDescription}}" />
      </DataTemplate>
    </ComboBox.ItemTemplate>
  </ComboBox>

  // Your other window layout goes here...

</Window>
Yoda
  • 539
  • 1
  • 9
  • 18