Tutorial: Creating an EPiServer Commerce site in MVC – Part 1
In EPiServer 7.5, it has become a lot easier to create a commerce site in MVC. Commerce 7.5 uses the same routing system, and the same typed content model as the CMS, which enables the same MVC support in commerce as in CMS.
We will write a commerce MVC site from scratch in this tutorial, a site that pretty much will be a light version of the official MVC sample site. The site will be a good start point when starting a new commerce project in MVC, and when you have done the tutorial, it can be a good idea to look at the official MVC sample package to see how this code can be used.
To understand this tutorial you have to understand asp.net MVC. For beginners of MVC, I strongly recommend to go through vanilla MVC first.
Create a project
We will create a new Commerce core site through EPiServer deployment center, and use that as a starting point for the MVC site.
1. Create a new site with database using deployment center using “Install site and SQL Server database”.
2. At step 4/6, check “Install EPiServer Commerce 7.5.394.2”.
3. When a site has been created, click on the link “go to the administrative interface to configure site”.
4. Go to the “config” tab, and choose “Manage web sites”.
5. Add a web site, and set the url.
6. Set the start page to the root page.
7. Save the settings.
8. Download the file Start.zip.
9. Unzip the file, and copy all folders and files to the “wwwroot” folder of the created site.
10. Add the nuget package “EPiServer Commerce” to get the latest official build of EPiServer Commerce.
11. Add the nuget package “Microsoft ASP.NET Web Optimization Framework”.
12. Compile.
Routing
The routing system in EPiServer uses the Microsoft routing system (System.Web.Routing). In EPiServer CMS, we have added special segments called language, node, and partial route. The language segment will set the language of the page, and the node segment will set the page reference from the url. Partial route gives partners the opportunity to set the routed data to something else than the PageData. In commerce, we are using this segment to set catalog content to routed data.
By default, there are no registration of any partial route that would make it possible to route to a product. The registration has to be made by the partner developer. The registration of the commerce partial route can be done very simple, and should be done in an initialization module.
Creating an initialization module
We will use the EPiServer Visual studio integration when creating an initialization module. If you haven’t got the VS integration, you can easy install it using “extensions and updates” in visual studio. Search for “EPiServer CMS visual studio extensions”.
1. Right click on the “Business” folder and choose “Add”->”New item”.
2. In the left panel, choose “EPiServer”.
3. In the right panel, choose “Initialization Module”.
4. Click Add.
5. In the class, add a module dependency to EPiServer.Commerce.Initialization.InitializationModule. This will make sure that this module run after the core initialization module of commerce.
[InitializableModule, ModuleDependency(typeof(EPiServer.Commerce.Initialization.InitializationModule))]
public class Initialization : IInitializableModule
{
public void Initialize(InitializationEngine context)
{
}
public void Preload(string[] parameters)
{
}
public void Uninitialize(InitializationEngine context)
{
}
}
Register commerce partial route
In the “CatalogRouteHelper”, we have a helper method that lets you make the registration of the commerce partial route in one line. Use the method “MapDefaultHierarchicalRouter”, which takes the route collection in the first parameter (RouteTable.Routes). The second parameter is an optional delegate, where you can specify where the product routing will start. If this parameter isn’t used, the start page of the site will be used as starting point for the catalog route. The third argument let you enable outgoing routes for seo uri. If you want the links on your site to be created as seo uri:s, this flag should be set to true, otherwise false.
1. In the Initialize method (in the initialization module), add the registration.
public void Initialize(InitializationEngine context)
{
CatalogRouteHelper.MapDefaultHierarchialRouter(RouteTable.Routes, () => ContentReference.StartPage, false);
}
View model
My personally view on a view model is a class that contains auto properties without any logic. The view models shouldn’t use any services, they should just be very simple poco classes. To be able to have simple poco classes, we need to populate the view model properties through our controllers.
If you have looked at the Alloy MVC site in EPiServer CMS, you have seen that we have a default controller, which creates view models for all page types that hasn’t got a specific controller. The view model are created using “Activator.CreateInstance”. The “Activator.CreateInstance” is a very costly operation, se we will create our own activator, with improved performance.
IPageViewModel
We will create an interface under "Models"->"View Models", which all our pages will implement. The interface will be pretty simple. The interface will have a covariant generic, which makes it possible to cast the generic type, for example from IPageViewModel<ConcretePage> to IPageViewModel<BasePage> where ConcretePage inherits from BasePage.
The interface will contain a get property of the generic type
1. Create a new interface, and call it IPageViewModel
2. Make a generic type, which needs to inherit from PageData, and make the generic covariant (out).
public interface IPageViewModel<out T>
where T : PageData
{
T CurrentPage { get; }
}
PageViewModel
The default implementation of the IPageViewModel interface will be a very simple class, just like a view model should be in my opinion.
The default page view model will have one constructor, that takes the generic type as the only argument. We will also have a property of the generic type, that only should be readable outside of the class.
1. Create a new class under "Models->"View Models", and name it "PageViewModel".
2. Make the class generic, where the generic type is PageData.
3. Implement IPageViewModel
4. Create a constructor that takes the generic type as it's only argument
5. Create a property for the generic type
public class PageViewModel<T> : IPageViewModel<T>
where T : PageData
{
public PageViewModel(T currentPage)
{
CurrentPage = currentPage;
}
public T CurrentPage
{
get;
set;
}
}
ICatalogContentLeafViewModel
We have created an interface, and a default implementation for the page data view models. Now it's time to create an interface for catalog content. The base type for all catalog content types is “CatalogContentBase”. More concrete catalog content types are NodeContent (node), ProductContent (product) and VariationContent (variation of product).
We will start by creating a very simple interface called "ICatalogContentLeafViewModel", which inherits from the page data view model interface (IPageViewModel). This interface will be the interface that all catalog content view models will implement in our solution. By inherit from IPageViewModel, we will be able to get information about a page data object in our catalog content view model. The page data instance will most likely by the current page, which is the starting page for the catalog content route.
The interface will have two generic argument, and both will be covariant. One of the generic types will be used for a page data property, and the other for a catalog content property. The only property added to this class will be the generic catalog content. The page data property will be inherited from the IPageViewModel interface.
1. Create a new interface called "ICatalogContentLeafViewModel".
2. Add two covariant gereric types, one for CatalogContentBase, and one for PageData.
3. Inherit from IPageViewModel
4. Add a property for the catalog content generic type.
public interface ICatalogContentLeafViewModel<out T, out TPage> : IPageViewModel<TPage>
where T : CatalogContentBase
where TPage : PageData
{
T CatalogContent { get; }
}
IVariationContentViewModel
We will continue with catalog content interfaces, and create an interface called "IVariationContentViewModel". This interface will be used for variation content, and will contain the most important properties for variation content. The interface will inherit from "ICatalogContentLeafViewModel", but it will have a more specific generic catalog content type. This interface will use VariationContent as generic type for catalog content. VariationContent should always be used as base class for variations.
The properties in the interface will be inventory and price, and thay will be lazyloaded to only load the data when it's needed. The lazy loading of properties is something that will be used a lot in this project.
1. Create a new interface called "IVariationContentViewModel".
2. Add two covariant gereric types, one for VariationContent, and one for PageData.
3. Inherit from ICatalogContentLeafViewModel
4. Add a lazy inventory property.
5. Add a lazy price property.
public interface IVariationContentViewModel<out TVariationContent, out TPageData> : ICatalogContentLeafViewModel<TVariationContent, TPageData>
where TVariationContent : VariationContent
where TPageData : PageData
{
Lazy<Inventory> Inventory { get; set; }
Lazy<Price> Price { get; set; }
}
VariationContentViewModel
The default implementation of IVariationContentViewModel will be called "VariationContentViewModel". This model will inherit from PageViewModel, and implement the IVariationContentViewModel interface. By inheriting from PageViewModel, we don't need to implement the IPageViewModel properties in this class, if any exist.
1. Create a new class called "VariationContentViewModel".
2. Add two covariant gereric types, one for VariationContent, and one for PageData.
3. Inherit from PageViewModel
4. Implement IVariationContentViewModel
5. Add a catalog content property (of the generic variation content type).
6. Add a lazy inventory property.
7. Add a lazy price property.
public class VariationContentViewModel<TVariationContent, TPageData> : PageViewModel<TPageData>, IVariationContentViewModel<TVariationContent, TPageData>
where TVariationContent : VariationContent
where TPageData : PageData
{
public VariationContentViewModel(TVariationContent currentContent, TPageData currentPage)
: base(currentPage)
{
CatalogContent = currentContent;
}
public TVariationContent CatalogContent { get; private set; }
public Lazy<Inventory> Inventory { get; set; }
public Lazy<Price> Price { get; set; }
}
LazyVariationContentViewModelCollection
A product can contain several variations, and a common operation is getting the variations for a specific product. This would give us the signature IEnumerable<IVariationContentViewModel<VariationContent, PageData>>.
I wrote earlier that lazy properties will be common in this solution. A collection of variations should therefore also be lazy loaded. Therefore we need to change the signature to Lazy<IEnumerable<IVariationContentViewModel<VariationContent, PageData>>>. This signature starts to be pretty long and complex. We will create a class that inherits from this signature, just to make it easier to read in code.
1. Create a new class called "LazyVariationContentViewModelCollection".
2. Inherit from Lazy<IEnumerable<IVariationContentViewModel<VariationContent, PageData>>>.
3. Create a constructor that taked a Func<IEnumerable<IVariationContentViewModel<VariationContent, PageData>>> argument, and call the base class with the same argument.
public class LazyVariationContentViewModelCollection : Lazy<IEnumerable<IVariationContentViewModel<VariationContent, PageData>>>
{
public LazyVariationContentViewModelCollection(Func<IEnumerable<IVariationContentViewModel<VariationContent, PageData>>> valueFactory)
: base(valueFactory)
{
}
}
ICatalogContentViewModel
We have earlier created an interface called "ICatalogContentLeafViewModel", which only contained the catalog content property. We will now create a richer interface, that inherits from ICatalogContentLeafViewModel, and adds more properties to it.
The ICatalogContentViewModel will contain two lazy properties, one for child categories, and one for variants. Those properties should make it possible to get all child categories, and all variants for a specific catalog content.
1. Create a new interface called "ICatalogContentViewModel".
2. Add two covariant gereric types, one for CatalogContentBase, and one for PageData.
3. Inherit from ICatalogContentLeafViewModel
4. Add a lazy property for a node content collection.
5. Add a LazyVariationContentViewModelCollection property.
public interface ICatalogContentViewModel<out T, out TPage> : ICatalogContentLeafViewModel<T, TPage>
where T : CatalogContentBase
where TPage : PageData
{
Lazy<IEnumerable<NodeContent>> ChildCategories { get; set; }
LazyVariationContentViewModelCollection Variants { get; set; }
}
CatalogContentViewModel
The default implementation of ICatalogContentViewModel will be called "CatalogContentViewModel". This class will inherit from PageViewModel, and implement ICatalogContentViewModel.
1. Create a new class called "CatalogContentViewModel".
2. Add two covariant gereric types, one for CatalogContentBase, and one for PageData.
3. Inherit from PageViewModel
4. Implement ICatalogContentViewModel
5. Add a property for the gerneric catalog content.
6. Add a lazy property for a node content collection.
7. Add a LazyVariationContentViewModelCollection property.
public class CatalogContentViewModel<T, T1> : PageViewModel<T1>, ICatalogContentViewModel<T, T1>
where T : CatalogContentBase
where T1 : PageData
{
public CatalogContentViewModel(T currentContent, T1 currentPage) : base(currentPage)
{
CatalogContent = currentContent;
CurrentPage = currentPage;
}
public T CatalogContent { get; private set; }
public Lazy<IEnumerable<NodeContent>> ChildCategories { get; set; }
public LazyVariationContentViewModelCollection Variants { get; set; }
}
IProductContentViewModel
We will now create an interface for product content view models. This interface will inherit from ICatalogContentViewModel and change the generic restriction of the catalog content.
1. Create a new interface called "IProductContentViewModel".
2. Add two covariant gereric types, one for ProductContent, and one for PageData.
3. Inherit from ICatalogContentViewModel
public interface IProductContentViewModel<out T, out TPage> : ICatalogContentViewModel<T, TPage>
where T : ProductContent
where TPage : PageData
{
}
ProductContentViewModel
The default implementation of IProductContentViewModel will be called "ProductContentViewModel". This model will inherit from PageViewModel, and implement the IProductContentViewModel interface. By inheriting from PageViewModel, we don't need to implement the IPageViewModel properties in this class.
1. Create a new class called "ProductContentViewModel".
2. Add two covariant gereric types, one for ProductContent, and one for PageData.
3. Inherit from CatalogContentViewModel
4. Implement IProductContentViewModel
public class ProductContentViewModel<TProductContent, TPageData> : CatalogContentViewModel<TProductContent, TPageData>, IProductContentViewModel<TProductContent, TPageData>
where TProductContent : ProductContent
where TPageData : PageData
{
public ProductContentViewModel(TProductContent currentContent, TPageData currentPage)
: base(currentContent, currentPage)
{
}
}
LazyProductContentViewModelCollection
Listings of product content will be a common operation on the commerce site. Our signature for a collection of product content view models would now be IEnumerable<IProductContentViewModel<ProductContent, PageData>>. A collection of variations should also be lazy loaded. Therefore we need to change the signature to Lazy<IEnumerable<IProductContentViewModel<ProductContent, PageData>>>.
We will simplify the signature by creating a class that inherits from Lazy<IEnumerable<IProductContentViewModel<ProductContent, PageData>>>.
1. Create a new class called "LazyProductContentViewModelCollection".
2. Inherit from Lazy<IEnumerable<IProductContentViewModel<ProductContent, PageData>>>.
3. Create a constructor that taked a Func<IEnumerable<IProductContentViewModel<ProductContent, PageData>>> argument, and call the base class with the same argument.
public class LazyProductContentViewModelCollection : Lazy<IEnumerable<IProductContentViewModel<ProductContent, PageData>>>
{
public LazyProductContentViewModelCollection(Func<IEnumerable<IProductContentViewModel<ProductContent, PageData>>> valueFactory)
: base(valueFactory)
{
}
}
Update of IVariationContentViewModel and VariationContentViewModel
It can be a good idea to get the product information for the variation content. We will add a property of type LazyProductContentViewModelCollection to IVariationContentViewModel. The reason why we need to have a collection of product view models is because a variation can belong to several products.
1. Open the interface IVariationContentViewModel
2. Add a property of type LazyProductContentViewModelCollection named "ParentProducts".
3. Open the class VariationContentViewModel.
4. Implement the property ParentProducts.
public interface IVariationContentViewModel<out TVariationContent, out TPageData> : ICatalogContentLeafViewModel<TVariationContent, TPageData>
where TVariationContent : VariationContent
where TPageData : PageData
{
Lazy<Inventory> Inventory { get; set; }
Lazy<Price> Price { get; set; }
LazyProductContentViewModelCollection ParentProducts { get; set; }
}
public class VariationContentViewModel<TVariationContent, TPageData> : PageViewModel<TPageData>, IVariationContentViewModel<TVariationContent, TPageData>
where TVariationContent : VariationContent
where TPageData : PageData
{
public VariationContentViewModel(TVariationContent currentContent, TPageData currentPage)
: base(currentPage)
{
CatalogContent = currentContent;
}
public TVariationContent CatalogContent { get; private set; }
public Lazy<Inventory> Inventory { get; set; }
public Lazy<Price> Price { get; set; }
public LazyProductContentViewModelCollection ParentProducts { get; set; }
}
Update of ICatalogContentViewModel and CatalogContentViewModel
Right now, we do not have a property for getting all products for a catalog content view model. This is something we need to change. We will add a property of type LazyProductContentViewModelCollection to ICatalogContentViewModel.
1. Open the interface ICatalogContentViewModel
2. Add a property of type LazyProductContentViewModelCollection named "Products".
3. Open the class CatalogContentViewModel.
4. Implement the property Products.
public interface ICatalogContentViewModel<out T, out TPage> : ICatalogContentLeafViewModel<T, TPage>
where T : CatalogContentBase
where TPage : PageData
{
Lazy<IEnumerable<NodeContent>> ChildCategories { get; set; }
LazyProductContentViewModelCollection Products { get; set; }
LazyVariationContentViewModelCollection Variants { get; set; }
}
public class CatalogContentViewModel<T, T1> : PageViewModel<T1>, ICatalogContentViewModel<T, T1>
where T : CatalogContentBase
where T1 : PageData
{
public CatalogContentViewModel(T currentContent, T1 currentPage) : base(currentPage)
{
CatalogContent = currentContent;
CurrentPage = currentPage;
}
public T CatalogContent { get; set; }
public Lazy<IEnumerable<NodeContent>> ChildCategories { get; set; }
public LazyProductContentViewModelCollection Products { get; set; }
public LazyVariationContentViewModelCollection Variants { get; set; }
}
Activator
When having the most important view models on place, we need to decide how we can create instances of the models in a good way. The first thing that comes to mind is just to create an instance with the "new" keyword. This works when creating a specific view model, that inherits from one of the view models we have created. When we want to create a view model where the generic type is unknown at compile time, we have a problem. To support this scenario we need to have support for creating the view model in another way.
The simplest way to create a class with covariant generic types, where the generic types is unknown at compile time, is to use Activator.CreateInstance. The problem with this method is that it's very slow. We will create our own activator with compiled expressions, that will create and cache a delegate. The delegate will contain a lambda expression to instantiate a given type.
The class will contain several methods, so it can be a good idea to write the method signatures in the class before starting to implement logic.
1. Create a generic class and name it "Activator".
2. Create a delegate in the class with the generic type. Name the delegate "ObjectActivator", and have params object[] as argument for the delegate. The params will be used for constructor arguments.
3. Create a field of the type IObjectInstanceCache, that will be used to cache delegates. Name the field _objectInstanceCache.
4. Copy the method signatures to the class.
/// <summary>
/// Creates an instance of <see cref="T"/>.
/// </summary>
/// <typeparam name="T"></typeparam>
public class Activator<T>
{
private IObjectInstanceCache _objectInstanceCache;
private delegate T ObjectActivator(params object[] args);
private IObjectInstanceCache ObjectInstanceCache
{
get;
set;
}
/// <summary>
/// Creates an instance of the <see cref="T"/>.
/// </summary>
/// <param name="args">Arguments, used for constructor creation, and for creating a generic type</param>
/// <returns>The requested <see cref="T"/>.</returns>
/// <remarks>The supports generic arguments.</remarks>
public T Activate(params object[] args)
{
}
/// <summary>
/// Creates an instance of the <paramref name="type"/>.
/// </summary>
/// <param name="type">The type to create.</param>
/// <param name="constructorArguments">Arguments, used for constructor creation.</param>
/// <returns>The requested <paramref name="type"/>.</returns>
/// <remarks>The supports generic arguments.</remarks>
public T Activate(Type type, params object[] constructorArguments)
{
}
/// <summary>
/// Gets the objector activator delegate, that will be used to create the requested object.
/// </summary>
/// <param name="constructorArgumentTypes">The constructor information.</param>
/// <param name="type">The type to activate</param>
/// <returns>The delegate with instructions to create the specified type.</returns>
private static ObjectActivator GetObjectActivator(Type type, Type[] constructorArgumentTypes)
{
}
/// <summary>
/// Creates expressions for the constructor arguments.
/// </summary>
/// <param name="constructorArgumentTypes">The constructor argument types.</param>
/// <param name="delegateParameterExpression">The expression for the delegate parameter.</param>
/// <returns>Expressions for the constructor arguments.</returns>
private static IEnumerable<Expression> CreateTypeExpressions(Type[] constructorArgumentTypes, Expression delegateParameterExpression)
{
}
/// <summary>
/// Creates <see cref="ObjectActivator"/>, that will be used to create the requested type.
/// </summary>
/// <param name="constructorInfo">The constructor information.</param>
/// <param name="constructorTypeExpressions">Expressions for the constructor types.</param>
/// <param name="delegateParameterExpression">The delegate parameter expression.</param>
/// <returns>The <see cref="ObjectActivator"/></returns>
private static ObjectActivator CreateDelegate(ConstructorInfo constructorInfo, IEnumerable<Expression> constructorTypeExpressions, ParameterExpression delegateParameterExpression)
{
}
}
ObjectInstanceCache
The cache property will both have a getter and a setter. This makes it easier to unit test the class. The setter will simply set the field of the type, and the getter will check if the field has been set. If the field hasn't been set when the getter is called, the property will set the field from the service locator before returning the value.
1. In the getter, return the field _objectInstanceCache. If the field is null, set it using the service locator before returning the field.
2. In the setter, set the field to the value sent in to the property.
private IObjectInstanceCache ObjectInstanceCache
{
get
{
return _objectInstanceCache ?? (_objectInstanceCache = ServiceLocator.Current.GetInstance<ISynchronizedObjectInstanceCache>());
}
set
{
_objectInstanceCache = value;
}
}
Activate
The activate methods will be the only public methods. Those classes will compile a lambda expression if a delegate for the type hasn't been cached. The method will always cache the the delegate after it has been created using lambda expression.
After the delegate has been received, the delegate will be used to create an instance by calling it with the constructor parameters sent to the method.
1. In the Activate method with only params, call the overload of the method that also takes a Type argument. Use the generic type as type.
2. Go to the activator method with both a type argument, and params as constructor arguments. Create a variable that will get the types from the constructor arguments.
3. Create a cache key that will be a combination of the argument type, and the constructor types.
4. Try to get a delegate from the cache using the ObjectInstanceCache property, and the cache key.
5. If a delegate couldn't be found for the types, check if the type in the method argument is a generic type definition. If it is, call "MakeGenericType" with the constructor argument types on the type parameter to create the final type. Then call "GetObjectActivator" with the type, and constructor argument types to create the delegate.
6. Create an instance of the generic type by calling the delegate with the constructor arguments.
public T Activate(params object[] args)
{
return Activate(typeof(T), args);
}
public T Activate(Type type, params object[] constructorArguments)
{
var argumentTypes = constructorArguments
.Select(x => x != null ? x.GetOriginalType() : typeof(object))
.ToArray();
var cacheKey = String.Concat(type.GetHashCode(), "#", String.Join(":", argumentTypes.Select(x => x.GetHashCode())));
var objectActivator = ObjectInstanceCache.ReadThrough(cacheKey, () =>
{
var finalType = type.IsGenericTypeDefinition ? type.MakeGenericType(argumentTypes) : type;
return GetObjectActivator(finalType, argumentTypes);
}, null);
return objectActivator(constructorArguments);
}
GetObjectActivator
The method "GetObjectActivator" will create and receive an ObjectActivator by calling the methods "CreateTypeExpressions", and "CreateDelegate".
1. Create a variable, and set it to the constructor information, using "GetConstructor" with the constructor argument types on the type.
2. Create a variable, and set is to an expression parameter of type object[].
3. Call CreateTypeExpression with the constructor argument types, and the expression parameter that was just created. Store the result in a variable.
4. Call "CreateDelegate" with the constructor information, the created type expression, and the expression parameter to create a delegate.
private static ObjectActivator GetObjectActivator(Type type, Type[] constructorArgumentTypes)
{
var constructorInfo = type.GetConstructor(constructorArgumentTypes);
var delegateParameterExpression = Expression.Parameter(typeof(object[]), "args");
var typeExpressions = CreateTypeExpressions(constructorArgumentTypes, delegateParameterExpression).ToList();
return CreateDelegate(constructorInfo, typeExpressions, delegateParameterExpression);
}
CreateTypeExpressions
This method will create an expressions for the constructor argument types. Those expressions will be used to create an constructor expression later.
1. Create a loop that goes through all constructor argument types.
2. Get the position in the array for the constructor argument type, and create a constant expression using the index.
3. Create an expression for the array index using the constant expression, and the delegate paramenter expression.
4. Use the expression just created to create a convert expression. Use the constructor argument type as the target conversion type.
5. Return a collection of the convert expressions created.
private static IEnumerable<Expression> CreateTypeExpressions(Type[] constructorArgumentTypes, Expression delegateParameterExpression)
{
for (var i = 0; i < constructorArgumentTypes.Length; i++)
{
var paramType = constructorArgumentTypes[i];
var indexExpression = Expression.Constant(i);
var paramAccessorExpression = Expression.ArrayIndex(delegateParameterExpression, indexExpression);
var paramCastExpression = Expression.Convert(paramAccessorExpression, paramType);
yield return paramCastExpression;
}
}
CreateDelegate
The "CreateDelegate" method will compile a lambda expression, created using an constructor expression, and the expressions for the constructor arguments.
1. Create a constructor expression using Expression.New with the constructor information, and the constructor type expressions.
2. Create a lambda expression using the delegate type, the constructor expression, and the delegate parameter expression.
3. Compile the expression, and return the result as an ObjectActivator.
private static ObjectActivator CreateDelegate(ConstructorInfo constructorInfo, IEnumerable<Expression> constructorTypeExpressions, ParameterExpression delegateParameterExpression)
{
var constructorExpression = Expression.New(constructorInfo, constructorTypeExpressions);
var lambdaExpression = Expression.Lambda(typeof(ObjectActivator), constructorExpression, delegateParameterExpression);
return (ObjectActivator)lambdaExpression.Compile();
}
CatalogContentViewModelActivator
The activator class that just was created is a generic type creator that could be used in different scenarios. On top of the Activator class, we are going to add a more specific activator class called “CatalogContentViewModelActivator”. This class will be more specific, where you have to send in catalog content, and page data.
1. Add a new class, and name it “CatalogContentViewModelActivator”.
2. Create a generic type for the class called “TCatalogContent”, that is CatalogContentBase.
3. Create another generic type for the class called “TPage”, that is PageData.
4. Create a last generic type called “TResult”, that is ICatalogContentLeafViewModel<TCatalogContent, TPage>.
5. Inherit from the Activator class, and set the generic type in the activator class to TResult.
public class CatalogContentViewModelActivator<TCatalogContent, TPage, TResult> : Activator<TResult>
where TCatalogContent : CatalogContentBase
where TPage : PageData
where TResult : ICatalogContentLeafViewModel<TCatalogContent, TPage>
{
}
We have now created the class. In the class, we will create two methods to create an instance of the type TResult. The first method will only take catalog content, and page data as arguments.
1. Create a method called “Activate”, which takes TCatalogContent, and TPage as parameters. Make the method return TResult.
2. Return the base class implementation of the activator method, by sending the two parameters to the base method.
public TResult Activate(TCatalogContent catalogContent, TPage page)
{
return base.Activate(catalogContent, page);
}
Now we have a method that can handle concrete TResult classes. We also need a method that can handle TResult that is generic type definitions. We need to create an overload that also takes a Type as a parameter.
1. Create a method called “Activate”, which takes TCatalogContent, and TPage, and Type as parameters. Make the method return TResult.
2. Return the base class implementation of the activator method, by sending the three parameters to the base method.
public TResult Activate(TCatalogContent catalogContent, TPage page, Type viewModelType)
{
return Activate(viewModelType, catalogContent, page);
}
ContentViewModelFactory
We are going to create a class that creates instances of one of the view models. The class will set the interface properties on the view models after the view model have been created. This class will be pretty big, so we will start by copy the signatures to a new class.
1. Create a new class and name it “ContentViewModelFactory”.
2. Copy the signatures to the class, inclusive the constructor, and fields.
public class ContentViewModelFactory
{
private const string DefaultWarehouseCode = "default";
private readonly ICurrentMarket _currentMarket;
private readonly InventoryLoader _inventoryLoader;
private readonly ReadOnlyPricingLoader _readOnlyPricingLoader;
private readonly IContentLoader _contentLoader;
private readonly ILanguageSelector _languageSelector;
private readonly ILinksRepository _linksRepository;
/// <summary>
/// Initializes a new instance of the <see cref="ContentViewModelFactory"/> class.
/// </summary>
/// <param name="contentLoader">The content loader.</param>
/// <param name="languageSelector">The language selector.</param>
/// <param name="currentMarket">The current market service.</param>
/// <param name="inventoryLoader">The inventory loader.</param>
/// <param name="readOnlyPricingLoader">The read only pricing loader.</param>
/// <param name="linksRepository">The links repository.</param>
public ContentViewModelFactory(IContentLoader contentLoader, ILanguageSelector languageSelector, ICurrentMarket currentMarket, InventoryLoader inventoryLoader, ReadOnlyPricingLoader readOnlyPricingLoader, ILinksRepository linksRepository)
{
_contentLoader = contentLoader;
_languageSelector = languageSelector;
_currentMarket = currentMarket;
_inventoryLoader = inventoryLoader;
_readOnlyPricingLoader = readOnlyPricingLoader;
_linksRepository = linksRepository;
}
/// <summary>
/// Creates a page view model.
/// </summary>
/// <param name="pageData">The page data.</param>
/// <returns>A page view model instance.</returns>
public IPageViewModel<PageData> CreatePageViewModel(PageData pageData)
{
}
/// <summary>
/// Creates a page view model.
/// </summary>
/// <param name="pageData">The page data.</param>
/// <returns>A page view model instance.</returns>
public TViewModel CreatePageViewModel<TViewModel>(PageData pageData) where TViewModel : IPageViewModel<PageData>
{
}
/// <summary>
/// Creates a catalog content view model instance.
/// </summary>
/// <param name="catalogContent">The catalog content.</param>
/// <param name="pageData">The page data.</param>
/// <returns>A catalog content view model instance.</returns>
public ICatalogContentViewModel<CatalogContentBase, PageData> CreateCatalogContentViewModel(CatalogContentBase catalogContent, PageData pageData)
{
}
/// <summary>
/// Creates a catalog content view model instance.
/// </summary>
/// <param name="catalogContent">The catalog content.</param>
/// <param name="pageData">The page data.</param>
/// <returns>A catalog content view model instance.</returns>
public TViewModel CreateCatalogContentViewModel<TViewModel>(CatalogContentBase catalogContent, PageData pageData) where TViewModel : ICatalogContentViewModel<CatalogContentBase, PageData>
{
}
/// <summary>
/// Creates a product content view model instance.
/// </summary>
/// <param name="productContent">The product content.</param>
/// <param name="pageData">The page data.</param>
/// <returns>A catalog content view model instance.</returns>
public IProductContentViewModel<ProductContent, PageData> CreateProductContentViewModel(ProductContent productContent, PageData pageData)
{
}
/// <summary>
/// Creates a product content view model instance.
/// </summary>
/// <param name="productContent">The product content.</param>
/// <param name="pageData">The page data.</param>
/// <returns>A catalog content view model instance.</returns>
public TViewModel CreateProductContentViewModel<TViewModel>(ProductContent productContent, PageData pageData) where TViewModel : IProductContentViewModel<ProductContent, PageData>
{
}
/// <summary>
/// Creates a product content view model instance.
/// </summary>
/// <param name="variationContent">The variation content.</param>
/// <param name="pageData">The page data.</param>
/// <returns>A variation content view model instance.</returns>
public IVariationContentViewModel<VariationContent, PageData> CreateVariationContentViewModel(VariationContent variationContent, PageData pageData)
{
}
/// <summary>
/// Creates a product content view model instance.
/// </summary>
/// <param name="variationContent">The variation content.</param>
/// <param name="pageData">The page data.</param>
/// <returns>A variation content view model instance.</returns>
public TViewModel CreateVariationContentViewModel<TViewModel>(VariationContent variationContent, PageData pageData) where TViewModel : IVariationContentViewModel<VariationContent, PageData>
{
}
/// <summary>
/// Sets the page data view model properties.
/// </summary>
/// <typeparam name="TViewModel">The type of the view model.</typeparam>
/// <param name="model">The model.</param>
/// <remarks>This method will always override the property values.</remarks>
public void InitializePageViewModel<TViewModel>(TViewModel model)
where TViewModel : IPageViewModel<PageData>
{
}
/// <summary>
/// Sets the catalog content view model properties.
/// </summary>
/// <typeparam name="TViewModel">The type of the view model.</typeparam>
/// <param name="model">The model.</param>
/// <remarks>This method will always override the property values.</remarks>
public void InitializeCatalogContentViewModel<TViewModel>(TViewModel model)
where TViewModel : ICatalogContentViewModel<CatalogContentBase, PageData>
{
}
/// <summary>
/// Sets the variation content view model properties.
/// </summary>
/// <typeparam name="TViewModel">The type of the view model.</typeparam>
/// <param name="model">The model.</param>
/// <remarks>This method will always override the property values.</remarks>
public void InitializeVariationContentViewModel<TViewModel>(TViewModel model)
where TViewModel : IVariationContentViewModel<VariationContent, PageData>
{
}
/// <summary>
/// Sets the variation content view model properties.
/// </summary>
/// <typeparam name="TViewModel">The type of the view model.</typeparam>
/// <param name="model">The model.</param>
/// <param name="warehouseCode">The warehouse code.</param>
public void InitializeVariationContentViewModel<TViewModel>(TViewModel model, string warehouseCode)
where TViewModel : IVariationContentViewModel<VariationContent, PageData>
{
}
/// <summary>
/// Gets a lazy property, containing child content.
/// </summary>
/// <param name="contentLink">The content link.</param>
/// <returns>Lazy property, containing child content.</returns>
private Lazy<IEnumerable<NodeContent>> GetChildNodes(ContentReference contentLink)
{
}
/// <summary>
/// Gets the inventory.
/// </summary>
/// <param name="stockPlacement">The stock placement.</param>
/// <param name="warehouseCode">The warehouse code.</param>
/// <returns>The inventory.</returns>
private Lazy<Inventory> GetInventory(IStockPlacement stockPlacement, string warehouseCode)
{
}
/// <summary>
/// Gets the prices.
/// </summary>
/// <param name="pricing">The pricing.</param>
/// <returns>The prices.</returns>
private Lazy<Price> GetPrices(IPricing pricing)
{
}
/// <summary>
/// Creates a lazy property, containing product content view models.
/// </summary>
/// <param name="catalogContent">Tha catalog content.</param>
/// <param name="pageData">The page data.</param>
/// <returns>Lazy property, containing product content view models.</returns>
private LazyProductContentViewModelCollection CreateLazyProductContentViewModels(CatalogContentBase catalogContent, PageData pageData)
{
}
/// <summary>
/// Creates a lazy property, containing variant content view models.
/// </summary>
/// <param name="catalogContent">The catalog content.</param>
/// <param name="pageData">The page data.</param>
/// <returns>Lazy property, containing variant content view models.</returns>
private LazyVariationContentViewModelCollection CreateLazyVariantContentViewModels(CatalogContentBase catalogContent, PageData pageData)
{
}
/// <summary>
/// Gets the children and related entries.
/// </summary>
/// <typeparam name="TEntryContent">The entry content type.</typeparam>
/// <param name="catalogContent">The catalog content.</param>
/// <returns>The children and related entries.</returns>
private IEnumerable<TEntryContent> GetChildrenAndRelatedEntries<TEntryContent>(CatalogContentBase catalogContent)
where TEntryContent : EntryContentBase
{
}
/// <summary>
/// Gets the variants.
/// </summary>
/// <param name="content">The variant container.</param>
/// <returns>The variants.</returns>
private IEnumerable<TEntryContent> GetRelatedEntries<TEntryContent>(IVariantContainer content)
where TEntryContent : EntryContentBase
{
}
/// <summary>
/// Gets a lazy property containing parent product view models.
/// </summary>
/// <param name="content">The variation content.</param>
/// <param name="pageData">The page data object.</param>
/// <returns>A lazy property containing parent product view models.</returns>
private LazyProductContentViewModelCollection GetParentProducts(VariationContent content, PageData pageData)
{
}
}
CreatePageViewModel
We are going to start with the method that will create a PageViewModel instance. The method will take a PageData argument, and the created PageViewModel will be created with the correct generic type.
1. Go to the CreatePageViewModel method.
2. Create an instance of the Activator class in the method. The generic type should be IPageViewModel<PageData>.
3. Create an PageViewModel instance by calling the Acivate method on the instance, with the method argument (PageData), and typeof(PageViewModel<>).
4. Call the method “InitializePageViewModel” with the model view.
5. Return the model
public IPageViewModel<PageData> CreatePageViewModel(PageData pageData)
{
var activator = new Activator<IPageViewModel<PageData>>();
var model = activator.Activate(typeof (PageViewModel<>), pageData);
InitializePageViewModel(model);
return model;
}
We will also implement the typed version of the method in the same way. This method can be used when a concrete type are going to be instantiated.
1. Go to the typed CreatePageViewModel method.
2. Create an instance of the Activator class in the method.
3. Create an instance of the generic type by calling the Acivate method on the instance, with the method argument (PageData).
4. Call the method “InitializePageViewModel” with the model view.
5. Return the model
public TViewModel CreatePageViewModel<TViewModel>(PageData pageData) where TViewModel : IPageViewModel<PageData>
{
var activator = new Activator<TViewModel>();
var model = activator.Activate(pageData);
InitializePageViewModel(model);
return model;
}
CreateCatalogContentViewModel
The first catalog method we are going to implement is CreateCatalogContentViewModel, which will create an instance of CatalogContentViewModel. The method will populate the properties “ChildCategories”, “Products”, and “Variants” with data before receiving the instance of ICatalogContentViewModel.
1. In the method “CreateCatalogContentViewModel” that returns the interface ICatalogContentViewModel<CatalogContentBase, PageData>, create an instance of the CatalogContentViewModelActivator class with the generic types CatalogContentBase, PageData, and ICatalogContentViewModel<CatalogContentBase, PageData>.
2. Create a model view variable, and set the result from the method “Activate” on the CatalogContentViewModelActivator instance. Use the parameters catalogContent, pageData, and typeof(CatalogContentViewModel<,>) when calling “Activate”.
3. Call the method “InitializeCatalogContentViewModel” with the model variable, the catalog content, and the page data.
4. Return the model.
public ICatalogContentViewModel<CatalogContentBase, PageData> CreateCatalogContentViewModel(CatalogContentBase catalogContent, PageData pageData)
{
var activator = new CatalogContentViewModelActivator<CatalogContentBase, PageData, ICatalogContentViewModel<CatalogContentBase, PageData>>();
var model = activator.Activate(typeof(CatalogContentViewModel<,>), catalogContent, pageData);
InitializeCatalogContentViewModel(model);
return model;
}
We will also implement the typed version of the method in the same way. This method can be used when a concrete type are going to be instantiated.
1. Go to the typed CreateCatalogContentViewModel method.
2. Create an instance of the Activator class in the method.
3. Create an instance of the generic type by calling the Acivate method on the instance, with the method arguments (CatalogContentBase and PageData).
4. Call the method “InitializeCatalogContentViewModel” with the model view.
5. Return the model
public TViewModel CreateCatalogContentViewModel<TViewModel>(CatalogContentBase catalogContent, PageData pageData) where TViewModel : ICatalogContentViewModel<CatalogContentBase, PageData>
{
var activator = new Activator<TViewModel>();
var model = activator.Activate(catalogContent, pageData);
InitializeCatalogContentViewModel(model);
return model;
}
CreateProductContentViewModel
In the same way as we created the CatalogContentViewModel, we going to create the ProductContentViewModel in the method “CreateProductContentViewModel”, which return the interface IProductContentViewModel<ProductContent,PageData>.
1. In the method “CreateProductContentViewModel” that returns the interface IProductContentViewModel<ProductContent, PageData>, create an instance of the CatalogContentViewModelActivator class with the generic types ProductContent, PageData, and IProductContentViewModel<ProductContent, PageData>.
2. Create a model view variable, and set it the result from the method “Activate” on the CatalogContentViewModelActivator instance. Use the parameters productContent, pageData, and typeof(ProductContentViewModel<,>) when calling “Activate”.
3. Call the method “InitializeCatalogContentViewModel” with the model variable, the catalog content, and the page data.
4. Return the model variable.
public IProductContentViewModel<ProductContent, PageData> CreateProductContentViewModel(ProductContent productContent, PageData pageData)
{
var activator = new CatalogContentViewModelActivator<ProductContent, PageData, IProductContentViewModel<ProductContent, PageData>>();
var model = activator.Activate(typeof(ProductContentViewModel<,>), productContent, pageData);
InitializeCatalogContentViewModel(model);
return model;
}
We will also implement the typed version of the method in the same way. This method can be used when a concrete type are going to be instantiated.
1. Go to the typed CreateProductContentViewModelmethod.
2. Create an instance of the Activator class in the method.
3. Create an instance of the generic type by calling the Activate method on the instance, with the method arguments (ProductContent and PageData).
4. Call the method “InitializeCatalogContentViewModel” with the model view.
5. Return the model
public TViewModel CreateProductContentViewModel<TViewModel>(ProductContent productContent, PageData pageData) where TViewModel : IProductContentViewModel<ProductContent, PageData>
{
var activator = new Activator<TViewModel>();
var model = activator.Activate(productContent, pageData);
InitializeCatalogContentViewModel(model);
return model;
}
CreateVariationContentViewModel
This method will return IProductContentViewModel<ProductContent,PageData>, with it’s interface properties populated.
1. Create an instance of the CatalogContentViewModelActivator class with the generic types VariationContent, PageData, and IVariationContentViewModel<VariationContent, PageData>.
2. Create a model view variable, and set it the result from the method “Activate” on the CatalogContentViewModelActivator instance. Use the parameters variationContent, pageData, and typeof(VariationContentViewModel<,>) when calling “Activate”.
3. Call the method “InitializeVaraiationContentViewModel” with the model variable, the catalog content, and the page data.
4. Return the model variable.
public IVariationContentViewModel<VariationContent, PageData> CreateVariationContentViewModel(VariationContent variationContent, PageData pageData)
{
var activator = new CatalogContentViewModelActivator<VariationContent, PageData, IVariationContentViewModel<VariationContent, PageData>>();
var model = activator.Activate(typeof(VariationContentViewModel<,>), variationContent, pageData);
InitializeVariationContentViewModel(model);
return model;
}
We will also implement the typed version of the method in the same way. This method can be used when a concrete type are going to be instantiated.
1. Go to the typed CreateVariationContentViewModel.
2. Create an instance of the Activator class in the method.
3. Create an instance of the generic type by calling the Acivate method on the instance, with the method arguments (VariationContentand PageData).
4. Call the method “InitializeVaraiationContentViewModel” with the model view.
5. Return the model
public TViewModel CreateVariationContentViewModel<TViewModel>(VariationContent variationContent, PageData pageData) where TViewModel : IVariationContentViewModel<VariationContent, PageData>
{
var activator = new Activator<TViewModel>();
var model = activator.Activate(variationContent, pageData);
InitializeVariationContentViewModel(model);
return model;
}
InitializePageViewModel
This method will be empty for now, but can be used to set properties on the IPageViewModel interface.
InitializeCatalogContentViewModel
This method should set the properties on the “ICatalogContentViewModel<T,TPage>” interface.
1. Set “ChildCategories” by calling GetChildNodes with the given catalog content link.
2. Set “Products” by calling “CreateLazyProductContentViewModels” with the given catalog content, and page data.
3. Set “Variants” by calling “CreateLazyVariantContentViewModels” with the given catalog content, and page data.
public void InitializeCatalogContentViewModel<TViewModel>(TViewModel model)
where TViewModel : ICatalogContentViewModel<CatalogContentBase, PageData>
{
model.ChildCategories = GetChildNodes(model.CatalogContent.ContentLink);
model.Products = CreateLazyProductContentViewModels(model.CatalogContent, model.CurrentPage);
model.Variants = CreateLazyVariantContentViewModels(model.CatalogContent, model.CurrentPage);
}
InitializeVaraiationContentViewModel
There are two methods with the name “InitializeVariationContentViewModel”, one without and one with a warehouseCode argument. Those methods should set the two properties on the “IVariationContentViewModel<TVariationContent,TPage>” interface.
1. In the method without warehouseCode, call the method with warehouseCode with the DefaultWarehouseCode.
2. Set “Inventory” property by creating a lazy instance, that calls “GetInventory” with the given catalog content, and warehouseCode in its delegate. The lazy property do not need to be thread safe.
3. Set “Price” property by creating a lazy instance, which calls “GetPrices” with the given catalog content in its delegate. The lazy property do not need to be thread safe.
4. Set Parent products using the method GetParentProducts.
public void InitializeVariationContentViewModel<TViewModel>(TViewModel model)
where TViewModel : IVariationContentViewModel<VariationContent, PageData>
{
InitializeVariationContentViewModel(model, DefaultWarehouseCode);
}
public void InitializeVariationContentViewModel<TViewModel>(TViewModel model, string warehouseCode)
where TViewModel : IVariationContentViewModel<VariationContent, PageData>
{
model.Inventory = GetInventory(model.CatalogContent, warehouseCode);
model.Price = GetPrices(model.CatalogContent);
model.ParentProducts = GetParentProducts(model.CatalogContent, model.CurrentPage);
}
GetChildNodes
GetChildNodes should return a lazy instance, containing a collection of child nodes for the content reference received as a parameter.
1. Return a typed lazy collection instance, and use the content loader to get the children for the content link of type NodeContent.
private Lazy<IEnumerable<NodeContent>> GetChildNodes(ContentReference contentLink)
{
return new Lazy<IEnumerable<NodeContent>>(() =>
_contentLoader.GetChildren<NodeContent>(contentLink)
.ToList());
}
GetInventory
This method should return the inventory for the given content in the specific warehouse.
1. Get the inventory by calling “GetStockPlacement” on the content parameter.
2. Return the item that has the same warehouse code as the second parameter. Null should be removed if no inventory was found for the warehouse.
private Lazy<Inventory> GetInventory(IStockPlacement stockPlacement, string warehouseCode)
{
return new Lazy<Inventory>(() => stockPlacement.GetStockPlacement(_inventoryLoader)
.FirstOrDefault(x => x.WarehouseCode == warehouseCode), true);
}
GetPrices
This method should return the prices for the given content in the current market.
1. Get the prices by calling “GetPrices” on the parameter, and return the price where the market is the same as the current market.
2. Get the current market by using the method “GetCurrentMarket” on the field “_currentMarket”.
3. Return null if no price was found on the current market.
private Lazy<Price> GetPrices(IPricing pricing)
{
return new Lazy<Price>(() => pricing.GetPrices(_readOnlyPricingLoader)
.FirstOrDefault(x => x.MarketId == _currentMarket.GetCurrentMarket().MarketId), true);
}
CreateLazyProductContentViewModels
The method “CreateLazyProductContentViewModels” should return a lazy instance, containing product content view models. The modules should be created by finding all products for the specific catalog content, and create a product content view model for each one of them.
1. Create a new instance of “LazyProductContentViewModelCollection”.
2. In the delegate of the instance that was created, create a variable that will receive all product children and related products. Get the data by calling GetChildrenAndRelatedEntries<ProductContent> with the catalog content as parameter.
3. Create a product view model for each item in the variable created, by calling CreateProductContentViewModel.
4. Return the lazy instance.
private LazyProductContentViewModelCollection CreateLazyProductContentViewModels(CatalogContentBase catalogContent, PageData pageData)
{
return new LazyProductContentViewModelCollection(() =>
{
var products = GetChildrenAndRelatedEntries<ProductContent>(catalogContent);
return products.Select(x => CreateProductContentViewModel(x, pageData));
});
}
CreateLazyVariantContentViewModels
The method “CreateLazyVariationContentViewModels” should return a lazy instance, containing variation content view models. The modules should be created by finding all variations for the specific catalog content, and create a variation content view model for each one of them.
1. Create a new instance of “LazyVariationContentViewModelCollection”.
2. In the delegate of the instance that was created, create a variable that will receive all variation children and related variants. Get the data by calling GetChildrenAndRelatedEntries<VariationContent> with the catalog content as parameter.
3. Create a variation view model for each item in the variable created in, by calling CreateVariationContentViewModel.
4. Return the lazy instance.
private LazyVariationContentViewModelCollection CreateLazyVariantContentViewModels(CatalogContentBase catalogContent, PageData pageData)
{
return new LazyVariationContentViewModelCollection(() =>
{
var variants = GetChildrenAndRelatedEntries<VariationContent>(catalogContent);
return variants.Select(x => CreateVariationContentViewModel(x, pageData));
});
}
GetChildrenAndRelatedEntries
In this method, we want to get all the children of a specific type for the given catalog content. We also want to get all the related entries of the same type for the content.
1. Create a variable, and set by getting all children of the generic type for the given content.
2. Try to cast the parameter to IVaraiantContainer.
3. If the parameter implements IVariantContainer, add related entries to the variable by calling “GetRelatedEntries” with the parameter.
4. Return the items that are available in current market. Filter the items in the variable by calling “IsAvailableInCurrentMarket” for every item.
private IEnumerable<TEntryContent> GetChildrenAndRelatedEntries<TEntryContent>(CatalogContentBase catalogContent)
where TEntryContent : EntryContentBase
{
var variantContentItems = _contentLoader.GetChildren<TEntryContent>(catalogContent.ContentLink).ToList();
var variantContainer = catalogContent as IVariantContainer;
if (variantContainer != null)
{
variantContentItems.AddRange(GetRelatedEntries<TEntryContent>(variantContainer));
}
return variantContentItems.Where(e => e.IsAvailableInCurrentMarket(_currentMarket));
}
GetRelatedEntries
“GetRelatedEntries” should receive the related entries of the generic type for the given content.
1. Create a variable, and set it by calling “GetVariantRelations” on the parameter. Make a projection of the result of “GetVariantRelations” to only receive the target property before setting the variable.
2. Call “GetItems” on the content loader with the variable, and the language selector, and return the items that is of the generic type.
private IEnumerable<TEntryContent> GetRelatedEntries<TEntryContent>(IVariantContainer content)
where TEntryContent : EntryContentBase
{
var relatedItems = content.GetVariantRelations(_linksRepository).Select(x => x.Target);
return _contentLoader.GetItems(relatedItems, _languageSelector).OfType<TEntryContent>();
}
GetParentProductViewModels
We will get the parent product for a variation content. A variation content can have several parent products, and therefore we need to return a collection of parent products.
1. Create a variable that gets the parent relations through the links repository. Call the method “GetRelationsByTarget”, and use the content link from the method argument
2. Get the items by calling “GetItems” on the content loader with the source content link from the result.
3. Filter the result to only return ProductContent.
4. Create a product content view model for each product content received.
5. Create an instance of LazyProductContentViewModelCollection, and set the valueFactory to the view models.
private LazyProductContentViewModelCollection GetParentProducts(VariationContent content, PageData pageData)
{
var relatedItems = _linksRepository.GetRelationsByTarget(content.ContentLink).Select(x => x.Source);
var products = _contentLoader.GetItems(relatedItems, _languageSelector).OfType<ProductContent>();
return new LazyProductContentViewModelCollection(() => products.Select(x => CreateProductContentViewModel(x, pageData)));
}
Controllers
We will create base controllers for page data, and catalog content. We will also create default controllers for the both content types, which will be used if there's no specific controller for the routed content.
SitePageController
We will start by creating an abstract controller, that will inherit from PageController. The controller will contain properties to the ContentViewModelFactor. It will also contain a function, that will return the path to a view.
The "GetViewPath" method will look for a view that is specific for the current page (for example StandardPage), by looking for a view at "~/Views/StandardPage/Index.cshtml". If the folder or view do not exist, the property will fall back to call the default view for the controller.
1. Create a generic class in the controller folder called "SitePageController<TPage>", where TPage is PageData. Make the class abstract.
2. Inherit from PageController.
3. Add a property of type ContentViewModelFactory, and make the setter private.
4. Add a constructor that takes ContentViewModelFactory as argument. Set the property created earlier to the parameter value.
5. Add a virtual method named "GetViewPath", that will return a string.
6. Add a variable, and set it to String.Format("~/Views/{0}/Index.cshtml", currentPage.GetOriginalType().Name). We will use "GetOriginalType" to get the correcty type for currentPage. If we use "GetType", we will get a proxy class instead of the compiled page type.
7. Check if a file exists for the path created. If we do not find any file, we will return "Index". This will make the view engine look for a folder with the same name as the controller, and a view called index.
public abstract class SitePageController<TPage> : PageController<TPage>
where TPage : PageData
{
protected SitePageController(ContentViewModelFactory contentViewModelFactory)
{
ContentViewModelFactory = contentViewModelFactory;
}
public ContentViewModelFactory ContentViewModelFactory { get; private set; }
public virtual string GetViewPath(PageData currentPage)
{
var virtualPath = String.Format("~/Views/{0}/Index.cshtml", currentPage.GetOriginalType().Name);
if (System.IO.File.Exists(Request.MapPath(virtualPath)))
{
return virtualPath;
}
return "Index";
}
}
DefaultPageController
We will create a default page controller in our project. The controller will be used by all page types that do not have a specific controller. This will work, because we will add the attribute "TemplateDescriptor" to the class with the property "Inherited" set to true. This controller will not be used for catalog content types, only for normal page types.
1. Create a class in the controller folder called "DefaultPageController".
2. Inherit from SitePageController.
3. Add the TemplateDescriptor attribute to the class, and set the property "Inherited" to true.
6. Add a constructor that takes ContentViewModelFactory as argument. Call the base class with the arguments.
7. Create a default constructor, and call the base constructor. Use the service locator to get the registered instances of ContentViewModelFactory.
[TemplateDescriptor(Inherited = true)]
public class DefaultPageController : SitePageController<PageData>
{
public DefaultPageController()
: base(ServiceLocator.Current.GetInstance<ContentViewModelFactory>())
{
}
public DefaultPageController(ContentViewModelFactory viewModelFactory)
: base(viewModelFactory)
{
}
}
Index
Inside the default action in the controller (index), we will use the GetViewPath method in the base class to get the path to the view we want to call.
The index action will take a parameter of type PageData with the name "currentPage". By having a property with the "magic" name currentPage, the content model binder will bind the parameter to the stored page data object. We will now have to create a view model of the page data, and this will be easy using the ContentViewModelFactory. Just call the method "CreatePageViewModel" on an instance of a ContentViewModelFactory, and a view model of type PageViewModel will be created with correct generic type. The properties on the IPageViewModel interface will be set when calling CreatePageViewModel.
1. Create a public method of type ViewResult, and name it "Index".
2. Add a parameter to the method of type PageData, called "currentPage".
3. Create a view model by calling "CreatePageViewModel" on the content view model factory property.
4. Call View, using the GetViewPath method, and the view model. Return the result.
public ViewResult Index(PageData currentPage)
{
var virtualPath = GetViewPath(currentPage);
var model = ContentViewModelFactory.CreatePageViewModel(currentPage);
return View(virtualPath, model);
}
SiteCatalogContentController
Just like for page data, we will create a base controller for catalog content. We will inherit from ContentController instead of PageController this time, because catalog content is not a page data object. We will also add the "RequireClientResources" attribute to the controller to enable client resources, which isn’t done in the ContentController.
1. Create a generic class in the controller folder called "SiteCatalogContentController<TCatalogContent>", where TCatalogContent is CatalogContentBase. Make the class abstract.
2. Inherit from ContentController.
3. Add the RequireClientResources attribute to the class.
4. Create a property of type ContentViewModelFactory.
5. Add a constructor that takes ContentViewModelFactory as argument. Set the property created earlier to the parameter value.
6. Add a virtual method named "GetViewPath", that returns a string.
7. Create a variable, and set the value to String.Format("~/Views/{0}/Index.cshtml", currentContent.GetOriginalType().Name). We will use "GetOriginalType" to get the correcty type for currentContent. If we use "GetType", we will get a proxy class instead of the compiled page type.
8. Check if a file exists for the path created. If we do not find any file, we will use "String.Format("~/Views/{0}/Index.cshtml", Enum.GetName(typeof(CatalogContentType), currentContent.ContentType))". The fallback will make the view engine look for a folder with the same name as the current CatalogContentType, and a view called index.
public abstract class SiteCatalogContentController<TCatalogContent> : ContentController<TCatalogContent>
where TCatalogContent : CatalogContentBase
{
protected SiteCatalogContentController(ContentViewModelFactory viewModelFactory)
{
ContentViewModelFactory = viewModelFactory;
}
protected virtual ContentViewModelFactory ContentViewModelFactory { get; private set; }
protected virtual string GetViewPath(CatalogContentBase currentContent)
{
var virtualPath = String.Format("~/Views/{0}/Index.cshtml", currentContent.GetOriginalType().Name);
if (System.IO.File.Exists(Request.MapPath(virtualPath)))
{
return virtualPath;
}
return String.Format("~/Views/{0}/Index.cshtml", Enum.GetName(typeof(CatalogContentType), currentContent.ContentType));
}
}
DefaultCatalogContentController
Just like the DefaultPageController, we will create a default controller for the content type, but this time the content type will be catalog content. We will inherit from SiteCatalogContetnController.
1. Create a class in the controller folder called "DefaultCatalogContentController".
2. Inherit from SiteCatalogContentController<CatalogContentBase>.
3. Add the TemplateDescriptor attribute to the class, and set the property "Inherited" to true.
4. Add a constructor that takes ContentViewModelFactory as argument. Call the base class with the argument.
5. Create a default constructor, and call the other constructor. Use the service locator to get the registered instances of ContentViewModelFactory.
[TemplateDescriptor(Inherited = true)]
[RequireClientResources]
public class DefaultCatalogContentController : SiteCatalogContentController<CatalogContentBase>
{
public DefaultCatalogContentController()
: base(ServiceLocator.Current.GetInstance<ContentViewModelFactory>())
{
}
public DefaultCatalogContentController(ContentViewModelFactory viewModelFactory)
: base(viewModelFactory)
{
}
}
Index
Like the DefaultPageController, we will create an index action, where we will look for a view with the folder name like the content type. This time will the content type be CatalogContentBase instead of PageData. Another difference is that we will use two parameters for the method, one for CatalogContentBase, and one for PageData. Both parameters will be set by the content model binder if the arguments are named "currentContent", and "currentPage". When creating a view model, the method CreateCatalogContentViewModel will be used on the CatalogContentViewModelFactory property.
1. Create a public method of type ViewResult, and name it "Index".
2. Add a parameter to the method of type CatalogContentBase, called "currentContent".
3. Add another parameter to the method of type PageData, called "currentPage".
4. Create a view model by calling "CreateCatalogContentViewModel" on the content view model factory property.
5. Call View with the GetViewPath method, and the view model. Return the result.
public ViewResult Index(CatalogContentBase currentContent, PageData currentPage)
{
var virtualPath = GetViewPath(currentContent);
var model = ContentViewModelFactory.CreateCatalogContentViewModel(currentContent, currentPage);
return View(virtualPath, model);
}
VIEWS
We will now create the most important views that will be used when no specific controller takes care of the content route. I will only create the views, and set the view model for them. It’s up to you to do whatever you want with the data in the model.
Layout
The layout file should have the model set to something that all view models inherits from, or implements. We will use IPageViewModel<PageData> as the view model.
1. Open _Layout.cshtml
2. Set the model to IPageViewModel<PageData>
DefaultPage
We will create the default view for the DefaultPageController. This view will use IPageViewModel<PageData> as it's model.
1. Create a folder under "Views" and name it "DefaultPage".
2. In the folder, create a razor called "Index".
3. Set the model to IPageViewModel<PageData>
Catalog
We will continue with the default view for catalogs. This view will use ICatalogContentViewModel<CatalogContent,PageData> as it's model.
1. Create a folder under "Views" and name it "Catalog".
2. In the folder, create a razor called "Index".
3. Set the model to ICatalogContentViewModel<CatalogContent,PageData>
CatalogEntryNode
The node view will use ICatalogContentViewModel<NodeContent,PageData> as it's model.
1. Create a folder under "Views" and name it "CatalogNode".
2. In the folder, create a razor called "Index".
3. Set the model to ICatalogContentViewModel<NodeContent,PageData>
CatalogEntry
The entry view will use ICatalogContentViewModel<EntryContentBase,PageData> as it's model.
1. Create a folder under "Views" and name it "CatalogEntry".
2. In the folder, create a razor called "Index".
3. Set the model to ICatalogContentViewModel<EntryContentBase,PageData>
Use the project
The project we have created is very generic for an EPiServer Commerce MVC site. I will write an example how this project can be used in my next blog post.
EPIC
This is an awesome blog post Jonas! Thank you!
Much needed tutorial, Thanks!
HI, When using Episerver 7.5 together with Commerce and Community I get 404 pages on my Product and catalog pages.
Any idea?
Jon
Jonathan: Is it PageData content types that represents products like they had to be in Commerce 7? If so, we do not support that anymore. All products must inherit from ProductContent, and has to have the catalogcontenttype attribute on it. Hope that helps.
please upload Step by step installation of episerver in you tube as soon as possible
when video is uploaded please inform by mail
Thanks & Regards
Swetha Kumar
With this approach you don't get the ability to create nodes, entrys etc using the CMS Catalog GUI so the only way is to create them using Commerce manager, correct? You can of course create new CatalogContentType models but in the project we are setting up at the moment the product information will come from Perfion and the more generic approach layed out in this article is more preferably. But is there a way to have different MetaClasses for Products, Variations and Bundles created in Commerce manager and have different Views for those in MVC without creating the strongly typed models for them? Or how do you separate a ProductContentViewModel from a VariationContentViewModel on the CatalogEntry View?
Is the idea behind the setup that you present in this article not to create the strongly typed models or is it the expected next step in the development of a Commerce 7.5 site based on this foundation? And how do you display the MetaFields values from the Entry MetaClass in the CatalogEntry View when you dont fall back to a specific model?
Following this tutorial on a Commerce 8.4.0 site I get an exception on the registration of the commerce partial routing.
"Content with id -1073741823__CatalogContent was not found", where the contentLoaded can't get the root of the catalog. What have I missed?
Same error here. Have based a previous site on this tutorial without issues but now I tried to setup a new site and updated to last version and get the same error: Content with id -1073741823__CatalogContent was not found
Hey!
First time I'm setting up a Commerce solution from scratch so I might be way off but isn't there a bug in the GetRelatedEntries()?
The version shown here works for variations, as it calls content.GetVariantRelations(), but wont work for Related products.
But GetRelatedEntries() is used in both CreateLazyProductContentViewModels() and CreateLazyVariantContentViewModels().
Just wondering if I'm doing something wrong when not getting related products with the CreateLazyProductContentViewModels or not....
This blog post is pretty old now, and there might be some problems with the newest versions.
Sorry for not replying to the questions. I don't get any mail when a question has been raised, and I havn't looking in to my old blog posts for quite some time.
I would recommend you all to start looking at the Quicksilver project instead of this blog post. That is something that we in the Commerce team has been working on for quite some time.
This error "Content with id -1073741823__CatalogContent was not found" occured when I mistakenly used attribute parameter
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
instead of
[ModuleDependency(typeof(EPiServer.Commerce.Initialization.InitializationModule))]
on the InitializationModule class.