かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

WPFアプリケーションで表示言語を動的に切り替える

以前に書いた「WPFアプリケーションの国際化対応」の記事に以下のようなコメントがついてたので、もんもんと考えていました。

MVVMモデルでのアプリケーションにおいて、
OSの言語によって変わるだけであれば問題ないのですが、
ユーザが任意の言語を選択できるようにしたい場合、
どのような設計が考えられると思いますか?

前に書いた記事ではMVVMでも無ければ、表示言語の動的な切り替えは泥臭くBindingを手動でアップデートをかけるというものでした。普通は、動的に切り替えないだろうと思って上記のような実装にしたのですが、これを動的に切り替えること前提にした場合はどうなるか?というのが今回のテーマです。

リソースの管理は誰の仕事?

純粋に、現在のカルチャーによって表示言語を切りかえるだけならViewの範疇で良かったのですが、今回はユーザのアクションによって表示言語を切り替えるというのが大前提になります。
ということで、VMにResourceを管理するクラスを作りViewModelLocator(MVVM Lightを今回使ってます)に、このリソース管理クラスを追加するようにしました。Viewからは、このVMを経由してリソースにアクセスします。

namespace WpfDynamicResourceChange.ViewModel
{
    using System.Globalization;
    using GalaSoft.MvvmLight;
    using WpfDynamicResourceChange.Properties;

    /// <summary>
    /// リソースを管理するクラス
    /// </summary>
    public class ResourceHolder : ViewModelBase
    {
        /// <summary>
        /// 管理対象のリソース
        /// </summary>
        private static Resources resources = new Resources();

        /// <summary>
        /// Viewからアクセスするためにプロパティとして公開する
        /// </summary>
        public Resources Resources
        {
            get
            {
                return resources;
            }
        }

        /// <summary>
        /// カルチャー変更メソッド
        /// </summary>
        /// <param name="culture"></param>
        public void ChangeCulture(string culture)
        {
            // カルチャーを変更して
            Resources.Culture = CultureInfo.GetCultureInfo(culture);
            // Resourcesプロパティに変更があったことを通知
            this.RaisePropertyChanged("Resources");
        }

        /// <summary>
        /// このアプリでサポートしてるカルチャー
        /// </summary>
        private readonly string[] supportCulture = new[] { "ja-JP", "en-US" };

        /// <summary>
        /// このアプリでサポートしてるカルチャー
        /// </summary>
        public string[] SupportCulture
        {
            get
            {
                return this.supportCulture;
            }
        }
    }
}

そして、このクラスをViewModelLocatorに追加します。(コードスニペットのコードそのまま)

private static ResourceHolder _resourceHolder;

/// <summary>
/// Gets the ResourceHolder property.
/// </summary>
public static ResourceHolder ResourceHolderStatic
{
    get
    {
        if (_resourceHolder == null)
        {
            CreateResourceHolder();
        }

        return _resourceHolder;
    }
}

/// <summary>
/// Gets the ResourceHolder property.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance",
    "CA1822:MarkMembersAsStatic",
    Justification = "This non-static member is needed for data binding purposes.")]
public ResourceHolder ResourceHolder
{
    get
    {
        return ResourceHolderStatic;
    }
}

/// <summary>
/// Provides a deterministic way to delete the ResourceHolder property.
/// </summary>
public static void ClearResourceHolder()
{
    _resourceHolder.Cleanup();
    _resourceHolder = null;
}

/// <summary>
/// Provides a deterministic way to create the ResourceHolder property.
/// </summary>
public static void CreateResourceHolder()
{
    if (_resourceHolder == null)
    {
        _resourceHolder = new ResourceHolder();
    }
}

/// <summary>
/// Cleans up all the resources.
/// </summary>
public static void Cleanup()
{
    ClearResourceHolder();
}

Viewからテキストへのアクセス方法

では、次にViewからリソースで定義されたテキストを参照します。これはApp.xamlにViewModelLocatorが以下のように定義されているので、そこ経由で先ほど定義したResourceHolderクラスをたどっていく形になります。

<!-- App.xaml -->
<Application.Resources>
    <vm:ViewModelLocator x:Key="Locator" d:IsDataSource="True" />
</Application.Resources>
<!-- Viewからリソースにアクセスするコード。 -->
<Button 
    Content="{Binding Source={StaticResource Locator}, Path=ResourceHolder.Resources.ButtonText}" 
    ...その他のプロパティは割愛... />

因みに、リソースには以下のようなテキストを定義しています。

リソースの切り替え処理

これは画面に置いたボタンをCommandとバインドしてViewModelで以下のようなコードを書いてます。純粋にLocator経由でResourceHolderのカルチャー変更処理を呼び出しているだけです。

/// <summary>
/// カルチャー変更処理
/// </summary>
private void ChangeCulture()
{
    // Locator経由でResourceHolderのカルチャー変更処理を実行
    ViewModelLocator.ResourceHolderStatic.ChangeCulture(this.CurrentCulture);
}

実行結果

ということで実行してみます。最初は日本語が表示されています。

コンボボックスでen-USを選択して、ボタンを押すと英語になります。

もちろん、もう一度ja-JPを選択してボタンを押すと日本語表記になります。

まとめ

要件は達成できたような気がしますが、これが最適な方法なのかは自信がありません。(自信がないからコードレシピじゃなくてBlogに書いてます)
これでいいんじゃない?とか、こういう風にした方が良いとか言う意見があればコメントかTwitterあたりで突っ込みお願いしますm(_ _)m

ソース一式はSkyDrive上にUPしてます。