喜帳面の日記

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);
で、空のビューを戻しています。画面上の情報を初期化しようとの意図ですが、実際は効き目なしで、入力された値が再度表示されます。とほほ状態です。
 
 今回は以上です。