donderdag 8 december 2011

E-Commerce Services - Storing the configuration settings outside home node

Sitecore has a great E-Commerce module (They just released their latest version that runs under Sitecore 6.5 called E-Commerce Services). When running multiple sites you probably want each site to have it's own configuration items. The default setup isn't ideal for this situation as it forces you to store the configuration item under the /home node of your website. If you're not going to setup any presentation details for these items it makes no sense to store them under the /home node.

How does it work?
Well, first of all E-Commerce Services is configured using "site" settings and "business" settings. Site settings are general settings for your site like: links to item locations, design, defaults, formats etc. Whilst business settings define things like VAT regions, Currencies, Order statusses etc. Business settings can be placed anywhere you like because they are linked under site settings. The site settings itself however are resolved through a pipeline called "getConfiguration"

The getConfiguration pipeline
The configuration pipeline is configured in the Sitecore.Ecommerce.config file and looks like this:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
      <getConfiguration>
        <processor type="Sitecore.Ecommerce.Pipelines.GetConfiguration.GetFromContextSite, Sitecore.Ecommerce.Kernel" />
        <processor type="Sitecore.Ecommerce.Pipelines.GetConfiguration.GetFromWebSite, Sitecore.Ecommerce.Kernel" />
        <processor type="Sitecore.Ecommerce.Pipelines.GetConfiguration.GetFromLinkManager, Sitecore.Ecommerce.Kernel" />
        <processor type="Sitecore.Ecommerce.Pipelines.GetConfiguration.GetFromResolver, Sitecore.Ecommerce.Kernel" />
      </getConfiguration>
  </sitecore>
</configuration>


The problem lies in the fact that the GetFromContextSite processor gets the Site settings item based on the StartPath property of the site. This points to the /sitecore/content/Sitename/home item of the website and not the /sitecore/content/Sitename item. The module allows for the <site /> definition to contain a EcommerceSiteSettings attribute to be specified which defaults to "/Site Settings" in the processor. But as said this is relative to the StartPath property and not the ContentStartPath property.

The solution
The solution to the problem is very simple. I created a custom processor that retrieves the Site Settings relative to the ContentStartPath property of the website. Just insert this processor at the first position in the pipeline and you're good to go!

Good luck!


namespace Sitecore.SharedSource.Pipelines.GetConfiguration  
 {  
   class GetFromContextSiteOutsideRoot : GetConfigurationProcessor  
   {  
     private static readonly string SettingsRootTemplateId = Settings.GetSetting("Ecommerce.Settings.SettingsRootTemplateId");  
     private const string EcommerceSiteSettingsAttribute = "EcommerceSiteSettings";  
     private const string EcommerceSiteSettingsDefaultValue = "/Home/Site settings";  
     public GetFromContextSiteOutsideRoot()  
     {  
     }  
     public override void Process(ConfigurationPipelineArgs args)  
     {  
       object obj1 = IoCContainerExtensions.Resolve(Sitecore.Ecommerce.Context.Entity, args.ConfigurationItemType, new ResolverOverride[0]);  
       if (!(obj1 is IEntity) || Sitecore.Context.Site == null || Sitecore.Context.Database == null)  
         return;  
       string part2 = Sitecore.Context.Site.Properties[GetFromContextSiteOutsideRoot.EcommerceSiteSettingsAttribute];  
       if (string.IsNullOrEmpty(part2))  
         part2 = GetFromContextSiteOutsideRoot.EcommerceSiteSettingsDefaultValue;  
       Item obj2 = Sitecore.Context.Database.GetItem(FileUtil.MakePath(Sitecore.Context.Site.ContentStartPath, part2));  
       if (obj2 == null || obj2.Template.ID.ToString() != GetFromContextSiteOutsideRoot.SettingsRootTemplateId && Enumerable.FirstOrDefault<TemplateItem>(Enumerable.Where<TemplateItem>((IEnumerable<TemplateItem>)obj2.Template.BaseTemplates, (Func<TemplateItem, bool>)(x => x.ID.ToString() == GetFromContextSiteOutsideRoot.SettingsRootTemplateId))) == null)  
         return;  
       IEntity entity = (IEntity)obj1;  
       Item source = obj2.Axes.SelectSingleItem(string.Format(".//*[@@name='{0}']", (object)entity.Alias));  
       this.RegisterInstance(args, (IEntity)obj1, source);  
       args.AbortPipeline();  
     }  
   }  
 }  

woensdag 1 juni 2011

RazorForSitecore module - Enable Razor templating in the Sitecore CMS

I'm a huge fan of the ASP.NET MVC framework since it was launched. A few months ago I was forced back to Webforms when I started using the Sitecore content management system. While trying to combine elements from MVC into Sitecore I found a nice opportunity that allows for Razor syntax to be used to define re-usable renderings. Webforms and Razor together might not be "love at first sight" but after a bit of tweaking it turns out to work quite nicely.

I molded together a module called "RazorForSitecore". This module is backed by the open-source Razor Engine* and integrates nicely into the Sitecore presentation engine (how?). It can even be combined with other modules that enable strongly-typed classes to be created from templates (e.g. CompiledDomainModel). This has some benefits like:
  • Compile time type checking (views are compiled into memory assembly)
  • Intellisense support
