In a WPF application, you often work with collections of data. This data is then visually represented in list-types (ComboBox, ListView etc.) or data grids. A common design approach may look like this:
- There is a
Model
, which represents a single data record (E.g. aUserModel
, including atomic data like a name etc.). - A
View
, which contains a list-type or a data grid. The collection ofUserModel
-objects is then bound to the correspondingItemsSource
of the visual element. - A
ViewModel
, which contains a collection ofUserModel
-objects.
If you are not familiar with this concept, you should read about the MVVM-Pattern first, as I will not go into more detail in this article.
To focus more on the problem, lets keep the UserModel
as simple as possible.
public enum Gender
{
Female, Male // Let's not start a gender debate here... ;-)
}
public class UserModel
{
public Gender Gender { get; }
public string Name { get; }
public UserModel(Gender gender, string name)
{
this.Gender = gender;
this.Name = name;
}
}
A UserModel
contains a name as string and a gender as enum-type. The required code of our ViewModel
holds a list of UserModel
-objects inside a generic ObservableCollection<>
.
private ObservableCollection<UserModel> users;
public ObservableCollection<UserModel> Users
{
get => this.users ?? (this.users = new ObservableCollection<UserModel>
{
// Some sample data
new UserModel(Gender.Female, "Anna"),
new UserModel(Gender.Male, "Scott")
});
private set
{
this.users = value;
this.OnPropertyChanged(nameof(this.Users));
}
}
When this collection is now bound to a data grid, the result will look like this:

When developing a multilingual application, the problem of this approach quickly becomes obvious. In the English-speaking countries there are no problems with the presented data, as the user is correctly shown “Male” and “Female” as gender. But what options do we have if we want to localize these enum values with as little code as possible?
Solution 1: A naive approach
A naive approach would be to add another property to the UserModel
that directly converts the enum value into a string.
public class UserModel
{
public Gender Gender { get; }
public string GenderAsString =>
this.Gender == Gender.Female
? LocalizationStrings.GenderFemale;
: LocalizationStrings.GenderMale;
public string Name { get; }
public UserModel(Gender gender, string name)
{
this.Gender = gender;
this.Name = name;
}
}
If we now manually specify the columns in the XAML file and apply appropriate column-headers including localization, we get the desired result (in German).

The corresponding XAML-code:
<DataGrid AutoGenerateColumns="False"
ItemsSource="{Binding Users}">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding GenderAsString}"
Header="{x:Static loc:LocalizationStrings.Gender}" />
<DataGridTextColumn Binding="{Binding Name}"
Header="{x:Static loc:LocalizationStrings.Name}" />
</DataGrid.Columns>
</DataGrid>
The main disadvantage of this approach is that we no longer use our model class for pure data storage. The property GenderAsString
is used exclusively in the View
. No business logic will (hopefully) ever access this property. Therefore, having such a “redundant” property in a model class, is a bad design.
Solution 2: Encapsulation of the Model
Instead of binding directly to a collection of Model
-objects, we can bind to a collection another ViewModel
, whose task is to encapsulate the model. Thus our UserModel
remains a pure data storage class. This ViewModel
will hold a private instance of our Model
and offers additional View
-specific properties. Such a ViewModel
could look like this:
public class UserViewModel
{
private readonly UserModel model;
public UserViewModel(UserModel model)
{
this.model = model;
}
public string Name => this.model.Name;
public string GenderAsString =>
this.model.Gender == Gender.Female
? LocalizationStrings.GenderFemale;
: LocalizationStrings.GenderMale;
}
With this approach we get the exact result as shown in solution 1. However, due to encapsulation we have a better separation of concerns. Nevertheless, the same problem exists as in solution 1: The property GenderAsString
is used exclusively in the View
. Even though the actual purpose of a ViewModel
is to create a link between the View
and the model
, there is actually no other need for our ViewModel
to hold this data. So what other options do we have?
Solution 3: Using an IValueConverter
public class GenderToStringConverter : IValueConverter
{
public object Convert(
object value,
Type targetType,
object parameter,
CultureInfo culture)
{
var result = string.Empty;
if (value is Gender gender)
{
result = gender == Gender.Female
? LocalizationStrings.GenderFemale;
: LocalizationStrings.GenderMale;
}
return result;
}
// ConvertBack() is not required for our sample
// ...
}
Having such an IValueConverter
, which takes the Gender
-type as input and returns the corresponding string, will solve our problem as well. Due to this approach, we don’t have any unnecessary code in the Model
or ViewModel
. The Gender
-value is bound to the column and the binding gets the required converter as parameter:
<Grid>
<Grid.Resources>
<local:GenderToStringConverter x:Key="GenderToStringConverter" />
</Grid.Resources>
<DataGrid AutoGenerateColumns="False"
ItemsSource="{Binding Users}">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Gender,
Converter={StaticResource GenderToStringConverter}}"
Header="{x:Static loc:LocalizationStrings.Gender}" />
<DataGridTextColumn Binding="{Binding Name}"
Header="{x:Static loc:LocalizationStrings.Name}" />
</DataGrid.Columns>
</DataGrid>
</Grid>
However, one big limitation remains. We now have an IValueConverter
which only accepts values of the type Gender
. If you think of enterprise software, you would have to implement the almost identical IValueConverter
for each individual enum type. This results in unnecessary code duplication. The final solution therefore consists of a generic solution, which I think is the best approach.
Solution 4: Using an attribute to create generic IValueConverter
So we need a generic way to avoid code duplication. Therefore, we introduce a small attribute class, which gets a localization key as constructor parameter:
[AttributeUsage(AttributeTargets.Field)]
public class LocalizationKeyAttribute : Attribute
{
public string LocKey { get; }
public LocalizationKeyAttribute(string locKey)
{
this.LocKey = locKey;
}
}
Now we can decorate our enum values with this attribute.
public enum Gender
{
[LocalizationKey("GenderFemale")]
Female,
[LocalizationKey("GenderMale")]
Male
}
Through this decoration we can now implement a generic IValueConverter
which finds the appropriate localized resource for any enum type and enum value.
public class LocalizedEnumConverter : IValueConverter
{
public object Convert(
object value,
Type targetType,
object parameter,
CultureInfo culture)
{
var result = string.Empty;
if (value is Enum enumValue)
{
var type = enumValue.GetType();
// Look for our 'LocalizationKeyAttribute' in the field's custom attributes
var field = type.GetField(enumValue.ToString());
var key = string.Empty;
if (field.GetCustomAttributes(typeof(LocalizationKeyAttribute), false) is LocalizationKeyAttribute[] attributes
&& attributes.Length > 0)
{
key = attributes[0].LocKey;
}
result = string.IsNullOrWhiteSpace(key)
? string.Empty
: LocStrings.ResourceManager.GetString(key);
}
return result;
}
// ConvertBack() ...
}
Now we can use this IValueConverter
for any enum type, as long as the enum values are decorated with the required attribute.
Summary
The presented solutions illustrate that there are different approaches for localizing enum types depending on the use case. For smaller applications, the implementation from solution 3 may be sufficient for the specific enum type. Anyway you should avoid unnecessary code in the Model
and ViewModel
. Therefore, solution 1 and solution 2 should be avoided as well. If you are familiar with value converters and custom attributes, I highly recommend choosing the last solution.
Are you using a completely different approach to localize enum values? If so, I would be happy to hear from it in the comments section below. :-)
comments powered by Disqus