[ASP Net MVC] 使用CheckBoxListFor擴充功能簡化View之設計

使用CheckBoxListFor擴充功能簡化View之設計

前言

 

在開發MVC網站的時候,每當遇到畫面要產生CheckBoxList的時候,往往都是透過ViewBag把選項資料List<SelectListItem>傳到前端,然後於View取出資料利用迴圈逐一產出選項Html代碼;但這種事情做多了其實很煩(懶),所以就搜尋了一下是否有比較好的方式來處理這個問題。果然天下懶人不會只有我一個,剛好就發MvcCheckBoxList這個套件所提供之CheckBoxList(For)擴充方法,以下將針對此擴充方法進行簡單實作。

 

 

實作方式

 

首先透過NuGet安裝MvcCheckBoxList套件

image

 

首先來了解一下此擴充功能之Razor語法基本使用方式,以方便後續ViewModel的設計

image

簡單來說ViewModel中需要提供以下屬性:

1. 所有的選項清單 ex. IList<Item> AvariableItems

2. 被選的選項清單 ex. IList<Item> SelectedItems

3. 存放前端POST回的所有被選取清單ID ex. List<string> PostedItemIds

 

 

我們將以一個簡單的例子進行實作。以下是一個老梗的使用者-角色資料表,以此設計一個使用者建立及編輯畫面,可以依據資料庫Role Table中資料作為CheckBoxList資料選項來源。

 

image

 

各對應類別如下


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; }
}

 

新增使用者畫面如下

image

 

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);
                    }
                }
            }
        }
    }
}

 

測試一下畫面是否正常顯示出所有角色清單

image

 

點選Create後,前端POST回的所有被選取清單ID 及 使用者資訊都順利的Binding到ViewModel上

image

image

 

順利寫入DB

image

 

最後把編輯功能畫面一併完成

image

 

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標籤的話,請參考官方網站提供的詳細範例說明。

 

 

參考資料

 

http://mvccbl.com/


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

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