以前に書いた「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); }