使用CheckBoxListFor擴充功能簡化View之設計
前言
在開發MVC網站的時候,每當遇到畫面要產生CheckBoxList的時候,往往都是透過ViewBag把選項資料List<SelectListItem>傳到前端,然後於View取出資料利用迴圈逐一產出選項Html代碼;但這種事情做多了其實很煩(懶),所以就搜尋了一下是否有比較好的方式來處理這個問題。果然天下懶人不會只有我一個,剛好就發MvcCheckBoxList這個套件所提供之CheckBoxList(For)擴充方法,以下將針對此擴充方法進行簡單實作。
實作方式
首先透過NuGet安裝MvcCheckBoxList套件
首先來了解一下此擴充功能之Razor語法基本使用方式,以方便後續ViewModel的設計
簡單來說ViewModel中需要提供以下屬性:
1. 所有的選項清單 ex. IList<Item> AvariableItems
2. 被選的選項清單 ex. IList<Item> SelectedItems
3. 存放前端POST回的所有被選取清單ID ex. List<string> PostedItemIds
我們將以一個簡單的例子進行實作。以下是一個老梗的使用者-角色資料表,以此設計一個使用者建立及編輯畫面,可以依據資料庫Role Table中資料作為CheckBoxList資料選項來源。
各對應類別如下
public partial class User
{
public User()
{
this.UserTasks = new HashSet<UserTask>();
this.Roles = new HashSet<Role>();
RegisterOn = DateTime.Now;
IsEnable = true;
}
public string UserId { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string Name { get; set; }
public System.DateTime RegisterOn { get; set; }
public bool IsEnable { get; set; }
public virtual ICollection<Role> Roles { get; set; }
}
// 角色
public partial class Role
{
public Role()
{
this.Menus = new HashSet<Menu>();
this.Users = new HashSet<User>();
}
public int RoleId { get; set; }
public string Name { get; set; }
public bool IsEnable { get; set; }
public virtual ICollection<User> Users { get; set; }
}
新增使用者畫面如下
ViewModel會依照CheckBoxListFor需求加以設計
{
public UserViewModel()
{
User = new User();
AvailableRoles = new List<Role>();
SelectedRoles = new List<Role>();
PostedRoleIds = new List<string>();
}
// 使用者
public User User { get; set; }
// CheckBoxList中的選項清單
public IEnumerable<Role> AvailableRoles { get; set; }
// CheckBoxList中被選取的選項清單
public IEnumerable<Role> SelectedRoles { get; set; }
// CheckBoxList的名稱,也是被勾選資料Post回Server時Data binding之目標物件
public List<string> PostedRoleIds { get; set; }
}
View將使用CheckBoxListFor擴充方法產生Role的CheckBoxList供使用者點選
@using MvcCheckBoxList.Model
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>User</h4>
<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
<div class="form-group">
@Html.LabelFor(model => model.User.UserId, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.User.UserId, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.User.UserId, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.User.Email, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.User.Email, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.User.Email, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.User.Password, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.User.Password, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.User.Password, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.User.Name, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.User.Name, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.User.Name, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.User.Roles, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
<!-- 使用CheckBoxListFor擴充方法產生Role的CheckBoxList -->
@Html.CheckBoxListFor(model => model.PostedRoleIds,
model => model.AvailableRoles,
role => role.RoleId,
role => role.Name,
model => model.SelectedRoles,
Position.Vertical)
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" onclick="return validateRoleList();" />
</div>
</div>
</div>
}
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
<script>
// 驗證是否點選
function validateRoleList()
{
if ($('input[name="PostedRoleIds"]:checked').length == 0) {
alert("Atleast one CheckBox is checked");
return false;
}
}
</script>
}
最後Controll就只需依照所需資訊填入即可
{
private TestEntities db = new TestEntities();
// 取得所有角色清單
public IEnumerable<Role> GetAllRoles()
{
return db.Roles;
}
// 新增使用者
public ActionResult Create()
{
UserViewModel viewModel = new UserViewModel();
viewModel.User = new User();
viewModel.AvailableRoles = GetAllRoles(); // 所有的角色清單
viewModel.SelectedRoles = null; // 被選的角色清單
return View(viewModel);
}
// 新增使用者
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(UserViewModel viewModel)
{
// 篩出角色清單, viewModel.PostedRoleIds 為被選擇的角色Id List
var selectedRoles =
GetAllRoles().Where(r => viewModel.PostedRoleIds.Contains(r.RoleId.ToString()));
// reload check box list
viewModel.AvailableRoles = GetAllRoles(); // 所有的角色清單
viewModel.SelectedRoles = selectedRoles; // 被選的角色清單
if (ModelState.IsValid)
{
// add role to user
UpdateRoleRelation(viewModel.User, viewModel.PostedRoleIds);
// save user
db.Users.Add(viewModel.User);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(viewModel);
}
// 更新使用者角色
private void UpdateRoleRelation(User user, List<string> selectedRoleIds)
{
if (selectedRoleIds == null)
{
user.Roles.Clear();
return;
}
else
{
var currentRoleIds = user.Roles.Select(x => x.RoleId);
foreach (var role in GetAllRoles())
{
if (selectedRoleIds.Contains(role.RoleId.ToString()))
{
// 此role有被勾選到
if (!currentRoleIds.Contains(role.RoleId))
{
// 如果原本member沒有這個角色 就要增加
user.Roles.Add(role);
}
}
else
{
// 此role沒有被勾選到
if (currentRoleIds.Contains(role.RoleId))
{
// 如果原本member有這個角色 就要移除
user.Roles.Remove(role);
}
}
}
}
}
}
測試一下畫面是否正常顯示出所有角色清單
點選Create後,前端POST回的所有被選取清單ID 及 使用者資訊都順利的Binding到ViewModel上
順利寫入DB
最後把編輯功能畫面一併完成
Edit.cshtml
@using MvcCheckBoxList.Model
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>User</h4>
<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
@Html.HiddenFor(model => model.User.UserId)
<div class="form-group">
@Html.LabelFor(model => model.User.Email, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.User.Email, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.User.Email, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.User.Name, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.User.Name, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.User.Name, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.User.IsEnable, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
<div class="checkbox">
@Html.EditorFor(model => model.User.IsEnable)
@Html.ValidationMessageFor(model => model.User.IsEnable, "", new { @class = "text-danger" })
</div>
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.User.Roles, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
<!-- 使用CheckBoxListFor擴充方法產生Role的CheckBoxList -->
@Html.CheckBoxListFor(model => model.PostedRoleIds,
model => model.AvailableRoles,
role => role.RoleId,
role => role.Name,
model => model.SelectedRoles,
Position.Vertical)
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
}
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
完整Controller如下
{
private TestEntities db = new TestEntities();
// 新增使用者
public ActionResult Create()
{
UserViewModel viewModel = new UserViewModel();
viewModel.User = new User();
viewModel.AvailableRoles = GetAllRoles(); // 所有的角色清單
viewModel.SelectedRoles = null; // 被選的角色清單
return View(viewModel);
}
// 新增使用者
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(UserViewModel viewModel)
{
// 篩出角色清單, viewModel.PostedRoleIds 為被選擇的角色Id List
var selectedRoles =
GetAllRoles().Where(r => viewModel.PostedRoleIds.Contains(r.RoleId.ToString()));
// reload check box list
viewModel.AvailableRoles = GetAllRoles(); // 所有的角色清單
viewModel.SelectedRoles = selectedRoles; // 被選的角色清單
if (ModelState.IsValid)
{
// add role to user
UpdateRoleRelation(viewModel.User, viewModel.PostedRoleIds);
// save user
db.Users.Add(viewModel.User);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(viewModel);
}
// 編輯使用者
public ActionResult Edit(string id)
{
if (id == null)
{ return new HttpStatusCodeResult(HttpStatusCode.BadRequest); }
UserViewModel viewModel = new UserViewModel();
viewModel.User = db.Users.Find(id); // 編輯的User
viewModel.AvailableRoles = GetAllRoles(); // 所有的角色清單
viewModel.SelectedRoles = viewModel.User.Roles; // 被選的角色清單
if (viewModel.User == null)
{ return HttpNotFound(); }
return View(viewModel);
}
// 編輯使用者
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(UserViewModel viewModel)
{
// 篩出角色清單, viewModel.PostedRoleIds 為被選擇的角色Id List
var selectedRoles =
GetAllRoles().Where(r => viewModel.PostedRoleIds.Contains(r.RoleId.ToString()));
// reload check box list
viewModel.AvailableRoles = GetAllRoles(); // 所有的角色清單
viewModel.SelectedRoles = selectedRoles; // 被選的角色清單
if (ModelState.IsValid)
{
// Get current user
var currentUser = db.Users.Where( u => u.UserId == viewModel.User.UserId).FirstOrDefault();
// update user info
currentUser.Name = viewModel.User.Name;
currentUser.Email = viewModel.User.Email;
currentUser.IsEnable = viewModel.User.IsEnable;
// add/remove role to user
UpdateRoleRelation(currentUser, viewModel.PostedRoleIds);
// update db
db.Entry(currentUser).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(viewModel);
}
// 顯示所有使用者
public ActionResult Index()
{
return View(db.Users.ToList());
}
// 取得所有角色清單
public IEnumerable<Role> GetAllRoles()
{
return db.Roles;
}
// 更新使用者角色
private void UpdateRoleRelation(User user, List<string> selectedRoleIds)
{
if (selectedRoleIds == null)
{
user.Roles.Clear();
return;
}
else
{
var currentRoleIds = user.Roles.Select(x => x.RoleId);
foreach (var role in GetAllRoles())
{
if (selectedRoleIds.Contains(role.RoleId.ToString()))
{
// 此role有被勾選到
if (!currentRoleIds.Contains(role.RoleId))
{
// 如果原本member沒有這個角色 就要增加
user.Roles.Add(role);
}
}
else
{
// 此role沒有被勾選到
if (currentRoleIds.Contains(role.RoleId))
{
// 如果原本member有這個角色 就要移除
user.Roles.Remove(role);
}
}
}
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{ db.Dispose(); }
base.Dispose(disposing);
}
}
後記
這個擴充功能其實就是簡化了View的設計方式,透過HtmlHelper擴充方法CheckBoxListFor將所需的核取方塊清單建立出來;所帶來的好處是可以搭配ViewModel的設計,享受到強型別操作的便利性,並且讓View設計畫面上保持清爽的Razor語法,可讀性也會因此較佳。本文只使用到基本功能,如需讓選項套用DisplayTemplates或加註額外Html標籤的話,請參考官方網站提供的詳細範例說明。
參考資料
希望此篇文章可以幫助到需要的人
若內容有誤或有其他建議請不吝留言給筆者喔 !