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并水平排列

本篇主要包括:

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

商品模块小样简介

※ 界面

○ 类别区域,用来显示产品类别,点击选择某个类别,在"产品属性"区域出现该类别下的所有属性,以及属性值,对于单选的属性值用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,并且带异步验证,同时还出现提交按钮。

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

在界面提交的包括:

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

领域模型和视图模型

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

    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 propOptions
                                           select 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 propOptions
                    select 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 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="~/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 }数组的时候,这里的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="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 垂直方向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类

展开    public class Database
    {
        #region 关于分类
        public static List<Category> GetCategories()
        {
            return new List<Category>()
            {
                new Category(){Id = 1, Name = "家电"},
                new Category(){Id = 2, Name = "家具"}
            };
        }
        #endregion

        #region 关于属性
        public static List<Prop> GetProps()
        {
            var category1 = GetCategories().Where(c => c.Id == 1).FirstOrDefault();
            var category2 = GetCategories().Where(c => c.Id == 2).FirstOrDefault();
            return new List<Prop>()
            {
                new Prop(){Id = 1, Name = "重量",InputType = (short)InputTypeEnum.PropDropDownList,CategoryId = category1.Id, Category = category1},
                new Prop(){Id = 2, Name = "尺寸",InputType = (short)InputTypeEnum.PropCheckBoxList,CategoryId = category1.Id, Category = category1},
                new Prop(){Id = 3, Name = "颜色",InputType = (short)InputTypeEnum.PropCheckBoxList,CategoryId = category1.Id, Category = category1},
                new Prop(){Id = 4, Name = "成份",InputType = (short)InputTypeEnum.PropDropDownList,CategoryId = category2.Id, Category = category2}
            };

        }

        public static List<Prop> GetPropsByCategoryId(int categoryId)
        {
            return GetProps().Where(p => p.CategoryId == categoryId).ToList();
        }
        #endregion

        #region 关于属性值
        public static List<PropOption> GetPropOptions()
        {
            var prop1 = GetProps().Where(p => p.Id == 1).FirstOrDefault(); //重量
            var prop2 = GetProps().Where(p => p.Id == 2).FirstOrDefault(); //尺寸
            var prop3 = GetProps().Where(p => p.Id == 3).FirstOrDefault(); //颜色
            var prop4 = GetProps().Where(p => p.Id == 4).FirstOrDefault(); //成份

            return new List<PropOption>()
            {
                //重量的属性值
                new PropOption(){Id = 1, Prop = prop1, PropId = prop1.Id, RealValue = "5kg"},
                new PropOption(){Id = 2, Prop = prop1, PropId = prop1.Id, RealValue = "10kg"},

                //尺寸的属性值
                new PropOption(){Id = 3, Prop = prop2, PropId = prop2.Id, RealValue = "10英寸"},
                new PropOption(){Id = 4, Prop = prop2, PropId = prop2.Id, RealValue = "12英寸"},
                new PropOption(){Id = 5, Prop = prop2, PropId = prop2.Id, RealValue = "15英寸"},

                //颜色的属性值
                new PropOption(){Id = 6, Prop = prop3, PropId = prop3.Id, RealValue = "玫红色"},
                new PropOption(){Id = 7, Prop = prop3, PropId = prop3.Id, RealValue = "圣诞白"},
                new PropOption(){Id = 8, Prop = prop3, PropId = prop3.Id, RealValue = "宇宙光"},
                new PropOption(){Id = 9, Prop = prop3, PropId = prop3.Id, RealValue = "绚烂橙"},

                //成份的属性值
                new PropOption(){Id = 10, Prop = prop4, PropId = prop4.Id, RealValue = "实木"},
                new PropOption(){Id = 11, Prop = prop4, PropId = prop4.Id, RealValue = "橡木"},
            };
        }

        //根据属性Id获取所有属性值
        public static List<PropOption> GetPropOptionsByPropId(int propId)
        {
            return GetPropOptions().Where(p => p.PropId == propId).ToList();
        }

        //根据属性值Id获取属性值
        public static string GetOptionValueById(int optionId)
        {
            return (GetPropOptions().Where(p => p.Id == optionId).FirstOrDefault()).RealValue;
        }
        #endregion
    }

结束!

时间: 2024-12-18 09:46:38

ASP.NET MVC中商品模块小样的相关文章

ASP.NET MVC中实现属性和属性值的组合,即笛卡尔乘积02, 在界面实现

