喜帳面の日記

50歳越えおやじのASP.NET MVC への挑戦日記です。

Visual Studio Express 2012 for Web でいってみる 25.Partial View Ajax.BeginFormでレコード編集

 

 今回は、Ajax.BeginFormを使ったPartialViewでのレコード編集にチャレンジしたときのメモ書きです。
尚、初心者が行き当たりばったりにいろいろ動かしてみたときのメモなので、記載内容には勝手な解釈や誤りもあるかと思います。お気づきの点がありましたら、ご指摘よろしくお願いします。
前回はPartialViewを@Ajax.ActionLink や@Ajax.BeginFormでの呼び出しについて少しメモしまた。今回は、PartialViewでのレコード編集についてのメモです。
実は、まだ試作中の状態で課題の全てが解決している訳ではないのですが、今の状態をメモしておかないと忘れてしまいそうなのでメモを残します。
参考にさせてもらった情報はこれ。


尚、使用したデータベースは「NorthwindJ」で『サンプルで学ぶ ASP.NET MVC アプリケーション開発』http://msdn.microsoft.com/ja-jp/data/ff723829.aspx
のサンプルのZIPファイル内のサンプルデータベースです。
出来上がりのイメージはこんなかんじです。

f:id:SannomiyaNotes:20130311114731p:plain

ナビから「テスト」をクリックすると上記のページが表示されます。

このページのリストボックスで「商品コード」を指定し、「編集Form」もしくは「編集Link」をクリックすると、

f:id:SannomiyaNotes:20130311114934p:plain

 商品マスタの編集用PartialViewが現れます。商品名などを入力して「登録」ボタンをクリックすると、データベースに内容が更新されます。
また、入力必須項目の「商品名」が空の状態で「登録」ボタンをクリックすると、

f:id:SannomiyaNotes:20130311115211p:plain

データアノテーション機能の働きでエラーメッセージが表示されます。

それでは、ソースを貼り付けしておきます。edmxについては今回は割愛します。

[model]

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

using System.Web.Mvc;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace Mvc4ApplicationQ.Models
{
    //VMShouhinSitei:商品コード指定用フォームのmodel
    public class VMShouhinSitei
    {
        private NorthwindJEntities db = new NorthwindJEntities();
        [Required]
        [DisplayName("商品CD")]
        public int 商品コード { get; set; }

        //View Modelにフォーム上で使用しない項目を置かないように。
        //フォーム上に表示されていなくても検証は行われます。 

        // 商品のDropDownList用の商品リスト
        public IEnumerable<SelectListItem> Get商品List()
        {
            // --- 選択可能な商品リストの生成 ---
            var 商品SelectItemList = new List<SelectListItem>();
            foreach (var c in
                     from c in this.db.商品
                     orderby c.商品コード
                     select c)
            {
                var listItem = new SelectListItem
                {
                    Value = c.商品コード.ToString(),
                    Text = c.商品名
                };
                商品SelectItemList.Add(listItem);
            }
            return 商品SelectItemList;
        }
    }
    //VMShouhin:商品マスタの編集用model
    public class VMShouhin
    {
        [Required]
        [DisplayName("商品CD")]
        public int 商品コード { get; set; }

        [Required]
        [MaxLength(20)]
        [DisplayName("商品名")]
        public string 商品名 { get; set; }
    }
}

 先頭の商品コードを指定するページ用のmodel [ VMShouhinSitei ]と商品マスタの編集を行うページ用のmodel [ VMShouhin ]の2つのクラスを用意しました。

 

[Base.cshtml] 先頭の商品コードを指定するページ

@model Mvc4ApplicationQ.Models.VMShouhinSitei
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/jqueryval")

<script type="text/javascript">
    $(document).ready(function () {
        $("#btnajaxForm").click(function () {
            $("#pvShouhin").show();
        })
        $("#ajaxlink").click(function () {
            $(this).attr("href",
                     "/Shouhin/_editLink?ShouhinCD=" + $("#ShoouhinCD").val());
            $("#pvShouhin").show();
        })
    });
</script>

