Building a Reusable Product Search Block in Episerver 9 Part 1: Setup & Block Controller

I just finished up working on a pretty big Episerver project and wanted to share some cool things I learned how to build. I will touch on functionality dealing with Episerver CMS, Commerce and Find. What we are going to build is a reusable product search block. For the sake of simplicity, it will be a very basic search, but, there is still plenty to build to bring it all together. Below is what the block will look like. It will have a keyword field and a select list, which we will populate with product manufacturers, which equates to our product categories, or in Episerver Commerce terminology, NodeContent. This tutorial will include several code samples, and if you want to see everything, all of the code is in my public Github repo Episerver Commerce Sandbox.

Reusable Search Block

So let's break down all the pieces we are going to build. We will need a block model and view, a block controller and an Initializable Module, which we will use to register a custom route for the search block. For our search, we will build a service using Episerver Find. It will be an ITypeSearch, which is an interface used to search a specific type, which in this case will be our Commerce product variants. We will use another Initializable Module for Find functionality. The last piece we will build is a search results page type, which will need a model, controller and view. The way that everything flows is pretty easy to picture. The block will take the users input pass it to the the block controller, which will then pass the data in a query string to the results page controller, which will pump the input into our search service and display the returned results in the view. To make this all more digestible, I am going to break this tutorial into 3 parts. Part 1 will deal with the block pieces and some general setup classes. Now, let's start writing some code!

For the block model, I am going create one called ProductSearchBlockData. I am also going to create an abstract class that it will inherit from called SearchBlockData. This model will have all of the fields that will be common for all search blocks and ProductSearchBlockData will have fields specific to this search block. Episerver makes it easy to utilize abstraction with types, and I would recommend using it if you do not already.

SearchBlockData Abstract Model:

 
using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.DataAnnotations;
using System.ComponentModel.DataAnnotations;

namespace EpiCommerceSandbox.Models.Blocks.Abstracts
{
    public abstract class SearchBlockData : BlockData
    {
        [CultureSpecific]
        [Display(
           Name = "Title",
           GroupName = SystemTabNames.Content,
           Order = 1)]
        public virtual string Title { get; set; }

        [CultureSpecific]
        [Display(
           Name = "Search Results Page",
           Description = "",
           GroupName = SystemTabNames.Content,
           Order = 2)]
        [Required]
        public virtual ContentReference ResultsPage { get; set; }

        [CultureSpecific]
        [Display(
           Name = "Search Results Page Size",
           Description = "",
           GroupName = SystemTabNames.Content,
           Order = 3)]
        public virtual int PageSize { get; set; }       
    }
}

ProductSearchBlockData Model:

using EPiServer.DataAbstraction;
using EPiServer.DataAnnotations;
using EpiCommerceSandbox.Models.Blocks.Abstracts;
using System.Web.Mvc;
using EPiServer.ServiceLocation;
using Mediachase.Commerce.Catalog;
using EPiServer;
using EPiServer.Commerce.Catalog.ContentTypes;
using System.Collections.Generic;
using System.Linq;

namespace EpiCommerceSandbox.Models.Blocks
{
    [ContentType(
        DisplayName = "Product Search Block", 
        GUID = "9903c756-395a-4093-8428-1ef62118f7b0", 
        Description = "")]
    public class ProductSearchBlockData : SearchBlockData
    {
        [Ignore]
        public IEnumerable ManufacturerList
        {
            get 
            {
                List manufacturers = new List<SelectListItem>();
                manufacturers.Add(new SelectListItem { Text = "- Manufacturer -", Value = "" });

                // Let's get a reference to the catalog categories
                var referenceConverter = ServiceLocator.Current.GetInstance<ReferenceConverter>();
                var rootLink = referenceConverter.GetRootLink();
                var repository = ServiceLocator.Current.GetInstance<IContentRepository>();
                var catalogRef = repository.GetChildren<CatalogContent>(rootLink).FirstOrDefault();
                var categories = repository.GetChildren<NodeContent>(catalogRef.ContentLink);

                // Loop through the categories and populate the SelectList
                foreach (NodeContent c in categories.OrderBy(x => x.Name))
                {
                    manufacturers.Add(new SelectListItem() { Text = c.Name, Value = c.Name });
                }

                return manufacturers;
            }                                             
        }       
    }
}

Here are a few things to note about theses models. As you can see, our abstract class inherits from BlockData, which all blocks in Episerver inherit from. There is a required field which is a content reference to our results page. In ProductSearchBlock data, we inherit from our abstract class, and there is only one field, which gets a reference to our Commerce catalog, gets all the child categories <NodeContent> and loops through them and adds the data to a select list. This is a very simple implementation which assumes one level of categories. You will notice that I am including the [Ignore] attribute on this field, so it will only come in to play when we call it from the view.

