文章目录
概述
NopCommerce源码架构详解-产品列表相关源码分析1。
内容
nop中最常用到的就是产品列表展示,我们可以指定排序方式、指定数据显示方式(List或Grid)和指定每页显示的记录数。
在浏览器查看:http://localhost:15536/software-games?pagesize=4&viewmode=list&orderby=15
会有如下图所示:

这个分类的Url地址为:/software-games?pagesize=4&viewmode=list&orderby=15,可以看到Nop的所有产品的分类url都是通过用分类名这种利于seo的方式,而不是通过分类ID的访问。
这里主要用到了Nop.Web.Infrastructure.GenericUrlRouteProvider和Nop.Web.Framework.Seo.GenericPathRoute。
using System.Web.Routing; using Nop.Web.Framework.Localization; using Nop.Web.Framework.mvc.Routes; using Nop.Web.Framework.Seo; namespace Nop.Web.Infrastructure { public partial class GenericUrlRouteProvider : IRouteProvider { public void RegisterRoutes(RouteCollection routes) { //generic URLs routes.MapGenericPathRoute("GenericUrl", "{generic_se_name}", new {controller = "Common", action = "GenericUrl"}, new[] {"Nop.Web.Controllers"}); //省略其它代码... } public int Priority { get { //it should be the last route //we do not set it to -int.MaxValue so it could be overriden (if required) return -1000000; } } } }
MVC框架会根据路由里面变量generic_se_name值进行转换成具体真实Controller。
/software-games?pagesize=4&viewmode=list&orderby=15
上面的Url的对应generic_se_name为software-games。而我们看Nop的数据库有一个很关键的表:UrlRecord。

这个表的字段Slug正好和generic_se_name相应对应。我们可以看到Slug值为software-games这条记录的EntityName为Category。在类Nop.Web.Framework.Seo.GenericPathRoute有一个GetRouteData方法会根据generic_se_name获取到一个对应的UrlRecord,然后根据这个UrlRecord的EntityName的值,把data.Values值赋值,转到正确的Controller。如下代码:
public override RouteData GetRouteData(HttpContextBase httpContext) { RouteData data = base.GetRouteData(httpContext); if (data != null && DataSettingsHelper.DatabaseIsInstalled()) { var urlRecordService = EngineContext.Current.Resolve<IUrlRecordService>(); var slug = data.Values["generic_se_name"] as string; //performance optimization. //we load a cached verion here. it reduces number of SQL requests for each page load var urlRecord = urlRecordService.GetBySlugCached(slug); //comment the line above and uncomment the line below in order to disable this performance "workaround" //var urlRecord = urlRecordService.GetBySlug(slug); if (urlRecord == null) { //no URL record found //var webHelper = EngineContext.Current.Resolve<IWebHelper>(); //var response = httpContext.Response; //response.Status = "302 Found"; //response.RedirectLocation = webHelper.GetStoreLocation(false); //response.End(); //return null; data.Values["controller"] = "Common"; data.Values["action"] = "PageNotFound"; return data; } //ensre that URL record is active if (!urlRecord.IsActive) { //URL record is not active. let's find the latest one var activeSlug = urlRecordService.GetActiveSlug(urlRecord.EntityId, urlRecord.EntityName, urlRecord.LanguageId); if (!string.IsNullOrWhiteSpace(activeSlug)) { //the active one is found var webHelper = EngineContext.Current.Resolve<IWebHelper>(); var response = httpContext.Response; response.Status = "301 Moved Permanently"; response.RedirectLocation = string.Format("{0}{1}", webHelper.GetStoreLocation(false), activeSlug); response.End(); return null; } else { //no active slug found //var webHelper = EngineContext.Current.Resolve<IWebHelper>(); //var response = httpContext.Response; //response.Status = "302 Found"; //response.RedirectLocation = webHelper.GetStoreLocation(false); //response.End(); //return null; data.Values["controller"] = "Common"; data.Values["action"] = "PageNotFound"; return data; } } //ensure that the slug is the same for the current language //otherwise, it can cause some issues when customers choose a new language but a slug stays the same var workContext = EngineContext.Current.Resolve<IWorkContext>(); var slugForCurrentLanguage = SeoExtensions.GetSeName(urlRecord.EntityId, urlRecord.EntityName, workContext.WorkingLanguage.Id); if (!String.IsNullOrEmpty(slugForCurrentLanguage) && !slugForCurrentLanguage.Equals(slug, StringComparison.InvariantCultureIgnoreCase)) { //we should make not null or "" validation above because some entities does not have SeName for standard (ID=0) language (e.g. news, blog posts) var webHelper = EngineContext.Current.Resolve<IWebHelper>(); var response = httpContext.Response; //response.Status = "302 Found"; response.Status = "302 Moved Temporarily"; response.RedirectLocation = string.Format("{0}{1}", webHelper.GetStoreLocation(false), slugForCurrentLanguage); response.End(); return null; } //process URL switch (urlRecord.EntityName.ToLowerInvariant()) { case "product": { data.Values["controller"] = "Product"; data.Values["action"] = "ProductDetails"; data.Values["productid"] = urlRecord.EntityId; data.Values["SeName"] = urlRecord.Slug; } break; case "category": { data.Values["controller"] = "Catalog"; data.Values["action"] = "Category"; data.Values["categoryid"] = urlRecord.EntityId; data.Values["SeName"] = urlRecord.Slug; } break; case "manufacturer": { data.Values["controller"] = "Catalog"; data.Values["action"] = "Manufacturer"; data.Values["manufacturerid"] = urlRecord.EntityId; data.Values["SeName"] = urlRecord.Slug; } break; case "vendor": { data.Values["controller"] = "Catalog"; data.Values["action"] = "Vendor"; data.Values["vendorid"] = urlRecord.EntityId; data.Values["SeName"] = urlRecord.Slug; } break; case "newsitem": { data.Values["controller"] = "News"; data.Values["action"] = "NewsItem"; data.Values["newsItemId"] = urlRecord.EntityId; data.Values["SeName"] = urlRecord.Slug; } break; case "blogpost": { data.Values["controller"] = "Blog"; data.Values["action"] = "BlogPost"; data.Values["blogPostId"] = urlRecord.EntityId; data.Values["SeName"] = urlRecord.Slug; } break; case "topic": { data.Values["controller"] = "Topic"; data.Values["action"] = "TopicDetails"; data.Values["topicId"] = urlRecord.EntityId; data.Values["SeName"] = urlRecord.Slug; } break; default: { //no record found //generate an event this way developers could insert their own types EngineContext.Current.Resolve<IEventPublisher>() .Publish(new CustomUrlRecordEntityNameRequested(data, urlRecord)); } break; } } return data; }
当请求一个分类的产品列表的时候会进入switch的category的分支,也就是controller:Catalog,action:Category。
下面我们再来看看产品分类相关表:
1、Category:分类信息表

分类信息表包含了产品类别的基础信息以及一些配置信息,如seo相关信息、筛选页大小选项、是否在首页显示等等。
2、CategoryTemplate:分类对应视图、模版

CategoryTemplate表示分类对应的视图路径。
Nop.Web.Controllers.CatalogController
下面我们来看看Action–Category的完整代码:
public ActionResult Category(int categoryId, CatalogPagingFilteringModel command) { var category = _categoryService.GetCategoryById(categoryId); if (category == null || category.Deleted) return InvokeHttp404(); //Check whether the current user has a "Manage catalog" permission //It allows him to preview a category before publishing if (!category.Published && !_permissionService.Authorize(StandardPermissionProvider.ManageCategories)) return InvokeHttp404(); //ACL (access control list) if (!_aclService.Authorize(category)) return InvokeHttp404(); //Store mapping if (!_storeMappingService.Authorize(category)) return InvokeHttp404(); //'Continue shopping' URL _genericAttributeService.SaveAttribute(_workContext.CurrentCustomer, SystemCustomerAttributeNames.LastContinueShoppingPage, _webHelper.GetThisPageUrl(false), _storeContext.CurrentStore.Id); var model = category.ToModel(); //处理排序字段 PrepareSortingOptions(model.PagingFilteringContext, command); //view mode PrepareViewModes(model.PagingFilteringContext, command); //page size PreparePageSizeOptions(model.PagingFilteringContext, command, category.AllowCustomersToSelectPageSize, category.PageSizeOptions, category.PageSize); //价格筛选 model.PagingFilteringContext.PriceRangeFilter.LoadPriceRangeFilters(category.PriceRanges, _webHelper, _priceFormatter); var selectedPriceRange = model.PagingFilteringContext.PriceRangeFilter.GetSelectedPriceRange(_webHelper, category.PriceRanges); decimal? minPriceConverted = null; decimal? maxPriceConverted = null; if (selectedPriceRange != null) { if (selectedPriceRange.From.HasValue) minPriceConverted = _currencyService.ConvertToPrimaryStoreCurrency(selectedPriceRange.From.Value, _workContext.WorkingCurrency); if (selectedPriceRange.To.HasValue) maxPriceConverted = _currencyService.ConvertToPrimaryStoreCurrency(selectedPriceRange.To.Value, _workContext.WorkingCurrency); } //分类面包屑 model.DisplayCategoryBreadcrumb = _catalogSettings.CategoryBreadcrumbEnabled; if (model.DisplayCategoryBreadcrumb) { foreach (var catBr in category.GetCategoryBreadCrumb(_categoryService, _aclService, _storeMappingService)) { model.CategoryBreadcrumb.Add(new CategoryModel() { Id = catBr.Id, Name = catBr.GetLocalized(x => x.Name), SeName = catBr.GetSeName() }); } } var customerRolesIds = _workContext.CurrentCustomer.CustomerRoles .Where(cr => cr.Active).Select(cr => cr.Id).ToList(); //subcategories string subCategoriesCacheKey = string.Format(ModelCacheEventConsumer.CATEGORY_SUBCATEGORIES_KEY, categoryId, string.Join(",", customerRolesIds), _storeContext.CurrentStore.Id, _workContext.WorkingLanguage.Id, _webHelper.IsCurrentConnectionSecured()); model.SubCategories = _cacheManager.Get(subCategoriesCacheKey, () => { return _categoryService.GetAllCategoriesByParentCategoryId(categoryId) .Select(x => { var subCatModel = new CategoryModel.SubCategoryModel() { Id = x.Id, Name = x.GetLocalized(y => y.Name), SeName = x.GetSeName(), }; //prepare picture model int pictureSize = _mediaSettings.CategoryThumbPictureSize; var categoryPictureCacheKey = string.Format(ModelCacheEventConsumer.CATEGORY_PICTURE_MODEL_KEY, x.Id, pictureSize, true, _workContext.WorkingLanguage.Id, _webHelper.IsCurrentConnectionSecured(), _storeContext.CurrentStore.Id); subCatModel.PictureModel = _cacheManager.Get(categoryPictureCacheKey, () => { var picture = _pictureService.GetPictureById(x.PictureId); var pictureModel = new PictureModel() { FullSizeImageUrl = _pictureService.GetPictureUrl(picture), ImageUrl = _pictureService.GetPictureUrl(picture, pictureSize), Title = string.Format(_localizationService.GetResource("Media.Category.ImageLinkTitleFormat"), subCatModel.Name), AlternateText = string.Format(_localizationService.GetResource("Media.Category.ImageAlternateTextFormat"), subCatModel.Name) }; return pictureModel; }); return subCatModel; }) .ToList(); }); //featured products if (!_catalogSettings.IgnoreFeaturedProducts) { //We cache a value indicating whether we have featured products IPagedList<Product> featuredProducts = null; string cacheKey = string.Format(ModelCacheEventConsumer.CATEGORY_HAS_FEATURED_PRODUCTS_KEY, categoryId, string.Join(",", customerRolesIds), _storeContext.CurrentStore.Id); var hasFeaturedProductsCache = _cacheManager.Get<bool?>(cacheKey); if (!hasFeaturedProductsCache.HasValue) { //no value in the cache yet //let's load products and cache the result (true/false) featuredProducts = _productService.SearchProducts( categoryIds: new List<int>() { category.Id }, storeId: _storeContext.CurrentStore.Id, visibleIndividuallyOnly: true, featuredProducts: true); hasFeaturedProductsCache = featuredProducts.TotalCount > 0; _cacheManager.Set(cacheKey, hasFeaturedProductsCache, 60); } if (hasFeaturedProductsCache.Value && featuredProducts == null) { //cache indicates that the category has featured products //let's load them featuredProducts = _productService.SearchProducts( categoryIds: new List<int>() { category.Id }, storeId: _storeContext.CurrentStore.Id, visibleIndividuallyOnly: true, featuredProducts: true); } if (featuredProducts != null) { model.FeaturedProducts = PrepareProductOverviewModels(featuredProducts).ToList(); } } var categoryIds = new List<int>(); categoryIds.Add(category.Id); if (_catalogSettings.ShowProductsFromSubcategories) { //include subcategories categoryIds.AddRange(GetChildCategoryIds(category.Id)); } //products IList<int> alreadyFilteredSpecOptionIds = model.PagingFilteringContext.SpecificationFilter.GetAlreadyFilteredSpecOptionIds(_webHelper); IList<int> filterableSpecificationAttributeOptionIds = null; var products = _productService.SearchProducts(out filterableSpecificationAttributeOptionIds, true, categoryIds: categoryIds, storeId: _storeContext.CurrentStore.Id, visibleIndividuallyOnly: true, featuredProducts:_catalogSettings.IncludeFeaturedProductsInNormalLists ? null : (bool?)false, priceMin:minPriceConverted, priceMax:maxPriceConverted, filteredSpecs: alreadyFilteredSpecOptionIds, orderBy: (ProductSortingEnum)command.OrderBy, pageIndex: command.PageNumber - 1, pageSize: command.PageSize); model.Products = PrepareProductOverviewModels(products).ToList(); model.PagingFilteringContext.LoadPagedList(products); //specs model.PagingFilteringContext.SpecificationFilter.PrepareSpecsFilters(alreadyFilteredSpecOptionIds, filterableSpecificationAttributeOptionIds, _specificationAttributeService, _webHelper, _workContext); //template var templateCacheKey = string.Format(ModelCacheEventConsumer.CATEGORY_TEMPLATE_MODEL_KEY, category.CategoryTemplateId); var templateViewPath = _cacheManager.Get(templateCacheKey, () => { var template = _categoryTemplateService.GetCategoryTemplateById(category.CategoryTemplateId); if (template == null) template = _categoryTemplateService.GetAllCategoryTemplates().FirstOrDefault(); if (template == null) throw new Exception("No default template could be loaded"); return template.ViewPath; }); //activity log _customerActivityService.InsertActivity("PublicStore.ViewCategory", _localizationService.GetResource("ActivityLog.PublicStore.ViewCategory"), category.Name); return View(templateViewPath, model); }