So having made the suggestion of using Category Templates to implement a CMS feature I have got round to doing it.
This is going to be a multi-part step by step guide as there are going to be parts you want and others where you branch of on your own route.
What I am going to do is create a new Category Template Type (page category) which we will use much like a topic. In addition I'm guessing that some people will not want these page categories appearing in the left hand Category Navigation but somewhere else. Of course some will want those page categories included in the Category Navigation.
In part 2 we will customise the page template to display correctly, handling subcategories etc.
I've got some plans for part 3 but I'm keeping those under my hat for now.
To follow these steps you will need the source code version of 2.65 (it may well work with little or no modification on earlier versions back to 2.0 {untested} and the concept is certainly applicable to versions 1.7-1.9).
Note; We are modifying some core files in nop so you will not be able to upgrade without some work however the modifications are not that large. Quite possibly this could be done as a plugin but I haven't got my head around that. Feel free to do that if you want.
On to the steps.
1. find file Presentation\Nop.Web\Views\Catalog\CategoryTemplate.ProductsInGridOrLines.cshtml
copy the file and paste it into the same folder
it will appear as Presentation\Nop.Web\Views\Catalog\Copy of CategoryTemplate.ProductsInGridOrLines.cshtml
rename it to CategoryTemplate.PageType1.cshtml
2. Run the following code against the nop database replace YOURNOPDBNAMEHERE with the name of your nop database.
INSERT INTO [YOURNOPDBNAMEHERE].[dbo].[CategoryTemplate]
([Name]
,[ViewPath]
,[DisplayOrder])
VALUES
('Page Type 1'
,'CategoryTemplate.PageType1'
,20)
GO
3. Run the site in Visual Studio and check that this has worked by creating a new category 'Test Page' and selecting the Page Type 1 as the category template. You don't need to enter any other data at this point.
While your at it you need to find out what ID has been assigned to this page type. I've no idea what DB management tools you are using but if you are stuck right click on the web page when you are adding the new category and select view source. Then find the text 'Page Type 1' and look at the value before it. This is the categoryTemplateId we need. It will look like this if you have not added any other category templates;-
<option value="1">Products in Grid or Lines</option>
<option value="2">Page Type 1</option>
Make a note of the Page Type 1 value i.e. its categoryTemplateId
4. Go back to the front end of nop and you will see the new category in the left hand category navigation. We want to gain some control over the display of categories in this navigation and we are going to do this based on which template the category uses.
5. To do this we need to know which category template a category uses when we write out the category navigation. unfortunately this information is not in the Category Navigation Model so we need to add it.
Open file Presentation\Nop.Web\Models\Catalog\CategoryNavigationModel.cs
and replace the code with the following;
using Nop.Web.Framework.Mvc;
namespace Nop.Web.Models.Catalog
{
public partial class CategoryNavigationModel : BaseNopEntityModel
{
public string Name { get; set; }
public string SeName { get; set; }
public int NumberOfParentCategories { get; set; }
public bool DisplayNumberOfProducts { get; set; }
public int NumberOfProducts { get; set; }
public bool IsActive { get; set; }
// added for template driven navigation menus
public int CategoryTemplateId { get; set; }
}
}
Now I could have placed the added code in a new class file extending the CategoryNavigationModel as this is a partial class but I'm being lazy.
6. Now we need to populate this value
find file Presentation\Nop.Web\Controllers\CatalogController.cs
and find method GetChildCategoryNavigationModel
replace the method with the following code
[NonAction]
protected IList<CategoryNavigationModel> GetChildCategoryNavigationModel(IList<Category> breadCrumb, int rootCategoryId, Category currentCategory, int level)
{
var result = new List<CategoryNavigationModel>();
foreach (var category in _categoryService.GetAllCategoriesByParentCategoryId(rootCategoryId))
{
// added CategoryTemplateId for template driven navigation menus
var model = new CategoryNavigationModel()
{
Id = category.Id,
Name = category.GetLocalized(x => x.Name),
SeName = category.GetSeName(),
IsActive = currentCategory != null && currentCategory.Id == category.Id,
NumberOfParentCategories = level,
CategoryTemplateId = category.CategoryTemplateId
};
if (_catalogSettings.ShowCategoryProductNumber)
{
model.DisplayNumberOfProducts = true;
var categoryIds = new List<int>();
categoryIds.Add(category.Id);
if (_catalogSettings.ShowCategoryProductNumberIncludingSubcategories)
{
//include subcategories
categoryIds.AddRange(GetChildCategoryIds(category.Id));
}
IList<int> filterableSpecificationAttributeOptionIds = null;
model.NumberOfProducts = _productService.SearchProducts(categoryIds,
0, null, null, null, 0, string.Empty, false, false, 0, null,
ProductSortingEnum.Position, 0, 1,
false, out filterableSpecificationAttributeOptionIds).TotalCount;
}
result.Add(model);
for (int i = 0; i <= breadCrumb.Count - 1; i++)
if (breadCrumb[i].Id == category.Id)
result.AddRange(GetChildCategoryNavigationModel(breadCrumb, category.Id, currentCategory, level + 1));
}
return result;
}
7. Now we need to modify the Category Navigation view to exclude certain category templates. We want to provide some control over which ones are included so we will use a setting.
Run the following SQL code against your DB remembering to replace YOURNOPDBNAMEHERE with the name of your nop database.
INSERT INTO [YOURNOPDBNAMEHERE ].[dbo].[Setting]
([Name]
,[Value])
VALUES
('catalogsettings.categorynavigationallowedtemplateids'
,'1')
GO
Note if you already have more category templates that you have created that you wish to include in the category navigation you need to add them after the 1 separated by commas eg '1,2,3,etc'
and
INSERT INTO [YOURNOPDBNAMEHERE ].[dbo].[Setting]
([Name]
,[Value])
VALUES
('catalogsettings.menunavigationallowedtemplateids'
,'2')
GO
Note if your categoryTemplateId that you noted in step 3 is not 2 then change the 2 to whatever above
we are going to use the second setting later but may as well add it now
8. Find File Libraries\Nop.Core\Domain\Catalog\CatalogSettings.cs
and replace the CatalogSettings() method with
public CatalogSettings()
{
FileUploadAllowedExtensions = new List<string>();
// DMB 16/11/2012 added
CategoryNavigationAllowedTemplateIds = new List<string>();
MenuNavigationAllowedTemplateIds = new List<string>();
}
and at the bottom of CatalogSettings.cs add the following code
/// <summary>
/// added
/// Gets or sets a list of allowed template ids for category navigation
/// </summary>
public List<string> CategoryNavigationAllowedTemplateIds { get; set; }
/// <summary>
/// added
/// Gets or sets a list of allowed template ids for menu navigation
/// </summary>
public List<string> MenuNavigationAllowedTemplateIds { get; set; }
Note we are also adding code to handle the second setting now.
I was really impressed with how easy it is to add settings to Nop and as you will see they are very easy to access even from the views.
9. Now we can modify the category navigation
find file Presentation\Nop.Web\Views\Catalog\CategoryNavigation.cshtml
after line 7 add the following lines
// added for filtering based of category templates
var allowedTemplateIdList = EngineContext.Current.Resolve<CatalogSettings>().MenuNavigationAllowedTemplateIds;
so it looks like
@{
var categoryPadding = 15;
// added for filtering based of category templates
var allowedTemplateIdList = EngineContext.Current.Resolve<CatalogSettings>().CategoryNavigationAllowedTemplateIds;
}
and replace the main block of code (starts line 19 or 21 after the above change) with
@foreach (var category in Model)
{
// added to filter out those templates not displayed here
if (allowedTemplateIdList.Contains(category.CategoryTemplateId.ToString()))
{
<li class="@(category.IsActive ? "active" : "inactive")"
@if (category.NumberOfParentCategories > 0)
{
if (this.ShouldUseRtlTheme())
{
<text>style="margin-right: @(category.NumberOfParentCategories * categoryPadding)px"</text>
}
else
{
<text>style="margin-left: @(category.NumberOfParentCategories * categoryPadding)px"</text>
}
}
><a href="@Url.RouteUrl("Category", new { categoryId = category.Id, SeName = category.SeName })">@category.Name
@if (category.DisplayNumberOfProducts)
{
<text> (@(category.NumberOfProducts))</text>
}
</a></li>
}
}
10. Run the project and check that the Test Page category is no longer being displayed by the Category Navigation. Great but now we need to display an alternate navigation for our pages.
There are going to be numerous options/requirements for this but I'm going to create a dynamic navigation block for pages below the Category Navigation.
11. Find file Presentation\Nop.Web\Views\Catalog\CategoryNavigation.cshtml
copy the file and paste it into the same folder
it will appear as Presentation\Nop.Web\Views\Catalog\Copy of CategoryNavigation.cshtml
rename it to MenuNavigation.cshtml
change line 9 to
var allowedTemplateIdList = EngineContext.Current.Resolve<CatalogSettings>().MenuNavigationAllowedTemplateIds;
and line 15 to
@T("Pages")
12. Find file Presentation\Nop.Web\Controllers\CatalogController.cs
and add the following method (I would place it below the CategoryNavigation Method)
//added
[ChildActionOnly]
//[OutputCache(Duration = 120, VaryByCustom = "WorkingLanguage")]
public ActionResult MenuNavigation(int currentCategoryId, int currentProductId)
{
// this should only change when the current category is in this tree
// if they are not in the tree then return the tree for currentCategoryId = 0
// am ignoring products for now due to multiple category parents
var currentCategory = _categoryService.GetCategoryById(currentCategoryId);
if (currentCategory != null)
{
var allowedTemplateIdList = _catalogSettings.MenuNavigationAllowedTemplateIds;
if (!allowedTemplateIdList.Contains(currentCategory.CategoryTemplateId.ToString()))
{
currentCategoryId = 0;
currentCategory = _categoryService.GetCategoryById(currentCategoryId);
}
}
string cacheKey = string.Format(ModelCacheEventConsumer.CATEGORY_MENU_MODEL_KEY, currentCategoryId, currentProductId, _workContext.WorkingLanguage.Id);
var cacheModel = _cacheManager.Get(cacheKey, () =>
{
//var currentCategory = _categoryService.GetCategoryById(currentCategoryId);
if (currentCategory == null && currentProductId > 0)
{
var productCategories = _categoryService.GetProductCategoriesByProductId(currentProductId);
if (productCategories.Count > 0)
currentCategory = productCategories[0].Category;
}
var breadCrumb = currentCategory != null ? GetCategoryBreadCrumb(currentCategory) : new List<Category>();
var model = GetChildCategoryNavigationModel(breadCrumb, 0, currentCategory, 0);
return model;
});
return PartialView(cacheModel);
}
13. Find file Presentation\Nop.Web\Infrastructure\Cache\ModelCacheEventConsumer.cs
add the following (I placed it below the entries for CATEGORY_NAVIGATION_MODEL_KEY)
/// <summary>
/// added
/// Key for CategoryMenuModel caching
/// </summary>
/// <remarks>
/// {0} : current category id
/// {1} : current product id
/// {2} : language id
/// </remarks>
public const string CATEGORY_MENU_MODEL_KEY = "nop.pres.category.navigation.menu-{0}-{1}-{2}";
public const string CATEGORY_MENU_PATTERN_KEY = "nop.pres.category.navigation.menu";
14. Finally :) find file Presentation\Nop.Web\Views\Shared\_ColumnsThree.cshtml
Place the following code below the action to create the Category navigation
@Html.Action("MenuNavigation", "Catalog", new { currentCategoryId = currentCategoryId, currentProductId = currentProductId })
<div class="clear">
</div>
so it looks like this (partial)
@Html.Action("CategoryNavigation", "Catalog", new { currentCategoryId = currentCategoryId, currentProductId = currentProductId })
<div class="clear">
</div>
@Html.Action("MenuNavigation", "Catalog", new { currentCategoryId = currentCategoryId, currentProductId = currentProductId })
<div class="clear">
</div>
@Html.Action("ManufacturerNavigation", "Catalog", new { currentManufacturerId = currentManufacturerId })
<div class="clear">
</div>
Run it in Visual Studio and check that you now have an additional navigation block below the categories titled Pages.
Some Notes The ids that are included in each navigation block are stored as a comma separated list so you can have more than one template in each navigation. You can even have a template in both blocks.
You should be able to create navigation elements anywhere so for instance you could have navigation in the footer or header tho you wouldn't want to use the Category Navigation as it currently works.
In the next part I will go over various customisations of the Category Page Template including modifying how the subcategories display and removing various unwanted items.
If anyone spots any errors or has any queries let me know.