@using (Ajax.BeginForm("_editform", "Shouhin",
    new AjaxOptions
    {
        HttpMethod = "POST",
        UpdateTargetId = "pvShouhin",
        LoadingElementId = "loading1"
    }))
{
    @*↓Ajax.BeginFormに続く{}内にある項目がコントローラに正しく受け渡される。*@
    @Html.DisplayNameFor(model => model.商品コード)
    @Html.DropDownListFor(model => model.商品コード,
                   new SelectList(Model.Get商品List(),"Value","Text"),
                   new{ id="ShoouhinCD" })
    <br/><hr />
    <input ID ="btnajaxForm" type="submit" name="btnEdit" value="編集Form" />
    <img id="loading1" src="@Url.Content("~/Content/images/ajax-loader.gif")" 
                       alt="" class="loader" style = "display:none;"/>
}
<hr />
@Ajax.ActionLink("編集Link","_editLink","Shouhin",
      new { ShouhinCD = 0},
      new AjaxOptions() {
          HttpMethod = "POST",
          UpdateTargetId = "pvShouhin",
          LoadingElementId = "loading2"},
          new {ID ="ajaxlink",
               style = "height:30px; width:60px; float:left;
               border-style: solid;border-color:#787878; border-width:1px;" }
 )
<img id="loading2" src="@Url.Content("~/Content/images/ajax-loader.gif")" 
                   alt="" class="loader" style = "display:none;"/>
<br/><br/><hr />

@* PartialView の挿入場所*@
<div id="pvShouhin" style = "border-style: solid;
                    border-color:#B8B8B8; border-width:1px;
                    padding:4px;display:none"></div>

まず、

@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/jqueryval")

AjaxFormを使う為この2行が必要ですが、適当に扱わないと痛い目にあいます。

私の場合、この商品コード指定ページに記述済みにも関わらず、商品編集ページにも聞き込んでしまってました。商品編集ページでは特にエラーメッセージが出るわけでもなくしばらく気が付かなかったのですが、「登録」ボタンをクリックすると2回登録処理が実行されてました。

javascript 部分には、「編集Form」ボタンクリック時と「編集Link」クリック時にPartialView部分を見えるようにしています(更新後に隠しているので)。また「編集Link」クリック時にはリンク先を商品コードをパラメータにもつように変更しています。

このページでは、PartialViewを2つの方法で表示可能になっています。

「編集Form」ボタンクリック:@using  Ajax.BeginForm

 「編集Link」クリック:@Ajax.ActionLink

今回は、商品コード指定がリストボックスとなっています。省略されることがないので検証不要ですが、入力して内容の検証も行うのであれば、Ajax.BeginFormを使う方がいいと思います。

 

[_edit.cshtml] 商品マスタレコード編集用PartialView

@model Mvc4ApplicationQ.Models.VMShouhin
<script type="text/javascript">
    $(document).ready(function () {
        //ボタンをdisabledするとコントローラのUpdateアクションにはnullが渡される
        $("#MFMLbtnCreate").click(function () {
            $("#MFMLbtnCreate").hide();
            $("#MFMLbtnUpdate").hide();
        })
        $("#MFMLbtnCreate").click(function () {
            $("#MFMLbtnCreate").hide();
            $("#MFMLbtnUpdate").hide();
        })
    });

    function onEditCompletedWithSuccessUnobtrusive(data, status, xhr) {
        var ServerMSG = xhr.getResponseHeader('Content-ServerMSG');
        //alert(ServerMSG);
        if (ServerMSG == "Success") {
            $('#pvShouhin').hide();
        } else {
            $("#MFMLbtnCreate").show();
            $("#MFMLbtnUpdate").show();
        }
    }
</script>
@using (Ajax.BeginForm("Update", "Shouhin",
        new AjaxOptions
        {
            HttpMethod = "POST",
            UpdateTargetId = "pvShouhin",
            OnSuccess = "onEditCompletedWithSuccessUnobtrusive"
        }
       ))
{
    @Html.ValidationSummary(true)
    <fieldset>
        <legend>詳細</legend>
            <div class="editor-label">
                @Html.LabelFor(model => model.商品コード)
            </div>
            <div class="editor-field">
                @Html.EditorFor(model => model.商品コード)
                @Html.ValidationMessageFor(model => model.商品コード)
            </div>
            <div class="editor-label">
                @Html.LabelFor(model => model.商品名)
            </div>
            <div class="editor-field">
                @Html.EditorFor(model => model.商品名)
                @Html.ValidationMessageFor(model => model.商品名)
            </div>
      @* 更新ボタン *@
           <button id="MFMLbtnCreate" type="submit" name="Button" value="Create">登録</button>
           <button id="MFMLbtnUpdate" type="submit" name="Button" value="Update">変更保存</button>
    </fieldset>
}
 

 スクリプトの部分では [ 登録 ] や [ 変更保存 ]ボタンがクリックされたとき、その2つのボタンを.hide() しています。更新の2重実行の防止の為です。disabledを使うのが一般的でしょうか? 実はこのタイミングでsubmitボタンをdisabledしてしまうと、コントローラーの引数(Button)にはnullが渡されてしまいます。代替え策としての hide です。

onEditCompletedWithSuccessUnobtrusive()はAjax.BeginForm のオプション OnSuccessで指定しているファンクションです。Ajaxリクエストが成功したときに呼び出されるファンクションです。サーバーサイドでセットしたメッセージをResponse Headerから取り出し、更新成功か否かを判定しています。

@using (Ajax.BeginForm("Update", "Shouhin",
        new AjaxOptions
        {
            HttpMethod = "POST",
            UpdateTargetId = "pvShouhin",
            OnSuccess = "onEditCompletedWithSuccessUnobtrusive"
        }
       ))
{.....}
Ajax.BeginFormの引数  "Update", "Shouhin",は [ 登録 ] や [ 変更保存 ]ボタンがクリックされたときのAjax呼び出し指定で、Shouhinコントローラーの"Update"アクションが呼び出され制御はそちらに移ります。Updateアクションでサーバー側の処理が終わる際PartialViewを戻され、"pvShouhin"に再度表示されるはずです。ちょっと言葉尻が弱いのは、ここうまくいってないからです。
サーバーから戻る際、
 HttpContext.Response.AddHeader("Content-ServerMSG", ServerMSG);
 VMShouhin model = new VMShouhin();
 return PartialView("_edit", model);

てな感じで、空のPartialViewを戻していて、デバッグでステップ実行すると、確かにmodelは空で渡されているですが、表示されるのは [ 登録 ] ボタンクリック時点の内容になります。「更新完了です」とかのメッセージをmodelにセットして表示しようとしていたのですが、今の私の力ではできませんでした。アノテーションとの絡みかもしれませんが不明です。
submitボタンは2つ設置しています。2つともname="Button"にし、valueの値を違えておきます。そしてコントローラーの引数で
 public ActionResult Update(VMShouhin inputdata, string Button)
と記述すると、引数Buttonにvalueで設定した値が引き渡されます。
尚、コントローラー側で使用したい項目は、Ajax.BeginForm()に続く{...}の中に含めます。{...}の外に置いた場合エラーになるかというと、エラーにはなりませんが値が引き渡されないようです。
 
 [ShouhinController]
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Reflection;
using System.Web;
using System.Web.Mvc;

using Mvc4ApplicationQ.Models;

namespace Mvc4ApplicationQ.Controllers
{
    public class ShouhinController : Controller
    {
        private NorthwindJEntities db = new NorthwindJEntities();

        //
        // GET: /Shouhin/
        public ActionResult Base()
        {
            // ビューモデルを生成
            var model = new VMShouhinSitei();
            return View(model);
        }

        //@Ajax.ActionLinkのPOST
        [AjaxOnly]
        public ActionResult _editLink(int ShouhinCD)
        {
            VMShouhin model = new VMShouhin();
            if (Request.IsAjaxRequest())
            {
                商品 shouhin =
                    (from s in this.db.商品
                     where s.商品コード == ShouhinCD
                     select s).FirstOrDefault();
                model.商品コード = shouhin.商品コード;
                model.商品名 = shouhin.商品名;
            }
            return PartialView("_edit", model);
        }

        //@using (Ajax.BeginFormのPOST
        [AjaxOnly]
        public ActionResult _editform(VMShouhinSitei indata)
        {
            VMShouhin model = new VMShouhin();
            if (Request.IsAjaxRequest())
            {
                商品 shouhin =
                    (from s in this.db.商品
                     where s.商品コード == indata.商品コード
                     select s).FirstOrDefault();
                model.商品コード = shouhin.商品コード;
                model.商品名 = shouhin.商品名;
            }
            return PartialView("_edit", model);
        }
        
        //Attribute[AjaxOnly]汎用ルーチンなので本番ではコントローラーの外のクラスにします
        public class AjaxOnlyAttribute : ActionMethodSelectorAttribute
        {
            public override bool IsValidForRequest(
            ControllerContext controllerContext, MethodInfo methodInfo)
            {   
                return controllerContext.HttpContext.Request.IsAjaxRequest();
            }
        }

        //_editから呼び出される更新処理
        public ActionResult Update(VMShouhin inputdata, string Button)
        //↑:引数にはPartialViewのmodelを指定しないとデータアノテーションが効かなくなる。
        //↓:if (ModelState.IsValid)を必ず記述すること。書いとかないとエラーのまま更新してしまう。
        {
            string ServerMSG = "";
            if (ModelState.IsValid)
            {
                //更新処理...中身は省略....
                switch (Button)
                {
                    case "Create":
                        ServerMSG = "Success";
                        break;
                    case "Update":
                        ServerMSG = "Success";
                        break;
                    default:
                        break;
                }
                HttpContext.Response.AddHeader("Content-ServerMSG", ServerMSG);
                VMShouhin model = new VMShouhin();
                return PartialView("_edit", model);
            }
            //エラーメッセージの追加
            ModelState.AddModelError("", "更新処理は実行されませんでした。");
            ServerMSG = "Update process not carried out";
            HttpContext.Response.AddHeader("Content-ServerMSG", ServerMSG);
            return PartialView("_edit");
        }
}
コントローラーの途中
public class AjaxOnlyAttribute : ActionMethodSelectorAttribute
というクラスがありますが、これは[AjaxOnly]属性の部分です。今回はコントローラーの中に記述していますが、本来は汎用的なクラスなのでコントローラーの外に記述されるクラスです。
編集のPartialViewで[ 登録 ] や [ 変更保存 ]ボタンがクリックされたときに呼び出されるアクションが
public ActionResult Update(VMShouhin inputdata, string Button)
です。引数Buttonについては前述しました。
Ajax.BeginFormで特に引数の記述はしていませんが、VMShouhin 型のinputdata
ButtonにFormで入力した値がセットされます。
コード内のコメントにも書いてますが、フォームの入力内容をアノテーション機能でチェックする場合、引数にPartialViewのmodelを指定し、if (ModelState.IsValid)を必ず記述しましょう。どうやらAjaxFormのデータアノテーションAjaxを使わない一般的(?)なpostの場合と挙動がことなるようです。Ajaxじゃない場合のアノテーションって、submitした時点でクライアントサイドで検証され、例えば入力必須項目が空になってる場合コントローラーに制御が移らずにエラーメッセージ表示されていたように思います。今回のAjaxの事例では、入力必須項目が空であっても、制御は一旦コントローラーの Updateに飛び込んできます。その後 if (ModelState.IsValid)で引っかかって、return PartialView でクライアントに戻った時点でエラー表示されます。(ModelState.IsValid)が無いとそのままデータの更新処理を実行してしまいます。
尚、コントローラの更新終了後に
VMShouhin model = new VMShouhin();
return PartialView("_edit", model);
で、空のビューを戻しています。画面上の情報を初期化しようとの意図ですが、実際は効き目なしで、入力された値が再度表示されます。とほほ状態です。
 
 今回は以上です。
 

Visual Studio Express 2012 for Web でいってみる 24.Partial ViewとAjax.ActionLink か Ajax.BeginForm

今回は、Ajax.ActionLink と Ajax.BeginFormをコントローラーへのパラメータ渡しについてメモ書きをのこしときます。
尚、初心者が行き当たりばったりにいろいろ動かしてみたときのメモなので、記載内容には勝手な解釈や誤りもあるかと思います。お気づきの点がありましたら、ご指摘よろしくお願いします。

さて、PartialViewをAjaxを使ってレンダリングする場合、@Ajax.ActionLink や@Ajax.BeginFormを使うケースが多いのですが、どう使い分けしてますか?また、コントローラーへパラメータを渡したい場合どうやってます?

私の場合、入力処理を伴うPartialViewの呼び出しにはAjax.BeginFormを使い、表示のみの場合Ajax.ActionLinkを使うようにしています。

以下、コントローラーへパラメータを渡す方法についてメモ残しておきます。

 

Ajax.ActionLinkの場合

参考:AjaxExtensions.ActionLink メソッド

http://msdn.microsoft.com/ja-jp/library/system.web.mvc.ajax.ajaxextensions.actionlink(v=vs.100).aspx

@Ajax.ActionLinkを使って"Shouhin"コントローラーの"_edit"アクションからPartialViewを受け取ります。

その際、画面上のテキストボックス(ShouhinCD)に入力された値をコントローラーに渡します。

@Ajax.ActionLink("編集","_editLink","Shouhin",
      new { ShouhinCD = this.Model.ShouhinCD},
      new AjaxOptions() {
          HttpMethod = "POST",
          UpdateTargetId = "pvShouhin"},
      new {style = "height:28px; width:30px;float:left;"}
)

こんな感じでかいてしまうと、htmlは以下のようになります。

 <a data-ajax="true"
    data-ajax-method="POST"
    data-ajax-mode="replace"
    data-ajax-update="#pvShouhin"
    href="/Shouhin/_editLink?ShouhinCD=0"
    style="height:28px; width:30px;float:left;"
>編集</a>

 href="/Shouhin/_editLink?ShouhinCD=0"
とパラメータ値はコンパイル時点での値となってしまいます。
フォーム上に入力された値などをnew{praname = "hoge"}風に動的な値を渡したい場合は、Javascriptで値を変更してしまう方法があります。
こんなかんじでやってます。

[html]

@Ajax.ActionLink("編集","_editLink","Shouhin",
      new { FamilyCD = "paramvalue"},
      new AjaxOptions() {
          HttpMethod = "POST",
          UpdateTargetId = "pvShouhin"},
      new {ID ="ajaxlink",style = "height:28px; width:30px;float:left;"}
 )
<script type="text/javascript">
    $(document).ready(function () {
        $("#ajaxlink").click(function () {
            $(this).attr("href",
                         "/Shouhin/_editLink?ShouhinCD=" + $("#ShouhinCD").val());
            alert($(this).attr("href"));
        })
    });
</script>

[ShouhinController]

 [AjaxOnly]
 public ActionResult _editLink(int ShouhinCD)
 {
     VMShouhin model = new VMShouhin();
     if (Request.IsAjaxRequest())
     {
         商品 shouhin =
             (from s in this.db.商品
              where s.商品コード == ShouhinCD
              select s).FirstOrDefault();
         model.商品コード = shouhin.商品コード;
         model.商品名 = shouhin.商品名;
     }
     return PartialView("_edit", model);
 }

 Link要素にIDをつけ、jQueryでクリック時にhref属性を書き換えています。

これで単純なパラメータ渡しができます。この例では、入力値の検証部分がくみこまれていません。渡したい項目が多数であったり、入力値の検証をかけたい場合は@Ajax.BeginFormの方が便利だと思います。

Ajax.BeginFormの場合

 [html]

@model Mvc4ApplicationQ.Models.VMShouhinSitei

@using (Ajax.BeginForm("_editform", "Shouhin",
    new AjaxOptions
    {
        HttpMethod = "POST",
        UpdateTargetId = "pvShouhin"
    }))
{
    @*↓Ajax.BeginFormに続く{}内にある項目がコントローラに正しく受け渡される。*@
    @Html.DisplayNameFor(model => model.商品コード)
    @Html.DropDownListFor(model => model.商品コード,
                   new SelectList(Model.Get商品List(),"Value","Text"),
                   new{ id="ShoouhinCD" })
    <input ID ="btnajaxForm" type="submit" name="btnEdit" value="編集Form" />
}

 

 [ShouhinController]
 [AjaxOnly]
 public ActionResult _editform(VMShouhinSitei indata)
 {
     VMShouhin model = new VMShouhin();
     if (Request.IsAjaxRequest())
     {
         商品 shouhin =
             (from s in this.db.商品
              where s.商品コード == indata.商品コード
              select s).FirstOrDefault();
         model.商品コード = shouhin.商品コード;
         model.商品名 = shouhin.商品名;
     }
     return PartialView("_edit", model);
 }

Ajax.BeginFormの場合、Ajax.BeginFormに続く {...}の中にmodelを構成する要素とsubmitボタンを配置し、コントローラーはmodelを引数に定義することでコントローラー内でフォーム上の値を参照することが可能になります。

また、フォーム上に入力された値の検証(アノテーション)を行うことも可能ですが、AjaxのFormの場合、Ajaxを使わない普通のFormとはなんか挙動が異なる気がします。

この件は次のメモに残しときたいと思います。

今回は以上です。

 

 

 

 

Visual Studio Express 2012 for Web でいってみる 23.OAuth/OpenId認証(その2)

MVC4標準のOAuth + SQLServerの場合、照合順序が[Japanese_90_BIN2]だと問題が発生すると思われます。
 
 
今回は、MVC4のテンプレート内のOAuth/OpenId認証を使用する際の注意事項についてメモ書きをのこしときます。
尚、初心者が行き当たりばったりにいろいろ動かしてみたときのメモなので、記載内容には勝手な解釈や誤りもあるかと思います。お気づきの点がありましたら、ご指摘よろしくお願いします。
さて、以前の記事
Visual Studio Express 2012 for Web でいってみる 8.OAuth/OpenId認証(その1)
ここで、MVC4のテンプレート内のOAuth/OpenId認証にチャレンジして、何とかうまくいっている感じだったのですが、その後、登録された外部認証を削除する機能がうまく動作しない現象に遭遇しました。
原因は、SQLServer Databaseの照合順序にあるようです。
私の環境では、SQLServer Databaseの照合順序をバイナリ順の、[Japanese_90_BIN2]にしていました。文字列の比較やソートにこの照合順序が適用されるわけです。
具体的にはそんな環境で、
ローカルユーザーの新規登録を行った後
Manageページの外部ログインサービスの追加で、例えばTwitterを使ってログインを登録します。
この時点で[webpages_OAuthMembership]に紐付けレコードが追加されます。
Twitterを使ってログインも正常に機能します。
次に、同じくManageページで今度は、登録されている外部ログインからTwitterを削除します。そうすると[AccountController]メソッド[Disassociate]内の
 
OAuthWebSecurity.DeleteAccount(provider, providerUserId);
 
この行でエラーになります。
メッセージは「プロバイダーで不明なエラーが発生しました。

f:id:SannomiyaNotes:20130214133626p:plain

関連するテーブルの照合順序を[Japanese_CI_AS]で再作成することによってこのエラーを回避することができました。

照合順序がバイナリ系だとこのエラーが出るのか、[Japanese_90_BIN2]の場合のみでるのか?等の詳細な調査はできていません。

登録はできるのに削除はできないという現象でバグか?って気がしないでもないですが照合順序には注意しましょう。

私の場合は、データベースごと再作成することになりました。

今回は以上です。

Visual Studio Express 2012 for Web でいってみる 22.多言語対応をやってみた。

 

今回はResourcesを使った多言語対応にチャレンジしてみました。
初心者が行き当たりばったりにいろいろ動かしてみたときのメモなので、記載内容には勝手な解釈や誤りもあるかと思います。お気づきの点がありましたら、ご指摘よろしくお願いします。

 多言語対応って、自分のサイトには関係ないねって思ってはいるんですが、もし、もし、対応するとしたらどんな方法になるのかな?、簡単なら下準備だけしておこうと思い、ちょっと調べてちょっとだけやってみました。

なので、お手軽な手法をとってます。本当はもっとじっくり取り組むべき課題なのかもしれませんが、、、

さて、例によってWeb検索で参考にさせて頂いたページは

 

 
あと、以下の書籍の
4.3 入力の検証

 このあたりの情報をもとに、以下は、MVC4のテンプレートで生成される、「ユーザー登録」を英語対応してみたときのメモ書きです。

 ターゲットはこのページ

f:id:SannomiyaNotes:20130208162754p:plain

ASP.NET MVC 4 Web アプリケーション (Razor) のデフォルトで作成したプロジェクトの「登録」ページです。このページを英語でも表示されるようにします。

まず、今回採用した手法は、前述の各コンテンツ情報から、「リソース」を利用するのが最もお手軽そうな手法かなと思い、「リソース」に「文字列」を「アクセス修飾子 = Public」で登録してあっちこっちで使い回す方法を取ることにしました。

アクセス修飾子 = Publicにすることによるデメリットもあろうかと思いますが、この際(よくわかってないから)無視してしまいます。

1.リソースを格納するフォルダを作成する

特定のページグループで使用するローカルリソースは[App_LocalResources]フォルダ、すべてのページでページで使用できるグローバルリソースは[App_GlobalResources]フォルダに格納してHttpContextオブジェクトのメソッドで取り出すのが定石だったようですが、ローカルとグローバルを区別せず任意のフォルダを使用する方法が前述のコンテンツで記載されてましたのでそれに従うようにします。とはいえ、いろんなページに登場する文字列と、どう考えても1つのページにしか出てこないだろうという文字列はあるかと思いますんでその区分はしました。

今回用意したフォルダとファイルは以下の通りです。

f:id:SannomiyaNotes:20130208183223p:plain

 

 

ルート直下に

[Resources]フォルダを作成。そのフォルダ内に[Models]-[Account] , [Views]-[Account]といた風に、基本のフォルダ構造を[Resources]下に再現したような形式をとっています。そして[SharedStrings]フォルダを用意しました。

[SharedStrings]フォルダ以外のフォルダには、個別に対応するリソースファイルを配置しています。

例えば、[Views]-[Account]-[Register.cshtml]でのみ使用する文字列は、[Resources]-[Views]-[Account]-[Register.resx](デフォルト)か[Register.en.resx](英語用)から取り出し、

共通の文字列は[Resources]-[SharedStrings]-[Strings.resx](デフォルト)か[Strings.en.resx](英語用)から取り出します。

このあたりのフォルダ構造なんかは「お好み」ですきにしていいようです。

ただファイル名には規約「filename.XX.resx」があります。
[filename]は任意で、[XX]の部分はカルチャ名、en,it,fr,jpなどを指定します。
規定の言語用は「カルチャ」を省略します。

 .リソースを登録する

フォルダができたのでリソースファイルを登録します。

共通で使う文字列を[Resources]-[SharedStrings]-[Strings.resx]に登録します。

リソースの新規登録は、[SharedStrings]フォルダを右クリック、[追加]-[新しい項目]で[新しい項目の追加ダイアログ]を表示。ダイアログで[Web]-[全般]-[アセンブリ リソースファイル]を選択を指定し名前を入れて[追加]をクリックします。

登録画面が表示されます。

<Strings.resx>

f:id:SannomiyaNotes:20130208180246p:plain

アクセス修飾子を「Public」に変更します。

名前と値を登録します。

同様に英語版を用意します。

<Strings.en.resx>

f:id:SannomiyaNotes:20130208182208p:plain

同様に[Register.resx]と[Register.en.resx]、

[ValidationStrings.resx]と[ValidationStrings.en.resx]も登録。

<Register.resx>

f:id:SannomiyaNotes:20130208183802p:plain

<Register.en.resx>

f:id:SannomiyaNotes:20130208184048p:plain

 

<ValidationStrings.resx> 

f:id:SannomiyaNotes:20130208184610p:plain

<ValidationStrings.en.resx>

f:id:SannomiyaNotes:20130208184751p:plain

 

尚、各resxファイルのプロパティは以下の通り。

f:id:SannomiyaNotes:20130208185146p:plain

カスタムツール:PublicResXFileCodeGenerator

ビルドアクション:埋め込まれたリソース

カスタムツールが空白になっている場合は、resxファイルのアクセス修飾子がPublicになっているか確認してください。

これで、一通りの多言語化したい文字列のリソース登録の終了です。

実は「AccountController」のValidationで使用するエラーメッセージは他にもたくさんあり、本来はもっと登録しなきゃなんないのですが、今日のところはここまでにしときます。ちょっと手間ですね。

.Modelを変更する

 対象のクラスは、[Models]-[AccountModels.cs]のRegisterModelです。

変更前は

    public class RegisterModel
    {
        [Required]
        [Display(Name = "ユーザー名")]
        public string UserName { get; set; }

        [Required]
        [StringLength(100
                           , ErrorMessage = "{0} の長さは {2} 文字以上である必要があります。"
                           , MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "パスワード")]
        public string Password { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "パスワードの確認入力")]
        [Compare("Password", ErrorMessage = "パスワードと確認のパスワードが一致しません。")]
        public string ConfirmPassword { get; set; }
    }

ざっと見たところ、Display(Name)、StringLength()やCompare()のエラーメッセージの文字列部分が対象になります。

以下のように変更してみました。

(プロジェクト名はMvc4ApplicationLとなっています。)

使用するNamespace を追加

using Mvc4ApplicationL.Resources.SharedStrings;
using Mvc4ApplicationL.Resources.Models.Account;

1行目のSharedStringsは共通使用する文字列を格納している[Strings.resx]のあるフォルダ 。

2行目は、[Account]グループで使用する[ValidationStrings.resx]のあるフォルダを指定しています。

RegisterModelの変更後は以下のようになりました。

public class RegisterModel
{
    //ユーザー名
    [Required]
    [Display(ResourceType = typeof(Strings), Name = "Resユーザー名")]
    public string UserName { get; set; }
    
    //パスワード
    [Required]
    [StringLength(100, ErrorMessageResourceType = typeof(ValidationStrings)
                     , ErrorMessageResourceName = "ResStringLength"
                     , MinimumLength = 6)]
    [DataType(DataType.Password)]
    [Display(ResourceType = typeof(Strings), Name = "Resパスワード")]
    public string Password { get; set; }

    //パスワードの確認入力
    [DataType(DataType.Password)]
    [Display(ResourceType = typeof(Strings), Name = "Resパスワードの確認入力")]
    [Compare("Password"
            , ErrorMessageResourceType = typeof(ValidationStrings)
            , ErrorMessageResourceName = "ResPasswordMustMatch"
            )]
    public string ConfirmPassword { get; set; }
}

Display(Name)の変更。ユーザー名の部分を例に取ると、

[Display(ResourceType = typeof(Strings), Name = "Resユーザー名")]

「Strings」の部分は、リソースクラス Mvc4ApplicationL.Resources.SharedStrings.Stringsを指定しています。

 そのクラス内の名前="Resユーザー名"の値を指定しています。

StringLength()の変更。

パスワードの長さチェック部の変更前は、

[StringLength(100
                   ,ErrorMessage = "{0} の長さは {2} 文字以上である必要があります。"
                   ,MinimumLength = 6
                    )]

これで、入力値が6文字未満の場合、「パスワードの長さは6文字以上である必要があります。」とエラーメッセージが表示されます。

変更後は

[StringLength(100
                 , ErrorMessageResourceType = typeof(ValidationStrings)
                 , ErrorMessageResourceName = "ResStringLength"
                 , MinimumLength = 6)]

「ValidationStrings」の部分は、リソースクラス Mvc4ApplicationL.Resources.Models.Account.ValidationStringsを指定しています。

そのクラス内の名前="ResStringLength"の値を指定しています。

リソース登録内容は

規定の言語:「{0} の長さは {2} 文字以上である必要があります。」

英語(en) :「{0} must be at least {2} characters long.」

これで、言語が英語の場合、「Password must be at least 6 characters long.」と表示されます。

Compare()の変更も同様にErrorMessageの部分を

ErrorMessageResourceType = typeof(ValidationStrings) :リソースクラス

ErrorMessageResourceName = "ResPasswordMustMatch":名前

の2つの引数に置き換えてOKでした。

なお [Required]については特に変更していませんが、省略された場合英語の場合では

「The [DisplayName] field is required.」と表示されます。

 

.Controllerを変更する

 対象のコントローラーは[AccountController.cs]です。Modelの変更と同様に日本語のメッセージ定数部分をリソースを使用するように置き換えます。が、大量にあるので今回は一部分のみ記載します。例えば、このクラスの最終部分にある

ErrorCodeToString(MembershipCreateStatus createStatus) このメソッドの

入力したユーザー名が既に使用されている場合のエラーメッセージ部分は以下のように変更しました。

switch (createStatus)
{
    case MembershipCreateStatus.DuplicateUserName:
      //return "このユーザー名は既に存在します。別のユーザー名を入力してください。";
        return Resources.Views.Account.Register.ResourceManager.GetString("ResDuplicateUserName");
 以下省略

ResourceManagerを直接呼び出します。

 

.Viewを変更する

対象のViewは、[Views]-[Account]-[Register.cshtml]です。

変更後のソースは以下のようになりました。

@model Mvc4ApplicationL.Models.RegisterModel

@using Mvc4ApplicationL.Resources.SharedStrings; @*追加*@
@using Mvc4ApplicationL.Resources.Views.Account; @*追加*@

@{
  @*ViewBag.Title = "登録";*@
    ViewBag.Title = @Strings.Res登録;
}

<hgroup class="title">
    <h1>@ViewBag.Title.</h1>
  @*<h2>新しいアカウントを作成します。</h2>*@
    <h2>@Register.ResTitleMassage</h2>         
</hgroup>

@using (Html.BeginForm()) {
    @Html.AntiForgeryToken()
    @Html.ValidationSummary()

    <fieldset>
        <legend>登録フォーム</legend>
        <ol>
            <li>
                @Html.LabelFor(m => m.UserName)
                @Html.TextBoxFor(m => m.UserName)
            </li>
            <li>
                @Html.LabelFor(m => m.Password)
                @Html.PasswordFor(m => m.Password)
            </li>
            <li>
                @Html.LabelFor(m => m.ConfirmPassword)
                @Html.PasswordFor(m => m.ConfirmPassword)
            </li>
        </ol>
      @*<input type="submit" value="登録" />*@
        <input type="submit" value=@Strings.Res登録 />


    </fieldset>
}

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

先頭に@usingでこのページで使用するリソースフォルダを指定してます。

文字列を取得したい部分で、「@ResourceFile.name」と記述すればOKです。

簡単ですよね。今回はてきとうなリソース名を付けてますが、本番ではしっかり命名規則を作る必要がありそうです。

そろそろ実行してみますが、その前に1つやっておくことがあります。

.Web.configを変更する

 <system.web>に以下の1行を追加しておきます。

<globalization culture="auto" uiCulture="auto" />

これで準備完了です。

デバッグ実行してみる

 ヘッダー部の「登録」リンクをクリックすると、規定の言語で「登録」ページが表示されます。

f:id:SannomiyaNotes:20130208162754p:plain

次にブラウザーの言語設定を英語に変更してみます。

IEの場合は、[ツール]-[インターネットオプション] [全般]の[デザイン]-[言語]ボタンをクリックして[言語の優先順位]ダイアログを表示して、言語の優先順位を変更します。

言語のリストに英語が存在しない場合は[追加]ボタンをクリックして、英語(米国)[en-US]を追加します。その上で英語を先頭に移動します。

f:id:SannomiyaNotes:20130209091243p:plain

設定が終わったら、登録ページを「最新の情報に更新(F5)」します。

f:id:SannomiyaNotes:20130209091452p:plain

英語で表示されました。

Validationを確認してみます。

[Required]

f:id:SannomiyaNotes:20130209091800p:plain

 [StringLength]と[Compare]

f:id:SannomiyaNotes:20130209092016p:plain

ユーザー名の重複チェック

f:id:SannomiyaNotes:20130209092151p:plain

と英語対応ができてきました。

今回は、言語指定をIEの「言語の優先順位」で設定しましたが、プログラムで変更する方法については、「冒頭の書籍」や以下のページが参考になります。

ASP.NET MVC 2 Localization complete guide」

http://adamyan.blogspot.jp/2010/02/aspnet-mvc-2-localization-complete.html#!/2010/02/aspnet-mvc-2-localization-complete.html

また、「冒頭の書籍」には言語別のViewを呼び出す手法についての記述もあります。

今回は以上です。

 

---  2013/02/14  追記  ---

アカウント回りの英語対応をやってみて、リソースのフォルダとファイルについては結局、以下のようにroot下の[Resources]、その中に[Global]フォルダを作り、システム全体で共用する文字列をに格納し、[SharedStrings]フォルダでは、コントロール単位で共用するリソースファイルを作成することにしました。モデル、コントローラー、ビューで文字列を共用するケースも多いので、コントローラー単位にリソースファイルを作成しています。

f:id:SannomiyaNotes:20130214182930p:plain

 

ちなみに文脈的にどうしても言語を判定して文章を分けたいケースが出てきました。文章の途中に変数を入れるケースなど、リソースファイルを使っての置き換えでは厳しい(英語力不足の為、、)ケースです。

その場合、C#側で言語を取り出し、ViewBagにセットしてビュー側でif文で分岐させました。例えば

<コントローラー>
using System.Threading;
//言語情報の取得
ViewBag.TwoLetterISOLanguageName = Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName;

 

<ビュー>
@*言語によってメッセージを切り替える*@
 @if (ViewBag.TwoLetterISOLanguageName == "en")
 {
     <p>Login User name is  <strong> @User.Identity.Name </strong></p>
 }
 else
 {
     <p> <strong> @User.Identity.Name </strong>としてログインしています。</p>
 }

追記以上です。

 ---  2013/05/09  追記  ---

続編追加しました。

Visual Studio Express 2012 for Web でいってみる 28.javascriptでResourceObjectを使う方法