首页 > 代码库 > ASP.NET MVC中商品模块小样

ASP.NET MVC中商品模块小样

在前面的几篇文章中,已经在控制台和界面实现了属性值的笛卡尔乘积,这是商品模块中的一个难点。本篇就来实现在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并水平排列  

 

本篇主要包括:

□ 商品模块小样简介
□ 领域模型和视图模型
□ 控制器和视图实现

 

商品模块小样简介

 

※ 界面

1

○ 类别区域,用来显示产品类别,点击选择某个类别,在"产品属性"区域出现该类别下的所有属性,以及属性值,对于单选的属性值用Select显示,对于多选的属性值用CheckBoxList显示。
○ 产品描述,表示数据库中产品表中的字段,当然实际情况中,这里的字段更多,比如上传时间,是否通过,产品卖点,等等。
○ 产品属性,只有点击选择产品类别,这里才会显示
○ 定价按钮,点击这个按钮,如果"产品属性"区域中有CheckBoxList项,"产品SKU与定价"区域会出现关于属性值、产品价格的SKU组合项;如果"产品属性"区域中没有CheckBoxList项,"产品SKU与定价"区域只出现一个有关价格的input元素。另外,每次点击定价按钮,出现提交按钮,定价按钮隐藏。
○ 产品SKU与定价:这里要么呈现属性值、价格的SKU项,要么只出现一个有关价格的input元素

 

※ 点击类别项,在"产品属性"区域包括CheckBoxList

2

○ 点击类名中的"家电"选项,在"产品属性"区域中出现属性及其值,有些属性值以Select呈现,有些属性值以CheckBoxList呈现
○ 点击属性行后面的"删除行"直接删除属性行

 

※ 点击类别项,在"产品属性"区域包括CheckBoxList,点击"定价"按钮

3

点击"定价"按钮,如果每组的CheckBoxList中没有一项被选中,会在属性行后面出现错误提示。在"产品SKU与定价"区域不会出现内容。

 

※ 点击类别项,在"产品属性"区域包括CheckBoxList,点击"定价"按钮,再点击CheckBoxList选项,某些错误提示消失

4

点击CheckBoxList中的某项,该属性行后面的错误提示消失。在"产品SKU与定价"区域还是不会出现内容。

 

※ 点击类别项,在"产品属性"区域包括CheckBoxList,如果所有的CheckBoxList至少有一项被选中,点击"定价"按钮

5

 

○ 会把所有的选中属性值进行笛卡尔乘积显示到"产品SKU与定价"区域
○ 出现"提交"按钮
○ 如果有关价格的input验证不通过会出现异步验证错误信息
○ 与有关价格的input一起渲染的还有一个隐藏域,用来存放该SKU项的属性值Id,以便和价格一起被保存到数据库

 

6

 

※ 点击类别项,在"产品属性"区域不包括CheckBoxList

7

当选择类别中的"家具"项,在"产品属性"区域中的属性值只是以Select来呈现。

 

※ 点击类别项,在"产品属性"区域不包括CheckBoxList,点击"定价"按钮

8

如果"产品属性"区域中只有Select元素,点击"定价"按钮,在"产品SKU与定价"区域只出现有关价格的input,并且带异步验证,同时还出现提交按钮。

 

※ 在控制器提交产品的方法中打断点,点击"提交"按钮

在界面提交的包括:

9

 

在控制器方法中收到了所有的提交:

10

 

 

领域模型和视图模型

 

有关产品类别的领域模型:

    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 = http://www.mamicode.com/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 = http://www.mamicode.com/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 propOptions
                                           select new SelectListItem() { Text = p.RealValue, Value = http://www.mamicode.com/p.Id.ToString() };>
                return PartialView("_AddPropOptionD", propOptionVmD);
            } 
            else
            {
                PropOptionVmC  propOptionVmC = new PropOptionVmC();
                propOptionVmC.PropId = propId;
                propOptionVmC.PropName = prop.Name;
                ViewData["propOptionsC"] = from p in propOptions
                    select new SelectListItem() {Text = p.RealValue, Value = http://www.mamicode.com/p.Id.ToString()};>
                return PartialView("_AddPropOptionC", propOptionVmC);
            }          
        }
        //当在前台界面上勾选CheckBoxList选项,点击"定价"按钮,就把PropAndOption集合传到这里
        [HttpPost]
        public ActionResult DisplaySKUs(List<PropAndOption> propAndOptions)
        {
            try
            {
                //属性值分组
                var groupValues = (from v in propAndOptions
                                   group v by v.PropId
                                       into grp
                                       select grp.Select(t => Database.GetOptionValueById(t.PropOptionId))).ToList();
                //属性值Id分组
                var groupIds = (from i in propAndOptions
                                group i by i.PropId
                                    into grep
                                    select 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="http://www.mamicode.com/~/Scripts/jquery.validate.min.js"></script>
    <script src="http://www.mamicode.com/~/Scripts/jquery.validate.unobtrusive.min.js"></script>
    <script src="http://www.mamicode.com/~/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 = http://www.mamicode.com/$(‘#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 }数组的时候,这里的propIdpropOptionId键必须和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="http://www.mamicode.com/javascript:void(0)" class="delRow">删除行</a>
        </span>
    </li>
}

 

其中,Html.BeginCollectionItem("PropOptionDs")根据导航属性生成满足批量上传条件的表单元素,详细介绍在这里

 

    public static class CollectionEditingHtmlExtensions
    {
        //目标生成如下格式
        //<input autocomplete="off" name="FavouriteMovies.Index" type="hidden" value="http://www.mamicode.com/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="http://www.mamicode.com/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="http://www.mamicode.com/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 垂直方向CheckBoxList
        public 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类

 

展开

 

结束!

 

 

 

 

 

 

 

 

 

 

 

ASP.NET MVC中商品模块小样