文章目录
概述
NopCommerce源码架构详解-产品列表相关源码分析3。
内容
我在前两篇给大家分析了nop产品列表Controller相关的源码,今天我们来看看Nop产品列表mvc视图相关的源码。
首先,我们来看看视图CategoryTemplate.ProductsInGridOrLines.cshtml:
@model CategoryModel @{ Layout = "~/Views/Shared/_ColumnsTwo.cshtml"; Html.AddTitleParts(!String.IsNullOrEmpty(Model.MetaTitle) ? Model.MetaTitle : Model.Name); Html.AddMetaDescriptionParts(Model.MetaDescription); Html.AddMetaKeywordParts(Model.MetaKeywords); var canonicalUrlsEnabled = EngineContext.Current.Resolve<seoSettings>().CanonicalUrlsEnabled; if (canonicalUrlsEnabled) { var categoryUrl = Url.RouteUrl("Category", new { SeName = Model.SeName }, this.Request.Url.Scheme); Html.AddCanonicalUrlParts(categoryUrl); } var breadcrumbDelimiter = EngineContext.Current.Resolve<CommonSettings>().BreadcrumbDelimiter; } @using Nop.Core.Domain.Common; @using Nop.Core.Domain.Seo; @using Nop.Core.Infrastructure; @using Nop.Web.Models.Catalog; @using Nop.Web.Extensions; @*category breadcrumb*@ @if (Model.DisplayCategoryBreadcrumb) { <div class="breadcrumb"> <ul> <li><a href="@Url.RouteUrl("HomePage")" title="@T("Categories.Breadcrumb.Top")">@T("Categories.Breadcrumb.Top")</a> <span class="delimiter">@breadcrumbDelimiter</span> </li> @foreach (var cat in Model.CategoryBreadcrumb) { var isLastCategory = cat.Id == Model.Id; <li> @if (isLastCategory) { <strong class="current-item">@cat.Name</strong> } else { <a href="@Url.RouteUrl("Category", new { SeName = cat.SeName })" title="@cat.Name">@cat.Name</a> <span class="delimiter">@breadcrumbDelimiter</span> } </li> } </ul> </div> @Html.Widget("categorydetails_after_breadcrumb", Model.Id) } <div class="page category-page"> <div class="page-title"> <h1>@Model.Name</h1> </div> <div class="page-body"> @Html.Widget("categorydetails_top", Model.Id) @*description*@ @if (!String.IsNullOrWhiteSpace(Model.Description)) { <div class="category-description"> @Html.Raw(Model.Description) </div> } @Html.Widget("categorydetails_before_subcategories", Model.Id) @*subcategories*@ @if (Model.SubCategories.Count > 0) { <div class="sub-category-grid"> @foreach (var item in Model.SubCategories) { <div class="item-box"> <div class="sub-category-item"> <h2 class="title"> <a href="@Url.RouteUrl("Category", new { SeName = item.SeName })" title="@item.PictureModel.Title"> @item.Name</a> </h2> <div class="picture"> <a href="@Url.RouteUrl("Category", new { SeName = item.SeName })" title="@item.PictureModel.Title"> <img alt="@item.PictureModel.AlternateText" src="@item.PictureModel.ImageUrl" title="@item.PictureModel.Title" /></a> </div> </div> </div> } </div> } @Html.Widget("categorydetails_before_featured_products", Model.Id) @*featured products*@ @if (Model.FeaturedProducts.Count > 0) { <div class="product-grid featured-product-grid"> <div class="title"> <strong>@T("Products.FeaturedProducts")</strong> </div> <div> @foreach (var item in Model.FeaturedProducts) { <div class="item-box"> @Html.Partial("_ProductBox", item) </div> } </div> </div> } @Html.Widget("categorydetails_after_featured_products", Model.Id) <div class="product-selectors"> @*view mode*@ @if (Model.PagingFilteringContext.AllowProductViewModeChanging && Model.Products.Count > 0) { <div class="product-viewmode"> <span>@T("Catalog.ViewMode")</span> @Html.DropDownList("products-viewmode", Model.PagingFilteringContext.AvailableViewModes, new { onchange = "setLocation(this.value);" }) </div> } @*sorting*@ @if (Model.PagingFilteringContext.AllowProductSorting && Model.Products.Count > 0) { <div class="product-sorting"> <span>@T("Catalog.OrderBy")</span> @Html.DropDownList("products-orderby", Model.PagingFilteringContext.AvailableSortOptions, new { onchange = "setLocation(this.value);" }) </div> } @*page size*@ @if (Model.PagingFilteringContext.AllowCustomersToSelectPageSize && Model.Products.Count > 0) { <div class="product-page-size"> <span>@T("Catalog.PageSize")</span> @Html.DropDownList("products-pagesize", Model.PagingFilteringContext.PageSizeOptions, new { onchange = "setLocation(this.value);" }) <span>@T("Catalog.PageSize.PerPage")</span> </div> } </div> @Html.Widget("categorydetails_before_filters", Model.Id) <div class="product-filters-wrapper"> @*filtering*@ @if (Model.PagingFilteringContext.PriceRangeFilter.Enabled) { @Html.Partial("_FilterPriceBox", Model.PagingFilteringContext.PriceRangeFilter, new ViewDataDictionary()) } @*filtering*@ @if (Model.PagingFilteringContext.SpecificationFilter.Enabled) { @Html.Partial("_FilterSpecsBox", Model.PagingFilteringContext.SpecificationFilter, new ViewDataDictionary()) } </div> @Html.Widget("categorydetails_before_product_list", Model.Id) @*product list*@ @if (Model.Products.Count > 0) { if (Model.PagingFilteringContext.ViewMode == "list") { @*list mode*@ <div class="product-list"> @foreach (var product in Model.Products) { <div class="item-box"> @Html.Partial("_ProductBox", product) </div> } </div> } else { @*grid mode*@ <div class="product-grid"> @foreach (var product in Model.Products) { <div class="item-box"> @Html.Partial("_ProductBox", product) </div> } </div> } } <div class="pager"> @Html.Pager(Model.PagingFilteringContext).QueryParam("pagenumber") </div> @Html.Widget("categorydetails_bottom", Model.Id) </div> </div>
可以看到这个视图使用领域模模型CategoryModel来绑定,同时你也可以注意到这个视图的绑定使用了很多HtmlHelper的扩展方法。下面我们就来针对这两个方面进行分析。
1、领域模模型CategoryModel
using System.Collections.Generic; using Nop.Web.Framework.Mvc; using Nop.Web.Models.Media; namespace Nop.Web.Models.Catalog { public partial class CategoryModel : BaseNopEntityModel { public CategoryModel() { PictureModel = new PictureModel(); FeaturedProducts = new List<ProductOverviewModel>(); Products = new List<ProductOverviewModel>(); PagingFilteringContext = new CatalogPagingFilteringModel(); SubCategories = new List<SubCategoryModel>(); CategoryBreadcrumb = new List<CategoryModel>(); } public string Name { get; set; } public string Description { get; set; } public string MetaKeywords { get; set; } public string MetaDescription { get; set; } public string MetaTitle { get; set; } public string SeName { get; set; } public PictureModel PictureModel { get; set; } public CatalogPagingFilteringModel PagingFilteringContext { get; set; } public bool DisplayCategoryBreadcrumb { get; set; } public IList<CategoryModel> CategoryBreadcrumb { get; set; } public IList<SubCategoryModel> SubCategories { get; set; } public IList<ProductOverviewModel> FeaturedProducts { get; set; } public IList<ProductOverviewModel> Products { get; set; } #region Nested Classes public partial class SubCategoryModel : BaseNopEntityModel { public SubCategoryModel() { PictureModel = new PictureModel(); } public string Name { get; set; } public string SeName { get; set; } public PictureModel PictureModel { get; set; } } #endregion } }
using System.Collections.Generic; using System.Web.Mvc; namespace Nop.Web.Framework.Mvc { /// <summary> /// Base NopCommerce model /// </summary> public partial class BaseNopModel { public BaseNopModel() { this.CustomProperties = new Dictionary<string, object>(); } public virtual void BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { } /// <summary> /// Use this property to store any custom value for your models. /// </summary> public Dictionary<string, object> CustomProperties { get; set; } } /// <summary> /// Base nopCommerce entity model /// </summary> public partial class BaseNopEntityModel : BaseNopModel { public virtual int Id { get; set; } } }
2、Html.Pager
<div class="pager"> @Html.Pager(Model.PagingFilteringContext).QueryParam("pagenumber") </div>
Nop分页导航是通过一个分页类Pager和扩展方法实现的。
public static Pager Pager(this HtmlHelper helper, IPageableModel pagination) { return new Pager(pagination, helper.ViewContext); }
Pager类:
//Contributor : MVCContrib using System; using System.Collections.Generic; using System.linq; using System.Text; using System.Web; using System.Web.Mvc; using System.Web.Routing; using Nop.Core.Infrastructure; using Nop.Services.Localization; namespace Nop.Web.Framework.UI.Paging { /// <summary> /// Renders a pager component from an IPageableModel datasource. /// </summary> public partial class Pager : IHtmlString { protected readonly IPageableModel model; protected readonly ViewContext viewContext; protected string pageQueryName = "page"; protected bool showTotalSummary; protected bool showPagerItems = true; protected bool showFirst = true; protected bool showPrevious = true; protected bool showNext = true; protected bool showLast = true; protected bool showIndividualPages = true; protected int individualPagesDisplayedCount = 5; protected Func<int, string> urlBuilder; protected IList<string> booleanParameterNames; public Pager(IPageableModel model, ViewContext context) { this.model = model; this.viewContext = context; this.urlBuilder = CreateDefaultUrl; this.booleanParameterNames = new List<string>(); } protected ViewContext ViewContext { get { return viewContext; } } public Pager QueryParam(string value) { this.pageQueryName = value; return this; } public Pager ShowTotalSummary(bool value) { this.showTotalSummary = value; return this; } public Pager ShowPagerItems(bool value) { this.showPagerItems = value; return this; } public Pager ShowFirst(bool value) { this.showFirst = value; return this; } public Pager ShowPrevious(bool value) { this.showPrevious = value; return this; } public Pager ShowNext(bool value) { this.showNext = value; return this; } public Pager ShowLast(bool value) { this.showLast = value; return this; } public Pager ShowIndividualPages(bool value) { this.showIndividualPages = value; return this; } public Pager IndividualPagesDisplayedCount(int value) { this.individualPagesDisplayedCount = value; return this; } public Pager Link(Func<int, string> value) { this.urlBuilder = value; return this; } //little hack here due to ugly MVC implementation //find more info here: http://www.mindstorminteractive.com/topics/jquery-fix-asp-net-mvc-checkbox-truefalse-value/ public Pager BooleanParameterName(string paramName) { booleanParameterNames.Add(paramName); return this; } public override string ToString() { return ToHtmlString(); } public virtual string ToHtmlString() { if (model.TotalItems == 0) return null; var localizationService = EngineContext.Current.Resolve<ILocalizationService>(); var links = new StringBuilder(); if (showTotalSummary && (model.TotalPages > 0)) { links.Append("<li class=\"total-summary\">"); links.Append(string.Format(localizationService.GetResource("Pager.CurrentPage"), model.PageIndex + 1, model.TotalPages, model.TotalItems)); links.Append("</li>"); } if (showPagerItems && (model.TotalPages > 1)) { if (showFirst) { //first page if ((model.PageIndex >= 3) && (model.TotalPages > individualPagesDisplayedCount)) { links.Append(CreatePageLink(1, localizationService.GetResource("Pager.First"), "first-page")); } } if (showPrevious) { //previous page if (model.PageIndex > 0) { links.Append(CreatePageLink(model.PageIndex, localizationService.GetResource("Pager.Previous"), "previous-page")); } } if (showIndividualPages) { //individual pages int firstIndividualPageIndex = GetFirstIndividualPageIndex(); int lastIndividualPageIndex = GetLastIndividualPageIndex(); for (int i = firstIndividualPageIndex; i <= lastIndividualPageIndex; i++) { if (model.PageIndex == i) { links.AppendFormat("<li class=\"current-page\"><span>{0}</span></li>", (i + 1)); } else { links.Append(CreatePageLink(i + 1, (i + 1).ToString(), "individual-page")); } } } if (showNext) { //next page if ((model.PageIndex + 1) < model.TotalPages) { links.Append(CreatePageLink(model.PageIndex + 2, localizationService.GetResource("Pager.Next"), "next-page")); } } if (showLast) { //last page if (((model.PageIndex + 3) < model.TotalPages) && (model.TotalPages > individualPagesDisplayedCount)) { links.Append(CreatePageLink(model.TotalPages, localizationService.GetResource("Pager.Last"), "last-page")); } } } var result = links.ToString(); if (!String.IsNullOrEmpty(result)) { result = "<ul>" + result + "</ul>"; } return result; } protected virtual int GetFirstIndividualPageIndex() { if ((model.TotalPages < individualPagesDisplayedCount) || ((model.PageIndex - (individualPagesDisplayedCount / 2)) < 0)) { return 0; } if ((model.PageIndex + (individualPagesDisplayedCount / 2)) >= model.TotalPages) { return (model.TotalPages - individualPagesDisplayedCount); } return (model.PageIndex - (individualPagesDisplayedCount / 2)); } protected virtual int GetLastIndividualPageIndex() { int num = individualPagesDisplayedCount / 2; if ((individualPagesDisplayedCount % 2) == 0) { num--; } if ((model.TotalPages < individualPagesDisplayedCount) || ((model.PageIndex + num) >= model.TotalPages)) { return (model.TotalPages - 1); } if ((model.PageIndex - (individualPagesDisplayedCount / 2)) < 0) { return (individualPagesDisplayedCount - 1); } return (model.PageIndex + num); } protected virtual string CreatePageLink(int pageNumber, string text, string cssClass) { var liBuilder = new TagBuilder("li"); if (!String.IsNullOrWhiteSpace(cssClass)) liBuilder.AddCssClass(cssClass); var aBuilder = new TagBuilder("a"); aBuilder.SetInnerText(text); aBuilder.MergeAttribute("href", urlBuilder(pageNumber)); liBuilder.InnerHtml += aBuilder; return liBuilder.ToString(TagRenderMode.Normal); } protected virtual string CreateDefaultUrl(int pageNumber) { var routeValues = new RouteValueDictionary(); foreach (var key in viewContext.RequestContext.HttpContext.Request.QueryString.AllKeys.Where(key => key != null)) { var value = viewContext.RequestContext.HttpContext.Request.QueryString[key]; if (booleanParameterNames.Contains(key, StringComparer.InvariantCultureIgnoreCase)) { //little hack here due to ugly MVC implementation //find more info here: http://www.mindstorminteractive.com/topics/jquery-fix-asp-net-mvc-checkbox-truefalse-value/ if (!String.IsNullOrEmpty(value) && value.Equals("true,false", StringComparison.InvariantCultureIgnoreCase)) { value = "true"; } } routeValues[key] = value; } if (pageNumber > 1) { routeValues[pageQueryName] = pageNumber; } else { //SEO. we do not render pageindex query string parameter for the first page if (routeValues.ContainsKey(pageQueryName)) { routeValues.Remove(pageQueryName); } } var url = UrlHelper.GenerateUrl(null, null, null, routeValues, RouteTable.Routes, viewContext.RequestContext, true); return url; } } }
3、Html.Widget
在Nop中用到很多@Html.Widget这个扩展方法,它其实是返回局部html。下面我们来看看它到底是怎么实现的。
调用方法:
@Html.Widget("categorydetails_after_breadcrumb", Model.Id)
HtmlHelper扩展方法:
public static MvcHtmlString Widget(this HtmlHelper helper, string widgetZone, object additionalData = null) { return helper.Action("WidgetsByZone", "Widget", new { widgetZone = widgetZone, additionalData = additionalData }); }
Nop.Web.Controllers.WidgetController:
[ChildActionOnly] public ActionResult WidgetsByZone(string widgetZone, object additionalData = null) { var cacheKey = string.Format(ModelCacheEventConsumer.WIDGET_MODEL_KEY, _storeContext.CurrentStore.Id, widgetZone); var cacheModel = _cacheManager.Get(cacheKey, () => { //model var model = new List<RenderWidgetModel>(); var widgets = _widgetService.LoadActiveWidgetsByWidgetZone(widgetZone, _storeContext.CurrentStore.Id); foreach (var widget in widgets) { var widgetModel = new RenderWidgetModel(); string actionName; string controllerName; RouteValueDictionary routeValues; widget.GetDisplayWidgetRoute(widgetZone, out actionName, out controllerName, out routeValues); widgetModel.ActionName = actionName; widgetModel.ControllerName = controllerName; widgetModel.RouteValues = routeValues; model.Add(widgetModel); } return model; }); //no data? if (cacheModel.Count == 0) return Content(""); //"RouteValues" property of widget models depends on "additionalData". //We need to clone the cached model before modifications (the updated one should not be cached) var clonedModel = new List<RenderWidgetModel>(); foreach (var widgetModel in cacheModel) { var clonedWidgetModel = new RenderWidgetModel(); clonedWidgetModel.ActionName = widgetModel.ActionName; clonedWidgetModel.ControllerName = widgetModel.ControllerName; if (widgetModel.RouteValues != null) clonedWidgetModel.RouteValues = new RouteValueDictionary(widgetModel.RouteValues); if (additionalData != null) { if (clonedWidgetModel.RouteValues == null) clonedWidgetModel.RouteValues = new RouteValueDictionary(); clonedWidgetModel.RouteValues.Add("additionalData", additionalData); } clonedModel.Add(clonedWidgetModel); } return PartialView(clonedModel); }