WPF Series -6- Globalization of Enums

In the previous blog post of this series I wrote about binding ComboBox or ListBox to an enum. Using just the approach described there exposes the enum values directly in the corresponding ComboBox or ListBox. Because the enum values do not always have descriptive names or because they are language specific, it’s usually more appropriate to display a localized description of each enum value in the UI instead of exposing enum values directly.

In this blog post I’ll show how to extend the approach of the previous blog post to support internationalization.

First of all a custom attribute has to be implemented to support resource file based translation. Our custom attribute is called ResourceNameAttribute and inherits from System.Attribute.

ResourceNameAttribute.cs

/**
 * Copyright 2017 d-fens GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System;
using System.Diagnostics.Contracts;

namespace biz.dfch.CS.ArbitraryWpfApplication.UI.Attributes
{
    /// <summary>
    /// This attribute is used to define the resource name of a specific enum value
    /// for resource file based translation
    /// </summary>
    [AttributeUsage(AttributeTargets.Field)]
    public class ResourceNameAttribute : Attribute
    {
        /// <summary>
        /// Holds the name of the resource file entry
        /// </summary>
        public string Name { get; protected set; }

        /// <summary>
        /// Constructor used to init a ResourceNameAttribute
        /// </summary>
        /// <param name="name">Name of the resource file entry</param>
        public ResourceNameAttribute(string name)
        {
            Contract.Requires(!string.IsNullOrWhiteSpace(name));

            Name = name;
        }
    }
}

Furthermore some resource files have to be added to the project. At least the default resource file Resources.resx has to be created. Culture specific resource files can be added as needed.

Next a custom EnumConverter has to be implemented that inherits from System.ComponentModel.EnumConverter. In our case we called it EnumTranslationConverter. It’s ConvertTo method relies on the built-in ResourceManager that uses the culture specific resource file depending on the current culture information. If no culture specific resource file exists for the current culture the Resources.resx will be used by default. The conversion works as follows.

  1. Returns the enum value as string, if corresponding enum value is not annotated with ResourceNameAttribute
  2. Returns the value of the resource entry, that matches the name specified in the ResourceNameAttribute of the corresponding enum value, out of the culture specific resource file
  3. Returns RESOURCE-ENTRY-MISSING, if no resource entry present, that matches the name specified in the ResourceNameAttribute of the corresponding enum value, in the culture specific resource file

EnumTranslationConverter.cs

/**
 * Copyright 2017 d-fens GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Reflection;
using biz.dfch.CS.ArbitraryWpfApplication.UI.Attributes;
using biz.dfch.CS.ArbitraryWpfApplication.UI.Properties;

namespace biz.dfch.CS.ArbitraryWpfApplication.UI.Converters
{
    public class EnumTranslationCoverter : EnumConverter
    {
        internal const string MISSING_RESOURCE_ENTRY = "RESOURCE-ENTRY-MISSING";

        public EnumTranslationCoverter(Type type)
            : base(type)
        {
            Contract.Requires(null != type);
        }

        public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
        {
            Contract.Assert(null != context);
            Contract.Assert(null != culture);
            Contract.Assert(null != destinationType);

            if (destinationType != typeof(string))
            {
                return base.ConvertTo(context, culture, value, destinationType);
            }

            if (value != null)
            {
                FieldInfo fieldInfo = value.GetType().GetField(value.ToString());
                Contract.Assert(null != fieldInfo);

                var attributes = (ResourceNameAttribute[])fieldInfo.GetCustomAttributes(typeof(ResourceNameAttribute), false);
                if (attributes.Length < 1)
                {
                    return value.ToString();
                }

                var resourceName = attributes[0].Name;
                var localizedText = Resources.ResourceManager.GetString(resourceName, culture);

                if (string.IsNullOrWhiteSpace(localizedText))
                {
                    return MISSING_RESOURCE_ENTRY;
                }

                return localizedText;
            }

            return string.Empty;
        }
    }
}

Last but not least the affected enums have to be adjusted as follows.

  • Add [TypeConverter(typeof(EnumTranslationCoverter))] to the enum
  • Specify ResourceName for each enum value by using [ResourceName("NAME_OF_THE_RESOURCE_ENTRY")]

ConnectionMaterial.cs

/**
 * Copyright 2017 d-fens GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System.ComponentModel;
using biz.dfch.CS.ArbitraryWpfApplication.UI.Attributes;
using biz.dfch.CS.ArbitraryWpfApplication.UI.Converters;

namespace biz.dfch.CS.ArbitraryWpfApplication.UI.Domain.Sewer
{
    [TypeConverter(typeof(EnumTranslationCoverter))]
    public enum ConnectionMaterial
    {
        [ResourceName("ConnectionMaterial_None")]
        None,
        [ResourceName("ConnectionMaterial_Kunststoff")]
        Kunststoff,
        [ResourceName("ConnectionMaterial_Beton")]
        Beton,
        [ResourceName("ConnectionMaterial_Saniert")]
        Saniert,
        [ResourceName("ConnectionMaterial_Andere")]
        Andere,
        [ResourceName("ConnectionMaterial_Unbekannt")]
        Unbekannt
    }
}

Don’t forget to add resource entries for every resource name specified using the ResourceNameAttribute.

That’s all. In the UI the values of the resource entries of the resource file based on the current culture will show up.

To change the current culture create a new instance of type CultureInfo and assign it to Thread.CurrentThread.CurrentCulture and Thread.CurrentThread.CurrentUICulture in OnStartup method of App.xaml.cs.

Many thanks to Brian Lagunas for his great and inspiring blog post.

« WPF Series -5- Binding ComboBox or ListBox to Enum WPF Series -7- ViewModel Validation using DataAnnotations »

Trackbacks

  1. […] WPF Series -6- Globalization of Enums » […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: