ASP.NET MVC 4でViewModelのDisplayName(ラベル)を多言語化する

ろーからいぜーしょんネタです。

前置き

ASP.NET MVC 4では、ViewModelクラスのフィールドにView表示用のラベルを与えることができます。

using System.ComponentModel.DataAnnotations;

namespace Foo.ViewModels.Login
{
    public class LoginViewModel
    {
        [Required]
        [Display(Name = "アカウント")]
        public string UserId { get; set; }

        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "パスワード")]
        public string Password { get; set; }
    }
}

フィールドに対してDisplayAttributeでラベルを追加し、View側で読み出します。

Viewには、利用するViewModelを指定しておく必要があります。

@model Foo.ViewModels.Login.LoginViewModel

<!-- 略 -->

<table>
    <tr>
        <th>@Html.LabelFor(m => m.UserId)</th>
        <td>@Html.TextBoxFor(m => m.UserId)</td>
    </tr>
    <tr>
        <th>@Html.LabelFor(m => m.Password)</th>
        <td>@Html.TextBoxFor(m => m.Password)</td>
    </tr>
</table>

このラベルの文字列を、条件によって英語または日本語で表示したいことがあります。たとえば、ユーザからのリクエストのHTTPヘッダを見てAccept-Languageに応じて振り分けたいとか、アプリケーションの言語設定で保持している値に従って振り分けたいとかです。

これを実現するには、ラベル文字列の保管場所によって、最低でも2つの方法があるでしょう。

  1. ラベル文字列をリソースファイルから取得する
  2. ラベル文字列をデータベースから取得する

1.の方法についての情報はたくさんありますが、2.があまりなくて困ったのでメモします。(特に2について)もっと良い方法があれば、ぜひ教えてください。

1. ラベル文字列をリソースファイルから取得する

言語別のリソースファイルにラベル文字列を定義し、ViewModelに適切なラベルを紐づけます。

ラベル文字列の管理

言語別*1のリソースファイルをプロジェクトに追加します。

VisualStudioで、リソースファイルを置くフォルダを右クリックして、[Add]→[New Item]→[Installed]→[Visual C#(もしくはVB)]→[General]で、Resource Fileを選び、拡張子resxのファイルを作ります。

たとえば、FooResource.resxに英語リソース、FooResources.ja.resxに日本語リソースを、Name-Value形式で定義します。それぞれの値にはCommentもつけることができます。

リソースファイルのAccess Modifierをpublicに設定します。

これを忘れると、実行時にInvalidOperationExceptionが起きます。

ローカライズに失敗したため、プロパティ 'Bar' を取得できません。型 'FooResource' がパブリックでないか、'Bar' という名前のパブリックで静的な文字列プロパティが含まれていません

ViewModelの記述

冒頭に載せたViewModelを編集します。

DisplayAttributeにハードコーディングしていたラベル文字列を、外出ししたリソースファイルの値に変えます。

using System.ComponentModel.DataAnnotations;

namespace Foo.ViewModels.Login
{
    public class LoginViewModel
    {
        [Required]
        [Display(Name = "Account", ResourceType = typeof(Foo.Resources.FooResources))]
        public string UserId { get; set; }

        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Password", ResourceType = typeof(Foo.Resources.FooResources))]
        public string Password { get; set; }
    }
}

DisplayAttributeのNameにリソースのName、ResourceTypeにResourcesファイルの型を書きます。

設定

言語別Resourcesファイルを選択させるための設定をします。

使用する言語をアプリケーション全体で一律にブラウザの言語設定に依存させるなら、Web.configに設定を1行追加するだけでOKです。

<configuration>
    <!--略-->
    <system.web>
        <globalization culture="auto" uiCulture="auto"/>
    </system.web>
    <!--略-->
</configuration>

この設定で、ViewModelオブジェクトが作成される時に、リクエストの言語に応じたResourcesファイルが選択されます。

一方、使用する言語の選択方法がアプリケーションの各ページで異なっていて、ブラウザの設定一択で済ますことができない場合は、Web.configで設定せず、必要に応じてフィルタを書き、フィルタをWeb.configに登録する必要があります。ActionMethodの実行前に実行されるメソッドに言語選択ロジックを書きます。

2. ラベル文字列をデータベースから取得する

データベースに、各言語IDのようなものと一緒にラベル文字列が格納されていて、それを引っ張ってくるパターンです。

ここでやりたいのは、1.のラベル文字列の取得先をデータベースに変えて、適切なラベルを紐づけたViewModelオブジェクトを.NET MVCフレームワークに作成させることです。

ラベル文字列の管理

話を簡単にするため、テーブルは仮に次のようなものとします。LANGID=1は英語で、2は日本語だと思ってください。

Resourcesテーブル

ID NAME LANGID VALUE
1 Account 1 Account
2 Account 2 アカウント
3 Password 1 Password
4 Password 2 パスワード

ViewModelの記述

ViewModelを編集します。

using System.ComponentModel.DataAnnotations;

namespace Foo.ViewModels.Login
{
    public class LoginViewModel
    {
        [Required]
        [Display(Name = "Account")]
        public string UserId { get; set; }

        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }
    }
}

DisplayAttributeのNameに、ラベル文字列を引き当てるためのキーとして、ResourcesテーブルのNAMEの値を書きます。

カスタムModelMetadataProviderの作成

ここが重要

MVCフレームワークがModelクラスを作成するタイミングで動作するModelMetadataProviderの動きをカスタマイズしてやる必要があります。

そのために、System.Web.Mvc.DataAnnotationsModelMetadataProviderを継承したカスタムModelMetadataProviderを作成します。

クラスを作成した後は、CreateMetadataメソッドを1つだけオーバーライドすればOKです。メソッドの中で、自分がやりたい方法でModelクラスを生成します。

今回は、基底クラスの実装を基本的には使いつつ、ラベル文字列の生成処理だけを上書きしました。つまり、DoisplayAttributeに渡したNAMEをキーに、データベースに接続するコードを書きました。

namespace Foo.Providers
{
    public class CustomModelMetadataProvider : DataAnnotationsModelMetadataProvider
    {

        protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType,
            string propertyName)
        {
            var modelMetadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
            if (modelMetadata.DisplayName != null)
            {
                // displayNameとLANGIDをキーに、Resourcesテーブルから値を取ってくる処理。略。
            }
            return modelMetadata;
        }
    }
}

このあたりの情報は、『Pro ASP.NET MVC 3 Framework』で見つけました*2

設定

Global.asaxに、カスタムModelMetadataProviderを登録する処理を追加します。

protected void Application_Start()
{
    // 略
    
    ModelMetadataProviders.Current = new CustomModelMetadataProvider();
    
    // 略
}

以上です。

考え中のこと

2.のとき、1つのAttribute引数でラベル文字列を一意に特定できればラッキークッキーハッグッキーですが、データベースの設計次第では、キーが複数必要な場合もあります。そういう場合、Attributeをどう使えばよいのか悩み中です。

あくまでもDisplayAttributeだけに頼って解決するなら、Nameにキーをハイフン繋ぎで並べるなどして、カスタムModelMetadataProviderの中で文字列操作をして、クエリに渡せばいいでしょう。しかし、キーの個数や使い方がアプリケーション全体で一律であるなら、AditionalAttributeを使って、オリジナルのアノテーションを定義する方が分かりやすい気がします。

*1:正確には、言語別ではなくSystem.Globalization.CultureInfo別ですが、簡単のため省略します。

*2:アップデート版の『Pro ASP.NET MVC 4 Framework』ではこの解説がばっさり削除されているので注意。