在"ASP.NET MVC中实现属性和属性值的组合,即笛卡尔乘积01, 在控制台实现"中,在控制台应用程序中实现了属性值的笛卡尔乘积.本篇在界面中实现.需要实现的大致如下: 在界面中勾选CheckBoxList中属性值选项: 把勾选的属性值进行笛卡尔乘积,每行需要填写价格: 我们暂且不考虑这些CheckBoxList是如何显示出来的(在后续有关商品模块的文章中会实现),还需考虑的方面包括: 1.从CheckBoxList中获取到什么再往控制器传? 对于每行的CheckBoxList来说

angular.js的路由和模板在asp.net mvc 中的使用

我们知道angular.js是基于mvc 的一款优秀js框架,它也有一套自己的路由机制,和asp.net mvc 路由不太一样.asp.net mvc 的路由是通过不同的URL到不同的controller然后交给controller去呈现视图.但是在angular.js则是需要提前指定一个module(ng-app),然后去定义路由规则,通过不同的URL,来告诉ng-app 去加载哪个页面.再渲染到ng-view.通过angular.js路由的使用,可以很容易实现页面的局部刷新.更加高效的去创建

ASP.NET MVC中注册Global.asax的Application_Error事件处理全局异常

在ASP.NET MVC中,通过应用程序生命周期中的Application_Error事件可以捕获到网站引发的所有未处理异常.本文作为学习笔记,记录了使用Global.asax文件的Application_Error事件处理和捕获全局异常的详细步骤. 文章演示项目是使用vs2013编译器编写的,下载地址:GlobalExceptionHandle-By-Application_Error.zip. 在VS2013中新建一个MVC项目,这里要先关闭自定义错误,将Web.config配置文件中cus

如何在 ASP.NET MVC 中集成 AngularJS(2)

在如何在 ASP.NET MVC 中集成 AngularJS(1)中,我们介绍了 ASP.NET MVC 捆绑和压缩.应用程序版本自动刷新和工程构建等内容. 下面介绍如何在 ASP.NET MVC 中集成 AngularJS 的第二部分. ASP.NET 捆绑和压缩 CSS 和 JavaScript 的捆绑与压缩功能是 ASP.NET MVC 最流行和有效的特性之一.捆绑和压缩降低了 HTTP 请求和有效载荷的大小,结果是可以更快和更好的执行 ASP.NET MVC 的网站.有许多可以减少 CS

ASP.NET MVC中分析淘宝网页发生乱码标题搞定方法

ASP.NET MVC中分析淘宝网页发生乱码标题搞定方法 近来正在分析淘宝中商品的信息,效果发生乱码,如: 原因便是中文字符格式发生冲突,ASP.NET MVC 默认采用utf-8,可是淘宝网页采用gbk.正在网上找了一下,最常常的搞定方法便是修改web.config:< system.web> ...... < globalization requestEncoding="gbk" responseEncoding="gbk" culture=&

ASP.NET MVC中实现属性和属性值的组合,即笛卡尔乘积01, 在控制台实现

在电商产品模块中必经的一个环节是:当选择某一个产品类别,动态生成该类别下的所有属性和属性项,这些属性项有些是以DropDownList的形式存在,有些是以CheckBoxList的形式存在.接着,把CheckBoxList的选中项组合生成产品SKU项. 本系列将在ASP.NET MVC中实现以上功能.但本篇,先在控制台实现属性值的笛卡尔乘积. 关于属性的类: public class Prop { public int Id { get; set; } public string Name {

ASP.NET MVC中的错误处理

ASP.NET MVC中的错误的错误处理跨越了两个主要领域:程序异常和路由异常的处理.前者是关于在控制器和视图中捕获错误的;而后者更多是有关重定向和HTTP错误的. 1.在WebConfig中把过滤器配置启动 <customErrors mode="On"> </customErrors> 控制器的代码报错时,会跳转到~/Views/Shared/Error.cshtml页面.mode="Off"页面不会跳转直接显示错误信息. 2.绑定异常过

log4net 使用总结- (2)在ASP.NET MVC 中使用

log4net在ASP.NET MVC中的配置,还有一种配置方式,即不在web.config中,而是单独新建一个log4net.config 在根目录下 第一.引用log4net.dll 第二.在站点根目录下增加log4net.config <?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="

ASP.NET MVC中使用异步控制器

线程池 一直想把项目改写成异步,但是ASP.NETMVC3下写的过于繁琐,.NET 4.5与ASP.NET MVC下代码写起来就比较简单了, MS好像也一直喜欢这样搞,每一个成熟的东西,都要演变好几个版本,才能趋于规范. ASP.NET MVC 中为什么需要使用异步呢,IIS有一个线程池来处理用户的请求,当一个新的请求过来时,将调度池中的线程以处理该请求,然而,但并发量很高的情况下,池中的线程已经不能够满足这么多的请求时候,池中的每一个线程都处于忙的状态则在处理请求时将阻塞处理请求的线程,并且该