在上個月,有位網友在一篇文章下面寫了這樣的留言,留言的文章是「ASP.NET MVC - 修改 CheckBoxList、增加 RadioButtonList」,
因為從四月一直到五月初,我都一直在瘋狂忙碌的狀態,除了幾篇簡短的練習題文章之外,我實在抽不出時間來好好研究這個問題,所以一直延宕到現在才有時間來研究一下這個問題。
這篇文章就來跟大家說明怎麼讓 CheckBoxList 也可以有 Validation 的功能。
CheckBoxList 的相關文章:
ASP.NET MVC 擴充HtmlHelper 加入 CheckBoxList 功能 – 1
ASP.NET MVC 擴充HtmlHelper 加入 CheckBoxList 功能 - 2
jQuery 取得CheckBoxList裡項目有被選取(Checked=true)的值
ASP.NET MVC - 修改 CheckBoxList、增加 RadioButtonLis
我稍微看了一下我之前所寫的 CheckBoxList 與 RadioButtonList,發現到還真的是沒有對前端驗證有做任何的支援,所以就著手進行修改,不過並無法作到像內建的 Html Helper 所能達到的效果,內建的 Html Helper 只需要在 Model 類別裡對 Property 增加 DataAnnotation Attrinbute 就可以在頁面上有驗證的功能與效果,而礙於原本的 CheckBoxList 的限制,所以我盡量作到讓開發人員在 Model 定義時可以不用改變原本使用 DataAnnotation Attribute 的方式,另外在檢視頁面裡的使用也不需要有太大的改變。
開發環境:Visual Studio 2013 Update 2, ASP.NET MVC 5.1.2, EntityFramework 6.1.0
在 Visual Studio 2012 使用 ASP.NET MVC 4 與 EF 5.0 同樣可以使用。
首先,我定義了一個 「Foo.cs」Model
建立 FooController.cs 與內容,那個 CategoryService 不是什麼了不起的東西,就只是一個取得分類資料的類別,
建立 View (~/Views/Foo/Index.cshtml)
一個基本的頁面
這個基本頁面是有驗證與顯示驗證訊息的功能
接著就是要來修改頁面上的分類,要修改為 CheckBoxList,不過這次不會直接把 CheckBoxList 直接用在 Index.cshtml 裡,而是另外建立 EditorTemplate,如此一來就可以在 EditorFor 這個 Html Helper 裡指定 EditorTemplate 與資料,原本頁面不需要做大幅度的變動,而且之後還可以重複使用。
在建立 CheckBoxList 的 EditorTemplate 之前,先來建立 CheckBoxList 的相關程式,
Position.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace WebApplication1.Infrastructure.Enums
{
public enum Position
{
Horizontal,
Vertical
}
}
CheckBoxListExtensions.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using WebApplication1.Infrastructure.Enums;
namespace WebApplication1.Infrastructure.Extensions
{
public static class CheckBoxListExtensions
{
#region -- CheckBoxList (Horizontal) --
/// <summary>
/// CheckBoxList.
/// </summary>
/// <param name="htmlHelper">The HTML helper.</param>
/// <param name="name">The name.</param>
/// <param name="listInfo">SelectListItem.</param>
/// <returns></returns>
public static MvcHtmlString CheckBoxList(this HtmlHelper htmlHelper,
string name,
IEnumerable<SelectListItem> listInfo)
{
return htmlHelper.CheckBoxList(name, listInfo, (IDictionary<string, object>)null, 0);
}
/// <summary>
/// CheckBoxList.
/// </summary>
/// <param name="htmlHelper">The HTML helper.</param>
/// <param name="name">The name.</param>
/// <param name="listInfo">SelectListItem.</param>
/// <param name="htmlAttributes">The HTML attributes.</param>
/// <returns></returns>
public static MvcHtmlString CheckBoxList(this HtmlHelper htmlHelper,
string name,
IEnumerable<SelectListItem> listInfo,
object htmlAttributes)
{
return htmlHelper.CheckBoxList(
name,
listInfo,
(IDictionary<string, object>)new RouteValueDictionary(htmlAttributes),
0);
}
/// <summary>
/// CheckBoxList.
/// </summary>
/// <param name="htmlHelper">The HTML helper.</param>
/// <param name="name">The name.</param>
/// <param name="listInfo">The list info.</param>
/// <param name="htmlAttributes">The HTML attributes.</param>
/// <param name="number">每個Row的顯示個數.</param>
/// <returns></returns>
public static MvcHtmlString CheckBoxList(this HtmlHelper htmlHelper,
string name,
IEnumerable<SelectListItem> listInfo,
IDictionary<string, object> htmlAttributes,
int number)
{
if (String.IsNullOrEmpty(name))
{
throw new ArgumentException("必須給這些 CheckBoxList 一個 Tag Name", "name");
}
if (listInfo == null)
{
throw new ArgumentNullException("listInfo", "必須要給List<SelectListItem> listInfo");
}
if (!listInfo.Any())
{
throw new ArgumentException("List<SelectListItem> listInfo 至少要有一組資料", "listInfo");
}
var sb = new StringBuilder();
var lineNumber = 0;
foreach (SelectListItem info in listInfo)
{
lineNumber++;
TagBuilder builder = new TagBuilder("input");
if (info.Selected)
{
builder.MergeAttribute("checked", "checked");
}
builder.MergeAttributes<string, object>(htmlAttributes);
builder.MergeAttribute("type", "checkbox");
builder.MergeAttribute("value", info.Value);
builder.MergeAttribute("name", name);
builder.MergeAttribute("id", string.Format("{0}_{1}", name, info.Value));
sb.Append(builder.ToString(TagRenderMode.Normal));
var labelBuilder = new TagBuilder("label");
labelBuilder.MergeAttribute("for", string.Format("{0}_{1}", name, info.Value));
labelBuilder.InnerHtml = info.Text;
sb.Append(labelBuilder.ToString(TagRenderMode.Normal));
if (number == 0 || (lineNumber % number == 0))
{
sb.Append("<br />");
}
}
return MvcHtmlString.Create(sb.ToString());
}
#endregion
#region -- CheckBoxListVertical --
/// <summary>
/// Checks the box list vertical.
/// </summary>
/// <param name="htmlHelper">The HTML helper.</param>
/// <param name="name">The name.</param>
/// <param name="listInfo">The list info.</param>
/// <param name="htmlAttributes">The HTML attributes.</param>
/// <param name="columnNumber">The column number.</param>
/// <returns></returns>
public static MvcHtmlString CheckBoxListVertical(this HtmlHelper htmlHelper,
string name,
IEnumerable<SelectListItem> listInfo,
IDictionary<string, object> htmlAttributes,
int columnNumber = 1)
{
if (String.IsNullOrEmpty(name))
{
throw new ArgumentException("必須給這些 CheckBoxList 一個 Tag Name", "name");
}
if (listInfo == null)
{
throw new ArgumentNullException("listInfo", "必須要給 List<CheckBoxListInfo> listInfo");
}
var selectListItems = listInfo as SelectListItem[] ?? listInfo.ToArray();
if (!selectListItems.Any())
{
throw new ArgumentException("List<CheckBoxListInfo> listInfo 至少要有一組資料", "listInfo");
}
var dataCount = selectListItems.Count();
// calculate number of rows
var rows = Convert.ToInt32(Math.Ceiling(Convert.ToDecimal(dataCount) / Convert.ToDecimal(columnNumber)));
if (dataCount <= columnNumber || dataCount - columnNumber == 1)
{
rows = dataCount;
}
var wrapBuilder = new TagBuilder("div");
wrapBuilder.MergeAttribute("style", "float: left; light-height: 25px; padding-right: 5px;");
var wrapStart = wrapBuilder.ToString(TagRenderMode.StartTag);
var wrapClose = string.Concat(wrapBuilder.ToString(TagRenderMode.EndTag), " <div style=\"clear:both;\"></div>");
var wrapBreak = string.Concat("</div>", wrapBuilder.ToString(TagRenderMode.StartTag));
var sb = new StringBuilder();
sb.Append(wrapStart);
var lineNumber = 0;
foreach (var info in selectListItems)
{
var builder = new TagBuilder("input");
if (info.Selected)
{
builder.MergeAttribute("checked", "checked");
}
builder.MergeAttributes<string, object>(htmlAttributes);
builder.MergeAttribute("type", "checkbox");
builder.MergeAttribute("value", info.Value);
builder.MergeAttribute("name", name);
builder.MergeAttribute("id", string.Format("{0}_{1}", name, info.Value));
sb.Append(builder.ToString(TagRenderMode.Normal));
var labelBuilder = new TagBuilder("label");
labelBuilder.MergeAttribute("for", string.Format("{0}_{1}", name, info.Value));
labelBuilder.InnerHtml = info.Text;
sb.Append(labelBuilder.ToString(TagRenderMode.Normal));
lineNumber++;
if (lineNumber.Equals(rows))
{
sb.Append(wrapBreak);
lineNumber = 0;
}
else
{
sb.Append("<br/>");
}
}
sb.Append(wrapClose);
return MvcHtmlString.Create(sb.ToString());
}
#endregion
#region -- CheckBoxList (Horizonal, Vertical) --
/// <summary>
/// Checks the box list.
/// </summary>
/// <param name="htmlHelper">The HTML helper.</param>
/// <param name="name">The name.</param>
/// <param name="listInfo">The list info.</param>
/// <param name="htmlAttributes">The HTML attributes.</param>
/// <param name="position">The position.</param>
/// <param name="number">Position.Horizontal則表示每個Row的顯示個數, Position.Vertical則表示要顯示幾個Column</param>
/// <returns></returns>
public static MvcHtmlString CheckBoxList(this HtmlHelper htmlHelper,
string name,
IEnumerable<SelectListItem> listInfo,
IDictionary<string, object> htmlAttributes,
Position position = Position.Horizontal,
int number = 0)
{
if (String.IsNullOrEmpty(name))
{
throw new ArgumentException("必須給這些 CheckBoxList 一個 Tag Name", "name");
}
if (listInfo == null)
{
throw new ArgumentNullException("listInfo", "必須要給List<SelectListItem> listInfo");
}
var selectListItems = listInfo as SelectListItem[] ?? listInfo.ToArray();
if (!selectListItems.Any())
{
throw new ArgumentException("List<SelectListItem> listInfo 至少要有一組資料", "listInfo");
}
var sb = new StringBuilder();
var lineNumber = 0;
switch (position)
{
case Position.Horizontal:
foreach (var info in selectListItems)
{
lineNumber++;
sb.Append(CreateCheckBoxItem(info, name, htmlAttributes));
if (number == 0 || (lineNumber % number == 0))
{
sb.Append("<br />");
}
}
break;
case Position.Vertical:
var dataCount = selectListItems.Count();
// 計算最大顯示的列數(rows)
var rows = Convert.ToInt32(Math.Ceiling(Convert.ToDecimal(dataCount) / Convert.ToDecimal(number)));
if (dataCount <= number || dataCount - number == 1)
{
rows = dataCount;
}
var wrapBuilder = new TagBuilder("div");
wrapBuilder.MergeAttribute("style", "float: left; light-height: 25px; padding-right: 5px;");
var wrapStart = wrapBuilder.ToString(TagRenderMode.StartTag);
var wrapClose = string.Concat(wrapBuilder.ToString(TagRenderMode.EndTag), " <div style=\"clear:both;\"></div>");
var wrapBreak = string.Concat("</div>", wrapBuilder.ToString(TagRenderMode.StartTag));
sb.Append(wrapStart);
foreach (var info in selectListItems)
{
lineNumber++;
sb.Append(CreateCheckBoxItem(info, name, htmlAttributes));
if (lineNumber.Equals(rows))
{
sb.Append(wrapBreak);
lineNumber = 0;
}
else
{
sb.Append("<br/>");
}
}
sb.Append(wrapClose);
break;
}
return MvcHtmlString.Create(sb.ToString());
}
/// <summary>
/// Creates the check box item.
/// </summary>
/// <param name="info">The info.</param>
/// <param name="name">The name.</param>
/// <param name="htmlAttributes">The HTML attributes.</param>
/// <returns></returns>
internal static string CreateCheckBoxItem(
SelectListItem info,
string name,
IDictionary<string, object> htmlAttributes)
{
var sb = new StringBuilder();
var labelBuilder = new TagBuilder("label");
labelBuilder.MergeAttribute("for", string.Format("{0}_{1}", name, info.Value));
sb.Append(labelBuilder.ToString(TagRenderMode.StartTag));
var builder = new TagBuilder("input");
if (info.Selected)
{
builder.MergeAttribute("checked", "checked");
}
builder.MergeAttributes<string, object>(htmlAttributes);
builder.MergeAttribute("type", "checkbox");
builder.MergeAttribute("value", info.Value);
builder.MergeAttribute("name", name);
builder.MergeAttribute("id", string.Format("{0}_{1}", name, info.Value));
sb.Append(builder.ToString(TagRenderMode.Normal));
sb.AppendFormat(" {0}",info.Text);
sb.Append("</label>");
return sb.ToString();
}
#endregion
}
}
建立 ~/Views/Shared/EditorTemplates/CheckBoxList.cshtml
@using WebApplication1.Infrastructure.Enums
@using WebApplication1.Infrastructure.Extensions
@{
var require = false;
object validationMessage = string.Empty;
var validationAttributes = Html.GetUnobtrusiveValidationAttributes("");
if (validationAttributes.ContainsKey("data-val")
&&
validationAttributes.ContainsKey("data-val-required"))
{
require = true;
if (!validationAttributes.TryGetValue("data-val-required", out validationMessage))
{
validationMessage = "This field is required.";
}
validationAttributes.Add("required", "required");
}
var tagName = ViewData["TagName"] == null
? "CheckBoxList"
: (string)ViewData["TagName"];
var checkboxItems = ViewData["CheckBoxItems"] == null
? new List<SelectListItem>()
: (IEnumerable<SelectListItem>)ViewData["CheckBoxItems"];
var position = ViewData["Position"] == null
? Position.Horizontal
: (Position)ViewData["Position"];
var numbers = 0;
if (ViewData["Numbers"] == null)
{
numbers = 1;
}
else if (!int.TryParse(ViewData["Numbers"].ToString(), out numbers))
{
numbers = 1;
}
}
@Html.CheckBoxList(
tagName,
checkboxItems,
new RouteValueDictionary(validationAttributes),
position,
numbers)
@Html.ValidationMessage(tagName,
"",
new
{
@class = "text-danger",
data_valmsg_for = tagName
})
在 CheckBoxList.cshtml 裡,一開始要先抓出放在 Model 類別裡標示在 Property 的 DataAnnotation Attributes 資訊,當需要驗證以及標示為必選的時候,就必須在 CheckBox 裡再加入 required 的 Html Attribute,並且還要抓出必選條件的 ErrorMessage 內容。
然後有定義四個參數是從 ViewData 裡取得資料,分別是 TagName, CheckBoxItems, Position, Numbers,因為使用了 EditorTemplate 後,要傳送資料進去就要以下的多載方法(下面的程式),將資料放到 additionalViewdata 裡,然後 CheckBoxList.cshtml 再從 ViewData 拿到資料。
public static MvcHtmlString EditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string templateName, object additionalViewData)
{
return html.TemplateFor<TModel, TValue>(expression, templateName, null, DataBoundControlMode.Edit, additionalViewData);
}
而 CheckBoxList.cshtml 最後所使用的 Html.ValidationMessage() 需要特別去指定 data_valmsg_for 的名稱,如果不指定的話,將會 Redenr 為「Categories.Categories」,如此一來就會無法正確的顯示驗證訊息,另外就是 ValidationMessage 的訊息內容也設定為空字串,如果有設定的話,將會直接就顯示在頁面上,所以就要設定為空字串,之後再到 Index.cshtml 裡另外使用前端程式去加入驗證訊息。
修改 Index.cshtml
以下是 ~/Views/Foo/Index.cshtml 的全部內容
@using WebApplication1.Infrastructure.Enums
@model WebApplication1.Models.ViewModels.Foo
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Foo</h4>
<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
<div class="form-group">
@Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Categories, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@*@Html.EditorFor(model => model.Categories, new { htmlAttributes = new { @class = "form-control" } })*@
@*@Html.ValidationMessageFor(model => model.Categories, "", new { @class = "text-danger" })*@
@Html.EditorFor(model => model.Categories,
"CheckBoxList",
new
{
TagName = "Categories",
CheckBoxItems = ViewBag.CategoryItems,
Position = Position.Vertical,
Numbers = 3
})
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
<script type="text/javascript">
$(function () {
$('input[name="Categories"]').rules('add', {
messages: {
required: "請至少選擇一個分類"
}
});
});
</script>
}
這邊先看「分類」的部份,是在 Html.EditorFor() 裡指定使用 EditorTemplate「CheckBoxList.cshtml」,第二個參數是指定要使用那一個 EditorTemplate,第三個參數就是「additionalViewdata 」分別指定要傳送到 CheckBoxList.cshtml 裡所要用的資料,
再來就是前端程式的部份,
因為無法使用 ASP.NET MVC 內建的方法直接對 CheckBoxList 產生驗證,所以就在頁面的前端程式加入驗證訊息。
執行結果
一開始的執行頁面
什麼都不填、不勾選,直接按下「Create」
在分類項目裡勾選一個
以下為完整的操作過程
以上就是在 ASP.NET MVC 5 與 Bootstrap 3 的情況下設定 CheckBoxList 有前端驗證的操作處理,下一篇文章再來看看 ASP.NET MVC 4 以及沒有使用 Bootstrap 情況下又該如何處理,等下一篇文章寫好之後再將程式放到 GitHub 上面讓大家參考。
參考連結
ASP.NET MVC學習筆記(十一)-超好用的Templates - 我的Coding之路- 點部落
以上
沒有留言:
張貼留言