Skip to content
Dima Enns edited this page Jul 18, 2021 · 5 revisions

Welcome to the MrMeeseeks.ResXToViewModelGenerator wiki!

Requirements

Installation

Please use nuget to install it in your project (https://www.nuget.org/packages/MrMeeseeks.ResXToViewModelGenerator/):

dotnet add package MrMeeseeks.ResXToViewModelGenerator

Using it as a project reference or by refencing the library directly won't work out-of-the-box, because you would need to set up your ResX file as "additional files" for the source generator. The nuget package contains a configuration file which'll do that for you.

Problem Statement

Old solutions I know of and what I don't like about them:

  • The ".Designer.cs"-file by the custom tool ResXFileCodeGenerator

    • Nice thing is that you can staticly reference the properties which give some sweat sweat compile time checks
    • However, on runtime it is impossible to switch the localization which lead to awkward situation to ask the user to restart whenever she or he switches the language
  • https://github.com/XAMLMarkupExtensions/WPFLocalizationExtension

    • A really really awesome project. In fact I've used it until I came up with this project
    • It let's you switch localization on runtime
    • their slogan is "...is really the easiest way to localize any type of DependencyProperties or native Properties on DependencyObjects since 2008!" and I believe them
      • However, "easiest" can mean "as easy as ever accomplished" rather than "as easy as it can possibly get"
      • It is not as bad, as I may have let it seem now, but I just remember a little bit of frustration when I tried to get it going. Once it did run, it actually was realy nice to use
    • It can localize more than strings. Which this project isn't capable of. So if that is a requirement of your project, then I recommend WPFLocalizationExtension to you.
      • in my personal project there never were the need to localize other types than string
    • But the keys are referenced string-based rather than statically, which in my opinion is a big caveat, because you'll loose the compile time checks

So! How do we resolve disadvantages of both while keeping - my personally topmost important - advantages? The answer is: we use Roslyn's new feature "C# Source Generator"!

How does it work

In the nutshell: It generates ViewModels based on your ResX files and you can just use them. Done!

The generated interfaces

But first things first. What does it generate (more) exactly? Let's see for the ResX files of the repository https://github.com/Yeah69/MrMeeseeks.ResXLocalizationSample (but for the sake of brevity only the interfaces):

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;

namespace MrMeeseeks.ResXToViewModelGenerator
{
	public interface ILocViewModel : INotifyPropertyChanged
	{
		CultureInfo CultureInfo { get; }
		string Title { get; }
		string HelloWorld { get; }
		string GoodNightSun { get; }

	}
	
	public interface ILocOptionViewModel : INotifyPropertyChanged
	{
		CultureInfo CultureInfo { get; }
	}

	public interface ICurrentLocViewModel : INotifyPropertyChanged
	{
		ILocViewModel CurrentLoc { get; }

		ILocOptionViewModel CurrentOption { get; set; }
        
		IReadOnlyList<ILocOptionViewModel> AvailableOptions { get; }
	}
}

The default ResX file of the sample project is called Loc.resx and use as a seed for the naming of the interfaces and some properties. Let's go through the interfaces one by one real quick:

  • ILocViewModel: instances of it represent a localization for a specific language
    • CultureInfo-property: has the language that the instance represents as language code (exception: invariant CultureInfo; more on that matte later)
    • Remaining properties: A string-property per ResX-key. Naming is adopted without changes. Instances get the corresponding values directly assigned.
  • ILocOptionViewModel: instances of it represent a localization for a specific language
    • CultureInfo-property: has the language that the instance represents as language code (exception: invariant CultureInfo; more on that matte later)
    • It doesn't hold any keys or value. Instances of this interface are meant to be lightweight as possible and their purpose is solely to choose a localization
  • ICurrentLocViewModel holds the current localization/option and lists all available options
    • CurrentLoc the current chosen localization. Initially set to the localization which represents the invariant culture.
    • CurrentOption the current chosen option. Initially set to the option which represents the invariant culture. Can be set to another option from AvailableOptions. Upon change the CurrentLoc property is changed accordingly as well.
    • AvailableOptions whole set of available options.

As you may have noticed all generated interface and therefore all instances of the implement INotifyPropertyChanged. That means they can be bound to like usual ViewModel. The notifications are emitted as expected as soon as the value changes. However, the only properties you can expect to change on runtime are CurrentLoc and CurrentOption from ICurrentLocViewModel.

Example of a localization class

private class enLocViewModel : ILocViewModel
{
#pragma warning disable 0067
        public event PropertyChangedEventHandler? PropertyChanged;
#pragma warning restore 0067

        public CultureInfo CultureInfo { get; } = CultureInfo.GetCultureInfo("en");
	public string Title { get; } = "ResX Localization Sample";
	public string HelloWorld { get; } = "Hello, World!";
	public string GoodNightSun { get; } = "Good night, sun!";
}

This class represents the English localization of the aforementioned sample project. You might ask yourself: Why should it implement INotifyPropertyChanged if its values never change? The reason for that is some unpleasant behavior of the binding engine of WPF. If the bound to class doesn't implement INotifyPropertyChanged the binding engine might cause memory leaks (see: https://stackoverflow.com/questions/18542940/can-bindings-create-memory-leaks-in-wpf).

Example of a option class

private class enLocOptionViewModel : ILocOptionViewModelInternal
{
#pragma warning disable 0067
	public event PropertyChangedEventHandler? PropertyChanged;
#pragma warning restore 0067

	public CultureInfo CultureInfo { get; } = CultureInfo.GetCultureInfo("en");

	public ILocViewModel Create() => new enLocViewModel();
}

The option class is similar to the localization class. But instead of the key/value-pair properties it has a sole factory method which creates an instance of the corresponding localization class. It's important to note that the factory method is part of a private inner interface, hence not visible externally.

How it is used

The following descriptions is the recommended usage of the generated code and will focus on WPF project.

Use a single ICurrent[Name]ViewModel instance

Where ever you use localizations, they have to originate from the very same ICurrent[Name]ViewModel instance. Or else they won't switch value whenever your switch the current localization language.

There is a little trick which is useful for WPF applications. Make it a resource in the "App.xaml"-file:

<Application x:Class="MrMeeseeks.ResXLocalizationSample.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:resXToViewModelGenerator="clr-namespace:MrMeeseeks.ResXToViewModelGenerator"
             Startup="App_OnStartup">
    <Application.Resources>
        <resXToViewModelGenerator:CurrentLocViewModel x:Key="CurrentLocViewModel" />
    </Application.Resources>
</Application>

That way it'll be know for the whole application and you'll be able to reference it from any XAML file:

<StackPanel DockPanel.Dock="Top">
    <TextBlock Text="{Binding CurrentLoc.HelloWorld, Source={StaticResource CurrentLocViewModel}}" />
    <TextBlock Text="{Binding CurrentLoc.GoodNightSun, Source={StaticResource CurrentLocViewModel}}" />
</StackPanel>

Note that the IDEs (both Visual Studio and Rider) will be able to statically check the existence of the properties and will indicate that by syntactic coloring. Which is nice to have. However, be aware, that because the Binding are Reflection-based, the compiler will still build if some of the keys get faulty (for example, if you rename a key and don't adjust its usages accordingly).

Using localizations in code (behind) files

You'll also be able to use localizations in code behind or ViewModel files. There are two approaches which I can think of. But both approaches require you get hold of the ICurrent[Name]ViewModel instance in code rather than the XAML files. I highly recommend to use dependency injection by constructor injection. Make sure that always the resource instance of the App is injected (for example, setup a custom factory method which references the App-resource and set its lifetime to Singleton/SingleInstance). Once you've done that you'll be able to comfortably inject it where ever you want:

public MainWindow(ICurrentLocViewModel currentLocViewModel)
{
    _currentLocViewModel = currentLocViewModel;
}

So, the first approach has less overhead just use the instance and reference the localization which you need:

private void InCodeButton_OnClick(object sender, RoutedEventArgs e)
{
    MessageBox.Show(
        this, 
        _currentLocViewModel.CurrentLoc.InCodeOnDemandText,
        _currentLocViewModel.CurrentLoc.InCodeOnDemandTitle);
}

Unfortunately, this'll only work properly under certain conditions: the localization references (here: InCodeOnDemandText and InCodeOnDemandTitle) are only used as long as the localization language isn't switched. In the above example it is guaranteed, because the MessageBox is modal and has to be closed before a localization switch can happen. Next time the MessageBox is opened the localization references are evaluated anew.

Such constraints are not always given, hence there is another way which has a bit more code involved:

private readonly ICurrentLocViewModel _currentLocViewModel;
private string _inCodeReactiveText;

public string InCodeReactiveText
{
    get => _inCodeReactiveText;
    set
    {
        _inCodeReactiveText = value;
        OnPropertyChanged();
    }
}

public MainWindowViewModel(ICurrentLocViewModel currentLocViewModel)
{
    _currentLocViewModel = currentLocViewModel;
    InCodeReactiveText = _currentLocViewModel.CurrentLoc.InCodeReactiveText;
    currentLocViewModel.PropertyChanged += (_, _) =>
        InCodeReactiveText = _currentLocViewModel.CurrentLoc.InCodeReactiveText;
}

Here, the property InCodeReactiveText represents a localization reference. If the localization language is switch we need to update it and emit a notification. In order to do that we register a callback on the PropertyChanged-event of ICurrentLocViewModel. That way we'll reactively adjust the property anytime the localization language got changed. Be aware that this sample isn't clean on the aspect of properly deregistering from the event!

Sample repository

For demonstration of the localization workflow with this project and another one of mine (https://github.com/Yeah69/MrMeeseeks.ResXTranslationCombinator), I've created a sample repository. Feel free to have a look: https://github.com/Yeah69/MrMeeseeks.ResXLocalizationSample

Caveats

  • This project doesn't use the full potential of ResX file. Meaning, that it is only capable of localizing sting
  • I am aware that .Net 5.0 is a difficult constraint for a lot of projects which target an earlier framework and a lot of consumers which don't have yet installed the runtime
    • Totally my fault. Until now I have plain and simple not managed to learn a code generation technique other than Source Generators. For example, maybe the concepts of this project are applicable on T4 templates.