[ASP Net MVC] 如何透過 Editor Template 綁定整個List資料

使用樣板(Template)綁定(Binding)整個List資料進行編輯

緣起

通常在設計上都會希望一次針對單筆資料進行異動,但有時候確實需要針對一系列的資訊進行編輯(例如:修改購物車內各商品購買數量),這時候就需要對List中的各筆資料進行資料綁定(Data Binding)後,將所有Input資料Post傳回Server以接續後端資料庫異動作業。

 

實作

當筆者初次接觸MVC時,針對使用者待做事項清單的異動需求寫了以下的Code。

介面如下圖所示,按下Save後會將所有待做事項資料Post到Server端以更新清單內所有資料。

image

 

Models

// 使用者
public class User
{
    public string UserId { get; set; }
    public string Name { get; set; }
    public string Phone { get; set; }
}

// 使用者待做事項
public class UserTask
{
    public string UserTaskId { get; set; }
    public string TaskName { get; set; }
    public DateTime CompletedDate { get; set; }

    // FK
    public string UserId { get; set; }

    // Navigation
    public virtual User User { get; set; }
}

 

Controller

public ActionResult Edit()
{
    // prepare default view model for testing
    var viewModel = new ViewModels.Task.IndexTaskViewModel();
    viewModel.User = new Models.User() { Name = "Chirs Chen", Phone = "0800-000-000", UserId = "Chris" };
    viewModel.UserTasks = new List<Models.UserTask>() 
    {
        new Models.UserTask() {UserId = "Chris", TaskName="do something 1", CompletedDate = System.DateTime.Now},
        new Models.UserTask() {UserId = "Chris", TaskName="do something 2", CompletedDate = System.DateTime.Now},
        new Models.UserTask() {UserId = "Chris", TaskName="do something 3", CompletedDate = System.DateTime.Now},
        new Models.UserTask() {UserId = "Chris", TaskName="do something 4", CompletedDate = System.DateTime.Now},
    };

    return View(viewModel);
}

[HttpPost]
public ActionResult Edit(ViewModels.Task.IndexTaskViewModel viewModel)
{
    // save ...
    return RedirectToAction("Index");
}

View Model

public class IndexTaskViewModel
{
    public User User { get; set; }
    public List<UserTask> UserTasks { get; set; }
}

 

View

@model JsWebApp.ViewModels.Task.IndexTaskViewModel

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true, "", new { @class = "text-danger" })
    
    <table class="table">
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.UserTasks[0].TaskName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.UserTasks[0].CompletedDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.UserTasks[0].UserId)
            </th>
            <th></th>
        </tr>

        @foreach (var item in Model.UserTasks)
        {
            @Html.HiddenFor(model => item.UserTaskId)

            <tr>
                <td>
                    @Html.EditorFor(model => item.TaskName, new { htmlAttributes = new { @class = "form-control" } })
                    @Html.ValidationMessageFor(model => item.TaskName, "", new { @class = "text-danger" })
                </td>
                <td>
                    @Html.EditorFor(model => item.CompletedDate, new { htmlAttributes = new { @class = "form-control" } })
                    @Html.ValidationMessageFor(model => item.CompletedDate, "", new { @class = "text-danger" })
                </td>
                <td>
                    @Html.EditorFor(model => item.UserId, new { htmlAttributes = new { @class = "form-control" } })
                    @Html.ValidationMessageFor(model => item.UserId, "", new { @class = "text-danger" })
                </td>
            </tr>
        }

    </table>
    <input type="submit" value="Save" class="btn btn-default" />

}

 

問題發生

依照上述方式實作後,畫面完全依照需求呈現;但是,當筆者要著手處理Controller中接收之View Model資料時,卻赫然發現內容都是空白!! 查詢了一下頁面Html後發現,透過Razor語法產出清單內各筆Element的Name都是相同的,所以在Post資料到Server端的時候,MVC無法將所有資料解析綁定至View Model中。

 

image