(* PLEASE NOTE: This module uses a slightly modified version of the Razor Engine so that template cache invalidation is made possible!)

The module is available at the Sitecore Shared Source (RazorForSitecore) library. To use the module you have to do the following (detailed description):
  1. Update IIS application pool .NET version to 4.0!
  2. Install the package from the Shared Source Library (link).
  3. Update your main web.config file (see below)
  4. Create your first Razor View
  5. Add your view to the presentation details of an item
To use RazorForSitecore in conjunction with a module like CompiledDomainModel you have to follow these additional steps:
  • Install your strong-typing module
  • Create a processor in the "initialize" pipeline that specifies which ITypedModelProvider the module should use
You can download the module from the Shared Source libary: http://trac.sitecore.net/RazorForSitecore



How does this module work?
The module allows you to create Razor renderings mostly as an alternative to XSLT renderings. XSLT renderings are used for simple presentation elements (from a backend perspective), unlike sublayouts which allow for complex behavior or logic to be implemented. These Razor views cannot contain complex behavior as they are totally unaware of the Webforms lifecycle (they are parsed in a PreRender event of a sublayout).

When you create a 'Razor view' the module automatically creates an item and a corresponding .cshtml template file. However, it also creates an .ascx sublayout to 'contain' and 'integrate' the Razor template into Sitecore. Don't worry, you don't have to do anything with the file it just sits there and inherits from a base class that contains the logic. The module also sets the parameters 'modelType' and 'razorPath' on the item. Don't change or overwrite these or you will die! :)

Creating view helpers
It is possible to extend the view with helper functions through extension methods applied on the SitecoreHelper class. The SitecoreHelper class is exposed as property "Sc" in the view baseclass. This is an example of an extension method that uses the FieldRenderer control and keeps supporting the Page Editor:

public static class RenderingHelpers
  
   {
  
     public static string FieldRenderer(this SitecoreHelper helper, Item item, string fieldName)
  
     {
  
       Sitecore.Web.UI.WebControls.FieldRenderer r = new Sitecore.Web.UI.WebControls.FieldRenderer();
  
       r.Item = item;
  
       r.FieldName = fieldName;
  
       return r.Render();
  
     }
  
   }  


Here is a detailed description of the steps involved.
1, 2
This really shouldn't be a problem

3. Update your main web.config file
Add the following section element under configSections:

<section name="razorEngine" type="RazorEngine.Configuration.RazorEngineConfigurationSection, RazorEngine" requirePermission="false" />  

Add the following razorEngine element under configuration (right above connectionStrings (as sibling) will do):

<razorEngine>  
   <namespaces>  
    <add namespace="Sitecore.Sharedsource.RazorForSitecore" />  
   </namespaces>  
   <templateServices default="Sitecore.Sharedsource.RazorForSitecore.TemplateService, RazorForSitecore">  
    <add name="Sitecore.Sharedsource.RazorForSitecore.TemplateService, RazorForSitecore" language="CSharp" />  
   </templateServices>  
  </razorEngine>  

Find the compilation element (at the bottom of the file) and add/change the targetFramework attribute to reflect "4.0" (changing the target framework under Project properties -> Application will also work):

<compilation defaultLanguage=”c#” debug=”false” targetFramework=”4.0” />  

Add the following buildProviders element under compilation:

<buildProviders>
  
   <add extension=".cshtml" type="System.Web.WebPages.Razor.RazorBuildProvider, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
  
 </buildProviders>
  

Add the following add elements under assemblies (under compilation). Make sure you remove the existing add element for System.Data.Linq which is probably pointing to version 3.5.0.0:

<add assembly="System.Data.Linq, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
  
 <add assembly="System.Web.Helpers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
  
 <add assembly="System.Web.WebPages, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />

In some cases you need to find the providerOption element and change the value to "v4.0". Don't worry if you can't find it, .NET will throw a nice exception if you forget to change it in that case.

<providerOption name="CompilerVersion" value="v4.0" />  

4. Create your first Razor View


Specify the view name.

The default model type is Item. The module sets the .Model property of the view to the current item in Sitecore.
You should only change the model type if you are using a strong-typing module that converts a Sitecore item into
another type.

Items belonging to the views are stored in a fixed location. You can create subfolders if you like.

The files (one .ascx 'container' and the .cshtml Razor template) are stored in /layouts/razor by default. You can change this
in the /App_Config/Include/RazorForSitecore.config file.

Refresh the content tree to see your view.


5. Add your view to the presentation details of an item


Additionally: to use CompiledDomainModel as your model provider.
  • Install the module as described on the module homepage (link).
  • Create a processor for the initialize pipeline that registers the model provider, e.g.
    namespace Sitecore.Sharedsource.RazorForSitecore
      
       {
      
         class RazorProcessor
      
         {
      
           public void Process(PipelineArgs args)
      
           {
      
             RazorForSitecore.ModelManager.SetModelProvider(new GenericTypedModelProvider(YourDomainModelNamespaceHere.ItemWrapper.CreateTypedWrapper));
      
           }
      
         }
      
       }  
    
  • Register the processor in ~/App_Config/Include/RazorForSitecore.config
From this point you can specify your newly created model types instead of the Sitecore Item class.

Enjoy the module and let me know if you find bugs or have suggestions for improvements!