ASP.NET MVC Model Binding (GridView Edit)
2023-07-23
筆記 ASP.NET MVC 5 使用 Model Binding 處理使用者與應用系統之間的資料傳送與處理。
Without Model Binding
// QueryString id
Request["id"]
Request.QueryString["id"]
// Form id
Request.Form["id"]
// Route id
Request.RequestContext.RouteData.Values["id"] as string)
// Files
HttpPostedFileBase file = Request.Files[0]
// Application/json
string jsonData;
using (var reader = new StreamReader(Request.InputStream, Encoding.UTF8))
{
jsonData = reader.ReadToEnd();
}
var person = JsonConvert.DeserializeObject<Person>(jsonData);
Value Provider 順序
Form Fields > Application/json > Route > Query String > Posted Files
Interesting Topics
-
簡單型別 / 複雜型別 / 集合
-
Bind Prefix
應用
GridView Edit
Controller
public ActionResult GridviewEdit()
{
return View(db.Regions.ToList());
}
[HttpPost]
public ActionResult GridviewEdit(IList<Region> regions)
{
foreach (var region in regions)
{
db.Regions.AddOrUpdate(region);
}
db.SaveChanges();
return View(db.Regions.ToList());
}
View
@model IList<NorthwindShop2.Web.Models.Region>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
@using (Html.BeginForm("GridViewEdit", "ModelBinding", FormMethod.Post))
{
<table class="table table-bordered">
<thead>
<tr>
<th>Regiod Id</th>
<th>Description</th>
</tr>
</thead>
<tbody>
@for (int i = 0; i < Model.Count; i++)
{
<tr>
<td>
<div class="form-group">
@Html.HiddenFor(m => m[i].RegionID)
<input type="text" value="@Model[i].RegionID" class="form-control" disabled />
</div>
</td>
<td>
<div class="form-group">
@Html.TextBoxFor(m => m[i].RegionDescription, new { @class = "form-control" })
</div>
</td>
</tr>
}
</tbody>
</table>
}
</div>
}
Dual Entity Edit
Model.cs
public partial class Categories
{
public Categories()
{
this.Products = new HashSet<Products>();
}
public int CategoryID { get; set; }
public string CategoryName { get; set; }
public string Description { get; set; }
public virtual ICollection<Products> Products { get; set; }
}
public partial class Products
{
public int ProductID { get; set; }
public string ProductName { get; set; }
public int CategoryID { get; set; }
public Nullable<decimal> UnitPrice { get; set; }
public Nullable<short> UnitsInStock { get; set; }
public virtual Categories Categories { get; set; }
}
兩個類別的 EDMX 關聯如圖:
View.cshtml
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Products</h4>
<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
@Html.HiddenFor(model => model.ProductID)
@Html.HiddenFor(model => model.Categories.CategoryID)
<div class="form-group">
@Html.LabelFor(model => model.ProductName, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.ProductName, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.ProductName, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.CategoryID, "CategoryID", htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.DropDownList("CategoryID", null, htmlAttributes: new { @class = "form-control" })
@Html.ValidationMessageFor(model => model.CategoryID, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.UnitPrice, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.UnitPrice, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.UnitPrice, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.UnitsInStock, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.UnitsInStock, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.UnitsInStock, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Categories.CategoryName, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Categories.CategoryName, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Categories.CategoryName, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Categories.Description, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Categories.Description, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Categories.Description, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group mt-3">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-outline-primary me-3" />
@Html.ActionLink("Back to List", "Index", null, new {@class= "btn btn-outline-secondary" })
</div>
</div>
</div>
}
除了 Scaffolding 提供的欄位外,額外新增了 HiddenFor(model => model.Categories.CategoryID)
, model.Categories.CategoryName
, model.Categories.Description
來達到複數 Entities 的綁定成功。
Controller.cs
// GET: Products/Edit/5
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Products products = db.Products.Find(id);
if (products == null)
{
return HttpNotFound();
}
ViewBag.CategoryID = new SelectList(
db.Categories, "CategoryID", "CategoryName",
products.CategoryID);
return View(products);
}
// POST: Products/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind] Products products)
{
if (ModelState.IsValid)
{
db.Entry(products).State = EntityState.Modified;
db.Entry(products.Categories).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
ViewBag.CategoryID = new SelectList(db.Categories, "CategoryID", "CategoryName", products.CategoryID);
return View(products);
}
需要加上額外的 db.Entry
來綁定除了主 Entity 外的其他 Entity,並設定為 EntityState.Modifies
來達成儲存置資料庫。
注意在 Post 的 Action 的 Parameter 綁定,有多種寫法達到多個 Entities 的更新,除了範例的方式外,也可以使用以下方式來綁定。
[Bind(Include = ("ProductID, ProductName, CategoryId, UnitPrice, UnitsInStock, Categories"))]
需要注意的是上述兩種綁定方式,因為沒有提供的值會清空資料表原來資料列的值,所枚舉的所有欄位名稱也有可能受到 OverPosting 的攻擊,如果要避免可以用 TryUpdateModel 搭配 Entity 的 Id,先取得 Entity 再透過 TryUpdateModel 去更新 Entity 並只允許特定的欄位。
TryUpdateModel
Model Binding 是被動式的綁定,藉由比對 Controller Action 的參數,透過 DefaultModelBinder 來進行綁定。而也可以進行主動式的綁定,使用 UpdateModel 以及 TryUpdateModel 的方式來達成。
public ActionResult Edit(int Id, FormCollection FormValue)
{
Product product = db.Product.FirstOrDefault(p => p.Id == Id);
if (TryUpdateModel(product, FormValue.AllKeys) && ModelState.IsValid)
{
db.SaveChanges();
return RedirectToAction("Index");
}
return View(product);
}
Referential integrity constraint violation
A referential integrity constraint violation occurred: The property value(s) of 'Categories.CategoryID' on one end of a relationship do not match the property value(s) of 'Products.CategoryID' on the other end.
需要注意目前的設定方式,如果變更 Product 所屬的 CategoryID 就會發生錯誤。
權衡的緩解方式如下,判斷如果 CategoryID 發生改變,則只改變 Product 本身,不去調整關聯到 Category 內容,否則才依照原本的邏輯進行複數 Entity 調整。
if (ModelState.IsValid)
{
if (products.CategoryID != products.Categories.CategoryID)
{
products.Categories = db.Categories.FirstOrDefault(i => i.CategoryID == products.CategoryID);
db.Entry(products).State = EntityState.Modified;
}
else
{
db.Entry(products).State = EntityState.Modified;
db.Entry(products.Categories).State = EntityState.Modified;
}
db.SaveChanges();
return RedirectToAction("Index");
}
參考資料
ASP.NET MVC - The Features and Foibles of ASP.NET MVC Model Binding