在前面的几篇文章中,已经在控制台和界面实现了属性值的笛卡尔乘积,这是商品模块中的一个难点。本篇就来实现在ASP.NET MVC4下商品模块的一个小样。与本篇相关的文章包括:
1、ASP.NET MVC中实现属性和属性值的组合,即笛卡尔乘积01, 在控制台实现
2、ASP.NET MVC中实现属性和属性值的组合,即笛卡尔乘积02, 在界面实现
3、再议ASP.NET MVC中CheckBoxList的验证
4、ASP.NET MVC在服务端把异步上传的图片裁剪成不同尺寸分别保存,并设置上传目录的尺寸限制
5、ASP.NET MVC异步验证是如何工作的01,jQuery的验证方式、错误信息提示、validate方法的背后
6、ASP.NET MVC异步验证是如何工作的02,异步验证表单元素的创建
7、ASP.NET MVC异步验证是如何工作的03,jquery.validate.unobtrusive.js是如何工作的
8、MVC批量更新,可验证并解决集合元素不连续控制器接收不完全的问题
9、MVC扩展生成CheckBoxList并水平排列
本篇主要包括:
□ 商品模块小样简介
□ 领域模型和视图模型
□ 控制器和视图实现
商品模块小样简介
※ 界面
○ 类别区域,用来显示产品类别,点击选择某个类别,在"产品属性"区域出现该类别下的所有属性,以及属性值,对于单选的属性值用Select显示,对于多选的属性值用CheckBoxList显示。
○ 产品描述,表示数据库中产品表中的字段,当然实际情况中,这里的字段更多,比如上传时间,是否通过,产品卖点,等等。
○ 产品属性,只有点击选择产品类别,这里才会显示
○ 定价按钮,点击这个按钮,如果"产品属性"区域中有CheckBoxList项,"产品SKU与定价"区域会出现关于属性值、产品价格的SKU组合项;如果"产品属性"区域中没有CheckBoxList项,"产品SKU与定价"区域只出现一个有关价格的input元素。另外,每次点击定价按钮,出现提交按钮,定价按钮隐藏。
○ 产品SKU与定价:这里要么呈现属性值、价格的SKU项,要么只出现一个有关价格的input元素
※ 点击类别项,在"产品属性"区域包括CheckBoxList
○ 点击类名中的"家电"选项,在"产品属性"区域中出现属性及其值,有些属性值以Select呈现,有些属性值以CheckBoxList呈现
○ 点击属性行后面的"删除行"直接删除属性行
※ 点击类别项,在"产品属性"区域包括CheckBoxList,点击"定价"按钮
点击"定价"按钮,如果每组的CheckBoxList中没有一项被选中,会在属性行后面出现错误提示。在"产品SKU与定价"区域不会出现内容。
※ 点击类别项,在"产品属性"区域包括CheckBoxList,点击"定价"按钮,再点击CheckBoxList选项,某些错误提示消失
点击CheckBoxList中的某项,该属性行后面的错误提示消失。在"产品SKU与定价"区域还是不会出现内容。
※ 点击类别项,在"产品属性"区域包括CheckBoxList,如果所有的CheckBoxList至少有一项被选中,点击"定价"按钮
○ 会把所有的选中属性值进行笛卡尔乘积显示到"产品SKU与定价"区域
○ 出现"提交"按钮
○ 如果有关价格的input验证不通过会出现异步验证错误信息
○ 与有关价格的input一起渲染的还有一个隐藏域,用来存放该SKU项的属性值Id,以便和价格一起被保存到数据库
※ 点击类别项,在"产品属性"区域不包括CheckBoxList
当选择类别中的"家具"项,在"产品属性"区域中的属性值只是以Select来呈现。
※ 点击类别项,在"产品属性"区域不包括CheckBoxList,点击"定价"按钮
如果"产品属性"区域中只有Select元素,点击"定价"按钮,在"产品SKU与定价"区域只出现有关价格的input,并且带异步验证,同时还出现提交按钮。
※ 在控制器提交产品的方法中打断点,点击"提交"按钮
在界面提交的包括:
在控制器方法中收到了所有的提交:
领域模型和视图模型
有关产品类别的领域模型:
monospace; width: 100%; margin: 0em; background-color: #f0f0f0"> public class Category{public int Id { get; set; }public string Name { get; set; }}
有关属性的领域模型:
public class Prop{public int Id { get; set; }public string Name { get; set; }public int CategoryId { get; set; }public short InputType { get; set; }public Category Category { get; set; }}
以上,InputType
属性对应InputTypeEnum
的枚举项,会依据此属性加载不同的视图(Select或CheckBoxList)。
public enum InputTypeEnum{//下拉选框
PropDropDownList = 0,//复选框
PropCheckBoxList = 1}
有关属性值的领域模型:
public class PropOption{public int Id { get; set; }public string RealValue { get; set; }public int PropId { get; set; }public Prop Prop { get; set; }}
在产品提交页,和产品有关包括:产品类别、产品本身的描述、属性及属性值(属性值有些以Select显示,有些以CheckBoxList显示)、属性值和价格的SKU组合项。提炼出有关产品的一个视图模型:
public class ProductVm{public ProductVm()
{this.PropOptionDs = new List<PropOptionVmD>();this.ProductSKUs = new List<ProductSKUVm>();this.PropOptionCs = new List<PropOptionVmC>();}public int Id { get; set; }[Required(ErrorMessage = "必填")]
public int CategoryId { get; set; }[Required(ErrorMessage = "必填")]
[Display(Name = "产品编号")]
[MaxLength(10, ErrorMessage = "最大长度10")]
public string Code { get; set; }[Required(ErrorMessage = "必填")]
[Display(Name = "产品名称")]
[MaxLength(10, ErrorMessage = "最大长度10")]
public string Name { get; set; }public List<PropOptionVmD> PropOptionDs { get; set; }public List<PropOptionVmC> PropOptionCs { get; set; }public List<ProductSKUVm> ProductSKUs { get; set; }}
以上,
○ PropOptionDs
表示以Select显示属性值的、有关属性和属性值的集合
○ PropOptionCs
表示以CheckBoxList显示属性值的、有关属性和属性值的集合
○ ProductSKUs
表示SKU项的集合
PropOptionVmD
视图模型用来显示每一个属性名,该属性下的属性值是以Select呈现:
public class PropOptionVmD{public int Id { get; set; }public int PropId { get; set; }public string PropName { get; set; }[Required(ErrorMessage = "必填")]
public int PropOptionId { get; set; }}
以上,
○ PropId
用来表示属性Id,在界面中是以隐藏域存在的,会被传给服务端
○ PropName
表示属性名,在界面中显示属性的名称
○ PropOptionId
表示界面中被选中的属性值Id
PropOptionVmC
视图模型也用来显示每一个属性名,该属性下的属性值以CheckBoxList呈现:
public class PropOptionVmC{public int Id { get; set; }public int PropId { get; set; }public string PropName { get; set; }public string PropOptionIds { get; set; }}
ProductSKUVm
视图模型用来显示SKU项中的价格部分:
public class ProductSKUVm{[Display(Name = "价格")]
[Required(ErrorMessage = "必填")]
[Range(typeof(Decimal), "0", "9999", ErrorMessage = "{0} 必须是数字介于 {1} 和 {2}之间.")]public decimal Price { get; set; }public string OptionIds { get; set; }}
以上,
○ Price
用来显示SKU项中的价格
○ OptionIds
用来存放SKU项中的所有属性值编号,以逗号隔开,在界面中以隐藏域存在
控制器和视图实现
□ HomeController
当呈现Home/Index.cshtml视图的时候,HomeController应该提供一个方法,把所有的类别放在SelectListItem集合中传给前台,并返回一个有关产品视图模型强类型视图。
当在界面上点击类别选项,HomeController应该有一个方法接收类别的Id,把该类别下所有的属性Id以Json格式返回给前台。
当在界面上接收到一个属性Id集合,需要遍历属性Id集合,把每个属性Id传给控制器,HomeController应该有一个方法接收属性Id,在方法内部根据InputType来决定显示带Select的视图,还是带CheckBoxList的视图。
当点击界面上的"定价"按钮,可能需要对属性值进行笛卡尔乘积,可能不需要,因此,HomeController应该提供2个方法,一个方法用来渲染出需要笛卡尔乘积的视图,另一个方法用来渲染不需要笛卡尔乘积的视图。
当点击界面上的"提交"按钮,HomeController应该提供一个提交产品的方法,该方法接收的参数是有关产品的视图模型。
public class HomeController : Controller{public ActionResult Index()
{//把类别封装成SelectListItem集合传递到前台
var categories = Database.GetCategories();var result = from c in categories
select new SelectListItem() {Text = c.Name, Value = c.Id.ToString()};
ViewData["categories"] = result;return View(new ProductVm());}//添加产品
[HttpPost]public ActionResult AddProduct(ProductVm productVm)
{if (ModelState.IsValid)
{//TODO:各种保存
return Json(new { msg = true });}else
{//把类别封装成SelectListItem集合传递到前台
var categories = Database.GetCategories();var result = from c in categories
select new SelectListItem() { Text = c.Name, Value = c.Id.ToString() };
ViewData["categories"] = result;
return RedirectToAction("Index", productVm);}}//根据分类返回分类下的所有属性Id
[HttpPost]public ActionResult GetPropIdsByCategoryId(int categoryId){var props = Database.GetPropsByCategoryId(categoryId);List<int> propIds = props.Select(p => p.Id).ToList();
return Json(propIds);
}//显示属性和属性项的部分视图
public ActionResult AddPropOption(int propId){var prop = Database.GetProps().Where(p => p.Id == propId).FirstOrDefault();var propOptions = Database.GetPropOptionsByPropId(propId);if (prop.InputType == (short) InputTypeEnum.PropDropDownList){PropOptionVmD propOptionVmD = new PropOptionVmD();
propOptionVmD.PropId = propId;propOptionVmD.PropName = prop.Name;ViewData["propOptionsD"] = from p in propOptionsselect new SelectListItem() { Text = p.RealValue, Value = p.Id.ToString() };
return PartialView("_AddPropOptionD", propOptionVmD);}else
{PropOptionVmC propOptionVmC = new PropOptionVmC();
propOptionVmC.PropId = propId;propOptionVmC.PropName = prop.Name;ViewData["propOptionsC"] = from p in propOptionsselect new SelectListItem() {Text = p.RealValue, Value = p.Id.ToString()};
return PartialView("_AddPropOptionC", propOptionVmC);}}//当在前台界面上勾选CheckBoxList选项,点击"定价"按钮,就把PropAndOption集合传到这里
[HttpPost]public ActionResult DisplaySKUs(List<PropAndOption> propAndOptions)
{try
{//属性值分组
var groupValues = (from v in propAndOptionsgroup v by v.PropIdinto grpselect grp.Select(t => Database.GetOptionValueById(t.PropOptionId))).ToList();//属性值Id分组
var groupIds = (from i in propAndOptions
group i by i.PropIdinto grepselect grep.Select(t => t.PropOptionId.ToString())).ToList();//属性值分组后进行笛卡尔乘积
IEnumerable<string> values;
values = groupValues.First();groupValues.RemoveAt(0);groupValues.ForEach(delegate(IEnumerable<string> ele){values = (from v in values
from e in ele
select v + " " + e).ToList();
});//属性值Id分组后进行笛卡尔乘积
IEnumerable<string> ids;
ids = groupIds.First();groupIds.RemoveAt(0);groupIds.ForEach(delegate(IEnumerable<string> ele){ids = (from i in ids
from e in ele
select i + "," + e).ToList();
});//把笛卡尔积后的集合传递给前台
ViewData["v"] = values;
ViewData["i"] = ids;
}catch (Exception)
{throw;
}return PartialView("_ShowSKUs");}//不涉及属性值的笛卡尔乘积
public ActionResult ShowSKUsWithoutCombination()
{ViewData["v"] = null;ViewData["i"] = null;return PartialView("_ShowSKUs");}}
□ Home/Index.cshtml视图
当初次显示界面的时候,需要把"提交"按钮隐藏,把"定价"按钮显示。
当点击类别下拉框的时候:
1、清空属性区域
2、清空SKU区域
3、隐藏"定价"按钮,显示"提交"按钮
4、把类别Id异步传给控制器
5、遍历从控制器异步传回的属性Id的集合,把属性Id传给控制器,发送异步请求,返回有关产品属性和属性值的强类型部分视图,并追加到界面"产品属性"区域
当点击"定价"按钮:
1、可能"产品属性"区域有CheckBoxList
1.1 判断每组CheckBoxList必须至少有一被勾选
1.2 遍历每个属性行,遍历每个被勾选的项,组成类似{ propId: pId, propOptionId: oId }的数组
1.3 把{ propId: pId, propOptionId: oId }的数组以json格式传给控制器
1.4 异步返回的部分视图追加到界面的"产品SKU与定价"区域,并给动态加载内容实施异步验证
2、可能"产品属性"区域没有CheckBoxList
2.1 异步加载显示SKU组合的部分视图,只显示一个有关价格的input元素
勾选"产品属性"区域的CheckBoxList:
1、检查每组CheckBoxList是否满足条件,即至少有一项被选中
2、隐藏"定价"按钮,显示"提交"按钮
点击"产品属性"区域中,每行的"删除行"按钮,删除当前属性行。
@model MvcApplication1.Models.ProductVm@{ViewBag.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}<div id="wrapper">
@using (Html.BeginForm("AddProduct", "Home", FormMethod.Post, new { id = "addForm" })){<fieldset><legend>类别</legend><div id="categories">
@Html.DropDownListFor(m => m.CategoryId, ViewData["categories"] as IEnumerable<SelectListItem>, "==选择类别==")@Html.ValidationMessageFor(m => m.CategoryId)</div></fieldset><br /><fieldset><legend>产品描述</legend><div id="description">
@Html.LabelFor(m => m.Name)@Html.TextBoxFor(m => m.Name)@Html.ValidationMessageFor(m => m.Name)<br /><br />@Html.LabelFor(m => m.Code)@Html.TextBoxFor(m => m.Code)@Html.ValidationMessageFor(m => m.Code)</div></fieldset><br /><fieldset><legend>产品属性</legend><ul id="props">
</ul></fieldset><br /><input type="button" id="displaySKU" value="定价" /><br /><fieldset><legend>产品SKU与定价</legend><ul id="skus">
</ul></fieldset><input type="button" id="up" value="提交" />}</div>@section scripts{<script src="~/Scripts/jquery.validate.min.js"></script>
<script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>
<script src="~/Scripts/dynamicvalidation.js"></script>
<script type="text/javascript">
$(function () {//提交按钮先隐藏直到点击定价按钮再显示
showPriceHideUp();//点击类别下拉框
$('#CategoryId').change(function () {changeCategory();});//点击定价按钮显示SKU项,以表格显示,属性名称 属性名称 价格,
//定价按钮消失,提交按钮出现
//对每组CheckBoxList进行验证,保证至少有一个选项勾选
$('#displaySKU').on("click", function () {
if ($('#props').find('.c').length) { //判断属性和属性值区域有没有包含CheckBoxList的li,存在if (checkCblist()) { //如果所有CheckBoxList组都至少有一项被勾选//遍历所有的CheckBoxList的选中项,一个属性Id带着1个或多个属性项Id
var propAndOptions = [];//遍历所有包含CheckBoxList的li
$.each($('#props').find('.c'), function () {//从隐藏域中获取属性Id <input type="hidden" value="" id='h_v' class='h_v'>
var pId = $(this).find('input[type=hidden]').val();
//遍历每个li中被选中的CheckBox
$.each($(this).find("input:checked"), function () {//获取选中值
var oId = $(this).val();
propAndOptions.push({ propId: pId, propOptionId: oId });});});//异步提交PropAndOption集合
$.ajax({cache: false,
url: '@Url.Action("DisplaySKUs", "Home")',contentType: 'application/json; charset=utf-8',dataType: "html",
type: "POST",
data: JSON.stringify({ 'propAndOptions': propAndOptions }),success: function (data) {$('#skus').html(data);$.each($('.s'), function (index) {$.validator.unobtrusive.parseDynamicContent(this, "#addForm");});hidePriceShowUp();},error: function (jqXhr, textStatus, errorThrown) {alert("出错了 '" + jqXhr.status + "' (状态: '" + textStatus + "', 错误为: '" + errorThrown + "')");}});} else {
return;
}} else {//判断属性和属性值区域有没有包含CheckBoxList的li,不存在$.ajax({cache: false,
url: '@Url.Action("ShowSKUsWithoutCombination", "Home")',dataType: "html",
type: "GET",
success: function (data) {$('#skus').html(data);$.validator.unobtrusive.parseDynamicContent('.s', "#addForm");
hidePriceShowUp();},error: function (jqXhr, textStatus, errorThrown) {alert("出错了 '" + jqXhr.status + "' (状态: '" + textStatus + "', 错误为: '" + errorThrown + "')");}});}});//删除属性属性值行
$('#props').on('click', '.delRow', function() {$(this).parent().parent().remove();
});//点击任意CheckBoxList中的选项,定价按钮出现,提交按钮隐藏
$('#props').on("change", "input[type=checkbox]", function () {//验证
checkCblist();showPriceHideUp();});//点击提交
$('#up').on("click", function () {
if (checkCblist) {
if ($('#addForm').valid()) {
$.ajax({cache: false,
url: '@Url.Action("AddProduct", "Home")',type: 'POST',dataType: 'json',data: $('#addForm').serialize(),success: function (data) {if (data.msg) {
alert('提交成功');}},error: function (xhr, status) {alert("添加失败,状态码:" + status);
}});}} else {
alert("属性值必须勾选");
}});});//点击类别下拉框
function changeCategory() {//获取选中的值
var selectedValue = $('#CategoryId option:selected').val();//如果确实选中
if ($.trim(selectedValue).length > 0) {
//清空属性和属性项区域
$('#props').empty();//清空SKU区域
$('#skus').empty();showPriceHideUp();//异步请求属性和属性项
$.ajax({url: '@Url.Action("GetPropIdsByCategoryId", "Home")',data: { categoryId: selectedValue },type: 'post',cache: false,
async: false,
dataType: 'json',success: function (data) {if (data.length > 0) {
$.each(data, function (i, item) {$.get("@Url.Action("AddPropOption", "Home")", { propId: item }, function (result) {$('#props').append(result);});});}}});}}//隐藏定价按钮 显示提交按钮
function hidePriceShowUp() {//隐藏定价按钮
$('#displaySKU').css("display", "none");//显示提交按钮
$('#up').css("display", "block");}//显示定价按钮 隐藏提交按钮
function showPriceHideUp(parameters) {$('#displaySKU').css("display", "block");$('#up').css("display", "none");}//检查每组CheckBoxList,如果没有一个选中,报错
function checkCblist() {var result = false;
//遍历每组li下的checkboxlist,如果没有一个选中,报错
$('#props li').each(function () {if ($(this).find("input:checked").length == 0) {$(this).find('.err').text("至少选择一项").css("color", "red");} else {
$(this).find('.err').text("");
result = true;
}});return result;
}</script>}
以上,关于给为动态加载内容实施验证的dynamicvalidation.js
文件,详细参考这里。
//对动态生成内容客户端验证
(function ($) {$.validator.unobtrusive.parseDynamicContent = function (selector, formSelector) {$.validator.unobtrusive.parse(selector);var form = $(formSelector);var unobtrusiveValidation = form.data('unobtrusiveValidation');var validator = form.validate();$.each(unobtrusiveValidation.options.rules, function (elname, elrules) {if (validator.settings.rules[elname] == undefined) {
var args = {};$.extend(args, elrules);args.messages = unobtrusiveValidation.options.messages[elname];//edit:use quoted strings for the name selector
$("[name='" + elname + "']").rules("add", args);} else {
$.each(elrules, function (rulename, data) {if (validator.settings.rules[elname][rulename] == undefined) {
var args = {};args[rulename] = data;args.messages = unobtrusiveValidation.options.messages[elname][rulename];//edit:use quoted strings for the name selector
$("[name='" + elname + "']").rules("add", args);}});}});};})(jQuery);
以上,当点击产品类别,搜集"产品属性"区域中的勾选项,组成{ propId: pId, propOptionId: oId }
数组的时候,这里的propId
和propOptionId
键必须和PropAndOption
中的属性吻合,因为在控制器方法中,接收的是List
类型。
public class PropAndOption{public int PropId { get; set; }public int PropOptionId { get; set; }}
□ _AddPropOptionD.cshtml部分视图
当点击界面上的类别选项,相应属性下的属性值以Select显示,即单选,就来加载这里的视图,并呈现到界面中的"产品属性"区域。
@using MvcApplication1.Extensions
@model MvcApplication1.Models.PropOptionVmD@using (Html.BeginCollectionItem("PropOptionDs")){<li><span>@Model.PropName:</span><span>@Html.DropDownListFor(m => m.PropOptionId, ViewData["propOptionsD"] as IEnumerable<SelectListItem>)</span><span>@Html.ValidationMessageFor(m => m.PropOptionId)</span><span>@Html.HiddenFor(m => m.PropId)</span><span><a href="javascript:void(0)" class="delRow">删除行</a></span></li>}
其中,Html.BeginCollectionItem("PropOptionDs")
根据导航属性生成满足批量上传条件的表单元素,详细介绍在这里。
public static class CollectionEditingHtmlExtensions{//目标生成如下格式
//<input autocomplete="off" name="FavouriteMovies.Index" type="hidden" value="6d85a95b-1dee-4175-bfae-73fad6a3763b" />
//<label>Title</label>
//<input class="text-box single-line" name="FavouriteMovies[6d85a95b-1dee-4175-bfae-73fad6a3763b].Title" type="text" value="Movie 1" />
//<span class="field-validation-valid"></span>
public static IDisposable BeginCollectionItem<TModel>(this HtmlHelper<TModel> html, string collectionName){//构建name="FavouriteMovies.Index"
string collectionIndexFieldName = string.Format("{0}.Index", collectionName);//构建Guid字符串
string itemIndex = GetCollectionItemIndex(collectionIndexFieldName);
//构建带上集合属性+Guid字符串的前缀
string collectionItemName = string.Format("{0}[{1}]", collectionName, itemIndex);TagBuilder indexField = new TagBuilder("input");indexField.MergeAttributes(new Dictionary<string, string>(){{"name", string.Format("{0}.Index", collectionName)},{"value", itemIndex},
{"type", "hidden"},{"autocomplete", "off"}});html.ViewContext.Writer.WriteLine(indexField.ToString(TagRenderMode.SelfClosing));return new CollectionItemNamePrefixScope(html.ViewData.TemplateInfo, collectionItemName);}private class CollectionItemNamePrefixScope : IDisposable{private readonly TemplateInfo _templateInfo;private readonly string _previousPrfix;//通过构造函数,先把TemplateInfo以及TemplateInfo.HtmlFieldPrefix赋值给私有字段变量,并把集合属性名称赋值给TemplateInfo.HtmlFieldPrefix
public CollectionItemNamePrefixScope(TemplateInfo templateInfo, string collectionItemName){this._templateInfo = templateInfo;
this._previousPrfix = templateInfo.HtmlFieldPrefix;
templateInfo.HtmlFieldPrefix = collectionItemName;}public void Dispose(){_templateInfo.HtmlFieldPrefix = _previousPrfix;}}/// <summary>
///
/// </summary>
/// <param name="collectionIndexFieldName">比如,FavouriteMovies.Index</param>
/// <returns>Guid字符串</returns>
private static string GetCollectionItemIndex(string collectionIndexFieldName){Queue<string> previousIndices = (Queue<string>)HttpContext.Current.Items[collectionIndexFieldName];if (previousIndices == null){HttpContext.Current.Items[collectionIndexFieldName] = previousIndices = new Queue<string>();string previousIndicesValues = HttpContext.Current.Request[collectionIndexFieldName];
if (!string.IsNullOrWhiteSpace(previousIndicesValues)){foreach (string index in previousIndicesValues.Split(',')){previousIndices.Enqueue(index);}}}return previousIndices.Count > 0 ? previousIndices.Dequeue() : Guid.NewGuid().ToString();
}}
□ _AddPropOptionC.cshtml部分视图
当点击界面上的类别选项,相应属性下的属性值以CheckBoxList显示,即多选,就来加载这里的视图,并呈现到界面中的"产品属性"区域。
@using MvcApplication1.Extensions
@model MvcApplication1.Models.PropOptionVmC@using (Html.BeginCollectionItem("PropOptionCs")){<li class="c"><span>@Model.PropName:</span><span>@Html.CheckBoxList("PropOptionIds",ViewData["propOptionsC"] as IEnumerable<SelectListItem>,null, 10)<span class="err"></span></span><span>@Html.HiddenFor(m => m.PropId)</span><span><a href="javascript:void(0)" class="delRow">删除行</a></span></li>}
其中,CheckBoxList是基于HtmlHelper
的扩展方法,用来呈现水平或垂直分布的CheckBoxList,详细介绍在这里。
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace System.Web.Mvc
{public static class InputExtensions{#region 水平方向CheckBoxList/// <summary>
/// 生成水平方向的CheckBoxList
/// </summary>
/// <param name="htmlHelper"></param>
/// <param name="name">name属性值</param>
/// <param name="htmlAttributes">属性和属性值的键值对集合</param>
/// <param name="number">每行显示的个数</param>
/// <returns></returns>
public static MvcHtmlString CheckBoxList(this HtmlHelper htmlHelper,string name,
IEnumerable<SelectListItem> listInfo,IDictionary<string, object> htmlAttributes,int number)
{//name属性值必须有
if (string.IsNullOrEmpty(name)){throw new ArgumentException("必须给CheckBoxList一个name值", "name");}//数据源SelectListItem的集合必须有
if (listInfo == null){throw new ArgumentNullException("listInfo", "List<SelectListItem>类型的listInfo参数不能为null");}//数据源中必须有数据
if (!listInfo.Any())
{throw new ArgumentException("List<SelectListItem>类型的listInfo参数必须有数据", "listInfo");}//准备拼接
var sb = new StringBuilder();
//每行CheckBox开始数数
var lineNumber = 0;//遍历数据源
foreach (var info in listInfo){lineNumber++;//创建type=checkbox的input
var builder = new TagBuilder("input");//tag设置属性
if (info.Selected)
{builder.MergeAttribute("checked", "checked");}builder.MergeAttributes(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));//创建checkbox的显示值
var lableBuilder = new TagBuilder("label");lableBuilder.MergeAttribute("for", string.Format("{0}_{1}", name, info.Value));lableBuilder.InnerHtml = info.Text;sb.Append(lableBuilder.ToString(TagRenderMode.Normal));//如果设置的每行数量刚好被当前数量整除就换行
if (lineNumber == 0 || (lineNumber % number == 0))
{sb.Append("<br />");
}}return MvcHtmlString.Create(sb.ToString());
}/// <summary>
/// 重载,不包含属性和属性值键值对的集合
/// </summary>
/// <param name="htmlHelper"></param>
/// <param name="name">name的属性值</param>
/// <param name="listInfor">SelectListItem集合类型的数据源</param>
/// <returns></returns>
public static MvcHtmlString CheckBoxList(this HtmlHelper htmlHelper,string name,
IEnumerable<SelectListItem> listInfor){return htmlHelper.CheckBoxList(name, listInfor, null, 5);}#endregion#region 垂直方向CheckBoxListpublic static MvcHtmlString CheckBoxListVertical(this HtmlHelper htmlHelper,string name,
IEnumerable<SelectListItem> listInfo,IDictionary<string, object> htmlAttributes,int columnNumber = 1)
{//name属性值不能为null
if (string.IsNullOrEmpty(name)){throw new ArgumentException("必须给CheckBoxList的name属性赋值","name");}//数据源不能为null
if (listInfo == null){throw new ArgumentNullException("listInfo","List<SelectListItem>类型的listInfo参数不能为null");}//数据源中必须有数据
if (!listInfo.Any())
{throw new ArgumentException("List<SelectListItem>类型的参数listInfo必须有数据","listInfo");}//数据源数据项的数量
var dataCount = listInfo.Count();//得到行数
var rows = Convert.ToInt32(Math.Ceiling(Convert.ToDecimal(dataCount) / Convert.ToDecimal(columnNumber)));//创建div
var wrapBuilder = new TagBuilder("div");wrapBuilder.MergeAttribute("style", "float:left; line-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 listInfo){var builder = new TagBuilder("input");if (info.Selected)
{builder.MergeAttribute("checked", "checked");}builder.MergeAttributes(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}}
□ _ShowSKUs.cshtml部分视图
当点击界面上的"定价"按钮,就来加载这个视图,如果属性值笛卡尔乘积为null,那就只显示一个有关价格的input元素。如果确实存在属性值笛卡尔乘积,就遍历这些SKU项显示出来,并且,每遍历一次,就去加载有关产品价格的强类型部分视图。
@if (ViewData["v"] == null){<li><span class="s">@{ProductSKUVm productSkuVm = new ProductSKUVm();
productSkuVm.OptionIds = "";Html.RenderPartial("_SKUDetail", productSkuVm);
}</span></li>}else
{string[] values = (ViewData["v"] as IEnumerable<string>).ToArray();string[] ids = (ViewData["i"] as IEnumerable<string>).ToArray();for (int i = 0; i < values.Count(); i++){<li><span>@values[@i]</span><span class="s">@{ProductSKUVm productSkuVm = new ProductSKUVm();
productSkuVm.OptionIds = ids[@i];Html.RenderPartial("_SKUDetail", productSkuVm);
}</span></li>}}
□ _SKUDetail.cshtml强类型部分视图
作为_ShowSKUs.cshtml部分视图的子部分视图,用来显示产品价格相关的强类型部分视图。
@using MvcApplication1.Extensions
@model MvcApplication1.Models.ProductSKUVm@using (Html.BeginCollectionItem("ProductSKUs")){@Html.TextBoxFor(m => m.Price)@Html.ValidationMessageFor(m => m.Price)@Html.HiddenFor(m => m.OptionIds)}
□ 模拟数据库存储的Database类
logs_code_Collapse">展开
结束!