image

 

解決方式

方法一、使用 For Loop 取代 Foreach Loop 呈現資料

如此即可以透過Razor語法以陣列方式產生Element Name,並藉由MVC剖析Post至Server的資料,直接Binding至ViewModel中供Controller使用。

@model JsWebApp.ViewModels.Task.IndexTaskViewModel

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true, "", new { @class = "text-danger" })

    <table class="table">
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.UserTasks[0].TaskName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.UserTasks[0].CompletedDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.UserTasks[0].UserId)
            </th>
            <th></th>
        </tr>

        <!-- Using For Loop to Get Correct Element Name -->
        @for (int i = 0; i < Model.UserTasks.Count; i++)
        {
            @Html.HiddenFor(model => Model.UserTasks[i].UserTaskId)

            <tr>
                <td>
                    @Html.EditorFor(model => Model.UserTasks[i].TaskName, new { htmlAttributes = new { @class = "form-control" } })
                    @Html.ValidationMessageFor(model => Model.UserTasks[i].TaskName, "", new { @class = "text-danger" })
                </td>
                <td>
                    @Html.EditorFor(model => Model.UserTasks[i].CompletedDate, new { htmlAttributes = new { @class = "form-control" } })
                    @Html.ValidationMessageFor(model => Model.UserTasks[i].CompletedDate, "", new { @class = "text-danger" })
                </td>
                <td>
                    @Html.EditorFor(model => Model.UserTasks[i].UserId, new { htmlAttributes = new { @class = "form-control" } })
                    @Html.ValidationMessageFor(model => Model.UserTasks[i].UserId, "", new { @class = "text-danger" })
                </td>
            </tr>
        }

    </table>
    <input type="submit" value="Save" class="btn btn-default" />

}

image

image

 

方法二、使用Edit Template進行實作

於Views/Shared中建立EditorTemplates資料夾,並在該資料夾下建立我們所需之Template View (注意名稱必須與對應類別名稱相符),並在此Template中明確定義Model以及希望在修改此類別物件時所呈現的畫面。

 

image

@model  JsWebApp.Models.UserTask

@Html.HiddenFor(model => model.UserTaskId)

<tr>
    <td>
        @Html.EditorFor(model => model.TaskName, new { htmlAttributes = new { @class = "form-control" } })
        @Html.ValidationMessageFor(model => model.TaskName, "", new { @class = "text-danger" })
    </td>
    <td>
        @Html.EditorFor(model => model.CompletedDate, new { htmlAttributes = new { @class = "form-control" } })
        @Html.ValidationMessageFor(model => model.CompletedDate, "", new { @class = "text-danger" })
    </td>
    <td>
        @Html.EditorFor(model => model.UserId, new { htmlAttributes = new { @class = "form-control" } })
        @Html.ValidationMessageFor(model => model.UserId, "", new { @class = "text-danger" })
    </td>
</tr>

在View中我們僅需要使用 Html.EditorFor() 方法引入需修改的物件,MVC將會自動依照類別型態找到對應的Template,並依照Template中定義的修改畫面進行呈現。另外,在轉換的過程中當發現目標修改物件為集合時,MVC也會很聰明地將集合中的每一筆資料依照Template中制定的方式逐一呈現,如同此例筆者並未使用任何迴圈來處理集合內的各物件,卻可以在畫面上呈現出集合內所有資料。

@model JsWebApp.ViewModels.Task.IndexTaskViewModel

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true, "", new { @class = "text-danger" })

    <table class="table">
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.UserTasks[0].TaskName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.UserTasks[0].CompletedDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.UserTasks[0].UserId)
            </th>
        </tr>

        <!-- Using Edit Template to Edit List Data -->
        @Html.EditorFor(model => model.UserTasks)
   
    </table>
    <input type="submit" value="Save" class="btn btn-default" />

}

 

image

image


希望此篇文章可以幫助到需要的人

若內容有誤或有其他建議請不吝留言給筆者喔 !