As I’ve mentioned in the previous post, I’m this one is going to be dedicated for one of the features. I think that it make sense to start from a catalog, as an e-commerce implementation will heavily rely on products information all over the site.
Let’s dive into …
Catalog feature in our case covered most of the components on the site where you might see products, not including check-out: product recommendations on a home page, product listing component, product details page, etc.
As componentization is mainly driven on a site by it an end-user experience, which is a presentation layer, so let’s start here and go down to the bottom. As you might expect in Habitat you components would be assigned to pages defined on a project level, while components itself sits at a feature level – no change here. While the original storefront implementation has catalogs folder renderings, it contains few pages and functionality related to bag and wishlists, that we moved to other features.
This clean-up is done due to the fact, that e.g. if you do not have ordering capabilities on a site you still might use catalog feature.
Templates breakdown for those components is quite simple – mostly we have templates of data source type, that are used to control various properties of a component.
Front-end logic related to those components was decoupled from original scripts and injected only when the component is used.
If we take a look at a code attached to a ProductRecommendations rendering this would lead us Catalog feature back-end implementation. Over here we have quite a lite controller that rely on dependency injection to get access to products information from Commerce Server. The functionality is injected via controller constructor params instantiated by Windsor and implemented using a facade pattern (or repository as it is usually called).
|/// An action to manage data for the ProductList|
|/// <param name="pageNumber">The page number.</param>|
|/// <param name="facetValues">The facet values.</param>|
|/// <param name="sortField">The sort field.</param>|
|/// <param name="sortDirection">The sort direction.</param>|
|/// The view that represents the ProductList|
|public ActionResult MultipleProductLists(|
|[Bind(Prefix = StorefrontConstants.QueryStrings.Paging)] int? pageNumber,|
|[Bind(Prefix = StorefrontConstants.QueryStrings.Facets)] string facetValues,|
|[Bind(Prefix = StorefrontConstants.QueryStrings.Sort)] string sortField,|
|[Bind(Prefix = StorefrontConstants.QueryStrings.SortDirection)] CommerceConstants.SortDirection? sortDirection)|
|var productSearchOptions = new CommerceSearchOptions|
|NumberOfItemsToReturn = StorefrontConstants.Settings.DefaultItemsPerPage,|
|StartPageIndex = 0,|
|SortField = sortField|
|var currentRendering = RenderingContext.Current.Rendering;|
|var datasource = currentRendering.Item;|
|var viewModel = _catalogRepository.GetMultipleProductList(datasource, currentRendering, productSearchOptions);|
|return View("ProductRecommendation", viewModel);|
Using facades here is a common practice to make a controller more testable, as you could easily substitute it with mock-object. Also, this helps to remove direct references from controllers to a search API, Commerce Connect API or even direct calls to Sitecore Commerce API.
|private MultipleProductSearchResults GetMultipleProductSearchResults(BaseItem dataSource,|
|MultilistField searchesField = dataSource.Fields[Templates.ProductSearch.Fields.NamedSearches.ToString()];|
|var searches = searchesField.GetItems();|
|var productsSearchResults = new List<SearchResults>();|
|foreach (var search in searches)|
|if (TemplateManager.GetTemplate(search).GetBaseTemplates().FirstOrDefault(t => t.ID == Templates.NamedSearch.ID) != null)|
|var productsSearchResult = _catalogManager.GetProductSearchResults(search, productSearchOptions);|
|if (productsSearchResult != null)|
|productsSearchResult.NamedSearchItem = search;|
|productsSearchResult.DisplayName = search[Templates.NamedSearch.Fields.Title.ToString()];|
|else if (TemplateManager.GetTemplate(search).GetBaseTemplates().FirstOrDefault(t => t.ID == Templates.SelectedProducts.ID) != null)|
|var itemCount = 0;|
|var staticSearchList = new SearchResults|
|DisplayName = search[Templates.SelectedProducts.Fields.Title.ToString()],|
|NamedSearchItem = search|
|MultilistField productListField = search.Fields[Templates.SelectedProducts.Fields.ProductList.ToString()];|
|var productList = productListField.GetItems();|
|foreach (var productItem in productList)|
|var catalogItemtype = productItem.ItemType();|
|if (catalogItemtype == StorefrontConstants.ItemTypes.Category || catalogItemtype == StorefrontConstants.ItemTypes.Product)|
|staticSearchList.TotalItemCount = itemCount;|
|staticSearchList.TotalPageCount = itemCount;|
|return new MultipleProductSearchResults(productsSearchResults);|
The code above makes calls to managers which are a part of the foundation layer. Managers are something that we carry over from the original storefront implementation as they contain most of the logic, however, they were moved to foundation layer as they supposed to hide specifics of integration with an e-commerce platform.
Most of the work in the foundation layer was done around wrapping and decoupling direct calls to Commerce Server. This also helps to make controllers of the feature layer thinner and easier to test.
Ideally, you would have generalized API for an abstract e-commerce platform that you should be able to expose from the foundation and reference in the feature layer. The complexity of this exercise was far beyond the scope of this project. Even a possibility to create and maintain API of that kind, let’s say, for a base functionality of 5 top commerce vendors is questionable, from my point of view. I’m not even talking about all customizations that usually added to them.
In the next post, I am going to cover challenges of the implementation as well as some aspects of front-end integrations and testing.
Posts in series
- Inception: Sitecore Storefront to Habitat Migrating
- Mapping Sitecore StoreFront to Habitat
- Sitecore Catalog Feature for Habitat
Follow me on twitter @true_shoorik. Would be glad to discuss ideas above in comments.