Block Controller:

using System.Web.Mvc;
using EPiServer.Web.Mvc;
using EpiCommerceSandbox.Models.Blocks;
using EPiServer.Framework.DataAnnotations;
using EPiServer.Framework.Web;

namespace EpiCommerceSandbox.Controllers.Block
{
    [TemplateDescriptor(Default = true, TemplateTypeCategory = TemplateTypeCategories.MvcPartialController, Inherited = false, AvailableWithoutTag = true)]
    public class ProductSearchBlockController : BlockController<ProductSearchBlockData>
    {
        public ActionResult Search(string redirect, string pageSize, string keyword = "", string manufacturer = "")
        {            
            return Redirect(redirect + "?keyword=" + keyword + "&manufacturer=" + manufacturer + "&pageSize=" + pageSize);
        }
    }
}

As you can see, this block controller is pretty simple. It has one MVC ActionResult, which takes the input from the block and redirects to our results page, passing the data in a query string. The most important thing to note about this controller is the TemplateDescriptor attribute at the top of the class.

The next thing we need to do is register our route with Episerver. The easiest way to do this is to create an init module. You can include all your custom routes in this class. 

Route Config Initializable Module:

using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using EPiServer.ServiceLocation;
using System.Web.Mvc;
using System.Web.Routing;

namespace EpiCommerceSandbox.Business.Initialization
{
    [InitializableModule]
    [ModuleDependency(typeof(ServiceContainerInitialization))]
    public class RouteConfig :  IInitializableModule
    {
        public void Initialize(InitializationEngine context)
        {
            RegisterRoutes(RouteTable.Routes);
        }

        private static string ControllerName(string Name) => Name.Replace("Controller", "");

        private void RegisterRoutes(RouteCollection routeCollection)
        {
            routeCollection.MapRoute(
               name: "productSearch",
               url: "ProductSearchBlock/Search",
               defaults: new { controller = "ProductSearchBlock", action = "Search" }
            );          
        }

        public void Uninitialize(InitializationEngine context) { }
        public void Preload(string[] parameters) { }
    }
}

The last piece I am going to touch on is the view for our search block, which you can see here. It's a very simple view, and you will note that inside the form, I have two hidden inputs to pass the model data for PageSize and ResultsPage. One thing I like to do with blocks that I am going to reuse is utilize Episerver display templates to make it really easy to render the content in other views. Below is how I would add this block to a type model as a content reference:

[Display(
            Name = "Footer Product Search Block",
            GroupName = SystemTabNames.Settings,
            Order = 10)]
        [AllowedTypes(typeof(ProductSearchBlockData))]
        [UIHint(Business.Constants.CustomUIHints.ContentDisplay, PresentationLayer.Website)]
        public virtual ContentReference ProductSearch { get; set; }

The thing to note here is the UIHint attribute, which refers to a class called Constants and the name of the display template I want to use. You can see the markup for the display template here. It takes a ContentReference as the model, gets the content, and then displays it. What is really nice about display templates it that it allows you to reuse markup using a very simple syntax. For our example here, to render the block view inside another view, we would simply write:

@if (Site.SiteSettings.ProductSearch != null)
{     
     @Html.PropertyFor(x => Site.SiteSettings.ProductSearch)
}

If you aren't currently utilizing Episerver Display templates in your projects, you really should start. You will see how it can simply the markup for rendering views inside of views. That's it for this part of the tutorial. Stay tuned for part 2, which will deal with our Episerver Find search service. I ran into several challenges while working with Find and Commerce content. If you have any questions or comments concerning the code above, please leave a comment below. Until next time, code monkeys, be excellent to each other. And party on dudes!

Read Part Two

2 comment(s) in response to Building a Reusable Product Search Block in Episerver 9 Part 1: Setup & Block Controller

Your Boyeee 30 Sep 16 @ 12:35 AM
Valdis Iljuconoks Says:
Hi, nice approach to unify things. However I do have sone questions: Why you need ControllerName private function in initializing module? What exactly is `SiteSettings` and how you define ProductSearch property on that? It's still a bit unclear on how exactly you are using newly registered route to the block controller.

Your Boyeee 23 Jan 17 @ 11:30 AM
john Says:
Hi Vladis, So, the private ControllerName function in the init module is really just a convenience method to call the controller. I'm not even using it in the example. Site Settings is a block on my homepage type that I use to store global settings in. It's a nice approach, because getting a reference to the homepage is really easy in the Episerver API, so these global settings can be accessed from most places. You can see how it's implemented in the github repo this article references. As for the route I registered for the product search block, it's basically taking the input and routing it to my controller called ProductSearchBlockController. You will find that Episerver doesn't know where to route a block like this unless you register it like I did. Sorry it took me so long to respond to this.

Have a comment?