How to debug Sitecore 10 .Net Core Rendering Host

 

First off all you need to install Sitecore 10 using Docker. You can follow this example.

To debug Sitecore 10 .Net Core Rendering Host are two ways:

  1. Using Visual Studio Container Tools 

      If you have Visual Studio 2019 version 16.4 or later, you can use the Containers window to view running containers on your machine, as well as images that you have available.

Open the Containers window by using the search box in the IDE (press Ctrl+Q to use it), type in container, and choose the Containers window from the list.

You need to right click on Rendering container , right click and choose “Attach to Process

Select the .NET Rendering Host process ( exe file) .

2. The second way to do it , is to run and to debug directly from Visual Studio

In Solution Explorer select the Rendering Host project , right click and select Debug

Select Start New Instance.

And the Rendering Host will run on the browser.

Happy Sitecoring with Docker!

View Sitecore xConnect logs in Log2Console

Few days ago I blogged about how to view Commerce logs in Log2Console.

I searched a little bit how to  view  xConnect logs in real time, but I didn’t find any article how to add a custom serilog sink.

I made a small research and here is there solution:

To view xConnect logs in Log2Console you need to follow next steps :

  1. Add Serilog.Sinks.UDP.dll and Serilog.Sinks.PeriodicBatching.dll to your xConnect bin folder . (https://github.com/FantasticFiasco/serilog-sinks-udp-sample-dotnet-framework)
  2.  Add a new Serilog configuration  into :     yourxConnectfolder\App_Data\Config\Sitecore\CoreServices and name it : sc.Serilog.Udp.xml where you set the remoteAddress and remotePort:
<?xml version="1.0" encoding="utf-8"?>
<Settings>
<Serilog>
<Using>
<UDPSinkAssembly>Serilog.Sinks.Udp</UDPSinkAssembly>
</Using>
<WriteTo>
<Udp>
<Name>Udp</Name>
<Args>
<remoteAddress>localhost</remoteAddress>
<remotePort>7071</remotePort>
</Args>
</Udp>
</WriteTo>
</Serilog>
</Settings>

3. To view xconnect logs from AutomationEngine, IndexWorker and ProcessEngine you need to copy above file  to the CoreServices folder (example  for IndexWorker : c:\inetpub\wwwroot\xconnectFolder\App_Data\jobs\continuous\IndexWorker\App_Data\config\sitecore\CoreServices\ )

4. Check log2console (I already setup the receivers)

log2consoleXconnect.PNG

Sitecore Commerce logviewing using Log2Console

To view Sitecore logs in Log2Console is pretty easy. Sitecore  Platform is using log4net and you can add an UdpAppender for logging in an external application.

On few blogs is described how to do it:

  1. http://blog.jan.hebnes.dk/2015/12/hackthedot-sitecore-logviewing-made.html
  2. https://community.sitecore.net/developers/f/8/t/136 ( few people described different tools for sitecore logs)
  3. https://publications.soulcode.agency/you-dont-monitor-your-local-sitecore-instance-close-enough/

Sitecore Commerce is a .NET Core application, and is using Serilog for log messages.

We will use an external nuget package to send UDP package over the network: https://github.com/FantasticFiasco/serilog-sinks-udp

To view the commerce logs in Log2Console we need to follow next steps:

  1. Add a settings in commerce config.json to enable/disable sending UDP packages over the network.

writetoudp2.JPG

2. Add reference to Serilog.Sinks.Udp nuget package

nugetpackage.JPG

3. Modify commerce engine to use Serilog.Sink.Udp

The UDP packages are sent on port 7071.

At the end of Startup.cs add next code:

var writeToUdp = false;

//check if writotoUdp is enabled
if (bool.TryParse(Configuration.GetSection(“Serilog:WritetoUDP”).Value, out writeToUdp))
{
if (writeToUdp)
Log.Logger = new LoggerConfiguration()
.WriteTo.Sink((ILogEventSink)Log.Logger)
.WriteTo.Udp(“localhost”, 7071, System.Net.Sockets.AddressFamily.InterNetwork)
.CreateLogger();
}

4. Add a log2console receiver, it will work on port 7071.

log2consoleReceiver.JPG

and commerce logs will appear on Log2console

log2consoleLogs.JPG

Extend Commerce Entity Views

To extend commerce entity view you need to have a look on Commerce Views service

The Commerce Views service is provided by the Sitecore.Commerce.Plugin.Views
plugin.

Commerce views are also designed to feed directly into a dynamic data-driven business tool experience. You can activate, extend, or remove functionality in the business tools through custom-developed plugins, without having to modify the out-of-box plugins or modify the source code of the business tools themselves. This approach dramatically improves upgradability.

In the Sitecore Commerce architecture, this view layer is provided on the server side, instead of the client side.In this way you don’t need to have frontend knowledge. This allows standard Commerce plugins to extend existing views and add new views.

Commerce views are exposed via the Authoring API. This Authoring API is based on OData.

The EntityView is the core Commerce artifact that supports views.

An EntityView represents a flattened, dynamic service response focused on supporting a dynamic user experience.

The EntityView provides a business user experience that is completely customizable and extensible. The EntityView allows the business user to take actions and dynamically updates the view without requiring significant intelligence or customization of the user experience itself.

The EntityView is a simple POCO class that provides a property bag. The EntityView can have child EntityViews. You can build out more complex views as needed.

A Composite EntityView includes a master view and child views.

The master view represents the overall page itself. It normally does not contain properties, but could. The master view contains a list of child views that each represent a section of the user interface. It also contains a list of actions that can be executed against the entity. An example of a child view is the Details section of a page. For example, on an Order page, the child view is the section that presents the order confirmation identifier, the date, and so on.

In the next example you will find how to add to sellable items  the first image from product “Images” field.

 

 

Add a policy where you keep the value of the CM server.

namespace Plugin.Demo.ExtendCommerceView.Policies
{
public class CMServerPolicy : Policy
{
public string ServerHostName { get; set; }
}
}

view raw
ServerHostPoliciy
hosted with ❤ by GitHub

Add the policy to the PlugIn.Habitat.CommerceAuthoring-1.0.0.json .

{
"$type": "Plugin.Demo.ExtendCommerceView.Policies.CMServerPolicy,Plugin.Demo.ExtendCommerceView",
"ServerHostName": "http://xp0.sc/&quot;
}

view raw
ServerHostPolicy
hosted with ❤ by GitHub

Inject the pipeline block which extend the entitiy view after PopulateEntityViewActionsBlock :

using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
namespace Plugin.Demo.ExtendCommerceView
{
using Sitecore.Commerce.Core;
using Sitecore.Commerce.EntityViews;
using Sitecore.Framework.Configuration;
using Sitecore.Framework.Pipelines.Definitions.Extensions;
/// <summary>
/// The configure sitecore class. This allows a Plugin to wire up new Pipelines or to change existing ones.
/// </summary>
public class ConfigureSitecore : IConfigureSitecore
{
/// <summary>
/// The configure services constructor.
/// </summary>
/// <param name="services">
/// The services.
/// </param>
public void ConfigureServices(IServiceCollection services)
{
var assembly = Assembly.GetExecutingAssembly();
services.RegisterAllPipelineBlocks(assembly);
services.Sitecore().Pipelines(config => config
.ConfigurePipeline<IFormatEntityViewPipeline>(d =>
{
d.Add<PopulateSellableItemImages>().After<PopulateEntityViewActionsBlock>();
})
);
services.RegisterAllCommands(assembly);
}
}
}

view raw
ConfigureServices
hosted with ❤ by GitHub

Implement the PopulateSellableItemImages pipelineblock:

 

 

namespace Plugin.Demo.ExtendCommerceView
{
using Plugin.Demo.ExtendCommerceView.Policies;
using Sitecore.Commerce.Core;
using Sitecore.Commerce.EntityViews;
using Sitecore.Commerce.Plugin.Catalog;
using Sitecore.Framework.Pipelines;
using System.Linq;
using System.Threading.Tasks;
[PipelineDisplayName("PopulateSellableItem")]
public class PopulateSellableItemImages : PipelineBlock<EntityView, EntityView, CommercePipelineExecutionContext>
{
private readonly CommerceCommander _commerceCommander;
public PopulateSellableItemImages(CommerceCommander commerceCommander)
{
this._commerceCommander = commerceCommander;
}
public override Task<EntityView> Run(EntityView entityView, CommercePipelineExecutionContext context)
{
if (entityView.EntityId.Contains("Entity-Category-"))
{
var currentEntityViewArgument = this._commerceCommander.Command<ViewCommander>().CurrentEntityViewArgument(context.CommerceContext);
//get the sellableitems section
var sellableItemsView = entityView.ChildViews.FirstOrDefault(p => p.Name == "SellableItems");
if (sellableItemsView != null)
{
var serverHostName = context.GetPolicy<CMServerPolicy>().ServerHostName;
// parse the sellableitems section
foreach (var currentsellableItem in (sellableItemsView as EntityView).ChildViews)
{
var currentsellableItemFoundEntity = context.CommerceContext.GetObjects<FoundEntity>().FirstOrDefault(p => p.EntityId == (currentsellableItem as EntityView).ItemId);
if (currentsellableItemFoundEntity != null)
{
var sellableItem = currentsellableItemFoundEntity.Entity as SellableItem;
if (sellableItem != null)
{
//get the images component
var imagesComponent = sellableItem.GetComponent<ImagesComponent>();
var defaultImage = imagesComponent.Images.FirstOrDefault().Replace("-","");
if (defaultImage != null)
{
(currentsellableItem as EntityView).Properties.Insert(1, new ViewProperty
{
Name = " ",
IsHidden = false,
IsReadOnly = true,
OriginalType = "Html",
UiType = "Html",
IsRequired = false,
Value = $"<img alt='' height=60 width=60 src='{serverHostName}-/media/{defaultImage}.ashx'/>",
});
}
}
}
}
}
}
return Task.FromResult(entityView);
}
}
}

view raw
gistfile1.txt
hosted with ❤ by GitHub

 

 

 

 

Commerce Computed Indexfield

Computed fields allow you to add entirely new fields in your search indexes, in addition to those of Sitecore.

To create a computed index field for commerce entities you need to follow next steps:

  1. Create your own class which inherits from : Sitecore.Commerce.Engine.Connect.Search.ComputedFields.BaseCommerceComputedField
using Newtonsoft.Json.Linq;
using Sitecore.Commerce;
using Sitecore.Commerce.Engine.Connect;
using Sitecore.Commerce.Engine.Connect.DataProvider;
using Sitecore.Commerce.Engine.Connect.Search.ComputedFields;
using Sitecore.ContentSearch;
using Sitecore.Data;
using Sitecore.Data.Items;
using System.Collections.Generic;
namespace Sitecore.CommerceDemo.Indexing.CommerceComputedIndexFields
{
public class ExampleComputedIndexField : BaseCommerceComputedField
{
private static readonly IEnumerable<ID> _validTemplates = new List<ID>
{
CommerceConstants.KnownTemplateIds.CommerceCatalogTemplate,
CommerceConstants.KnownTemplateIds.CommerceCategoryTemplate,
CommerceConstants.KnownTemplateIds.CommerceProductTemplate
};
protected override IEnumerable<ID> ValidTemplates
{
get
{
return _validTemplates;
}
}
public override object ComputeValue(IIndexable indexable)
{
Assert.ArgumentNotNull(indexable, "indexable");
Item item = indexable as SitecoreIndexableItem;
if (item == null)
{
return null;
}
Item validatedItem = base.GetValidatedItem(indexable);
if (validatedItem != null)
{
JToken entity = new CatalogRepository().GetEntity(validatedItem.ID.Guid.ToString(), null);
if (entity != null)
{
}
}
return null;
}
}
}

2. Customize the class to meet your particular needs.

The “JToken entity” object contains few information about your sellableitem (in the below example is a sellableitem from SXA)

{
"@odata.context": "https://localhost:5000/Api/$metadata#SellableItems/$entity&quot;,
"CompositeKey": null,
"CreatedBy": "sitecore\\Admin",
"UpdatedBy": "sitecore\\Admin",
"DateCreated": "2018-11-14T05:18:09.9239529Z",
"DateUpdated": "2018-11-14T05:17:44.2144901Z",
"DisplayName": "Studio X Over-the-Ear Wired Headphones",
"FriendlyId": "6042064",
"Id": "Entity-SellableItem-6042064",
"Version": 4,
"EntityVersion": 2,
"Published": false,
"IsPersisted": false,
"Name": "Studio X Over-the-Ear Wired Headphones",
"Policies": [
{
"@odata.type": "#Sitecore.Commerce.Plugin.Pricing.ListPricingPolicy",
"PolicyId": "d6e7c406ecb144cabbad6415bae23d26",
"Models": [],
"Prices": [
{
"CurrencyCode": "USD",
"Amount": 49.5
}
]
}
],
"SitecoreId": "2495594a-9914-4d13-9ffd-aff5484ac57c",
"CatalogToEntityList": null,
"ParentCatalogList": "59ddadc1-9b88-727e-9e14-3f6cf321ae0f",
"ParentCategoryList": "a0860c27-2a32-841f-7014-75b163b9471e|368aae84-008e-43a6-150f-826085a271ba|871c3b62-ce96-ad61-dabe-10db020f02c5|e5e9207c-58cd-0735-2a79-b5d2b199ea7a",
"ChildrenCategoryList": null,
"ChildrenSellableItemList": null,
"ItemVariations": null,
"Description": "The Studio X over-the-ear headphones delivery superb quality and design along with professional-grade sound. Its flexible headband brings you endless hours of comfort and is fully adjustable. This wired headphone has a wide frequency response, low distortion, and deep bass for all types of music and listening needs. ",
"ItemTemplate": null,
"ProductId": "6042064",
"Brand": "Studio X",
"Manufacturer": "",
"TypeOfGood": "",
"Tags": [
{
"Name": "over-the-ear",
"Policies": [],
"Excluded": false
},
{
"Name": "headphones",
"Policies": [],
"Excluded": false
},
{
"Name": "studiox",
"Policies": [],
"Excluded": false
},
{
"Name": "wired",
"Policies": [],
"Excluded": false
}
],
"ListPrice": null,
"Components": [
{
"@odata.type": "#Sitecore.Commerce.Plugin.Catalog.CatalogsComponent",
"Id": "80f9e1150e6a425d8a072b00f76dc127",
"Name": "",
"Comments": "",
"Policies": [],
"ChildComponents": [
{
"@odata.type": "#Sitecore.Commerce.Plugin.Catalog.CatalogComponent",
"Id": "4a38b5f02aa94fa492fe9def820c700f",
"Name": "Habitat_Master",
"Comments": "",
"Policies": [],
"CatalogLanguages": [],
"DefaultLanguage": null,
"ItemDefinition": "Headphones",
"ChildComponents": []
}
]
},
{
"@odata.type": "#Sitecore.Commerce.Plugin.Catalog.ImagesComponent",
"Id": "c0e48567d9324db58bb3a6ff244dff6c",
"Name": "",
"Comments": "",
"Policies": [],
"Images": [
"d2d12d06-341d-4ee8-b93e-fd6a963ff74f",
"bb5ca39d-2b41-452d-9bf4-e867adcaed8c",
"a2785fc9-0bd7-493d-ae2c-10ea5d55a3cd",
"2627453a-63d4-4907-9d62-aa9b902b9575"
],
"ChildComponents": []
},
{
"@odata.type": "#Sitecore.Commerce.Core.LocalizedEntityComponent",
"Id": "da9343a6568c49aeba0aa37d98adf420",
"Name": "",
"Comments": "",
"Policies": [],
"Entity": {
"Name": "",
"EntityTarget": "Entity-LocalizationEntity-a20582cd9bc74422a11fdf46c3996f6f",
"Policies": []
},
"ChildComponents": []
},
{
"@odata.type": "#Sitecore.Commerce.Plugin.Catalog.ItemVariationsComponent",
"Id": "efc538dc7303482694e500cd9a642a2c",
"Name": "",
"Comments": "",
"Policies": [],
"ChildComponents": [
{
"@odata.type": "#Sitecore.Commerce.Plugin.Catalog.ItemVariationComponent",
"Id": "56042064",
"Name": "Studio X Over-the-Ear Wired Headphones",
"Comments": "",
"Policies": [
{
"@odata.type": "#Sitecore.Commerce.Plugin.Pricing.ListPricingPolicy",
"PolicyId": "4d8bd6a11ec2405394422811db3beebc",
"Models": [],
"Prices": [
{
"CurrencyCode": "USD",
"Amount": 49.5
}
]
}
],
"DisplayName": "Studio X Over-the-Ear Wired Headphones",
"Description": "",
"ListPrice": null,
"Tags": [
{
"Name": "over-the-ear",
"Policies": [],
"Excluded": false
},
{
"Name": "headphones",
"Policies": [],
"Excluded": false
},
{
"Name": "studiox",
"Policies": [],
"Excluded": false
},
{
"Name": "wired",
"Policies": [],
"Excluded": false
}
],
"Disabled": false,
"ChildComponents": [
{
"@odata.type": "#Sitecore.Commerce.Plugin.Catalog.ImagesComponent",
"Id": "c13007d97cf54c46b98d4a2ba34e6fc5",
"Name": "",
"Comments": "",
"Policies": [],
"Images": [
"d2d12d06-341d-4ee8-b93e-fd6a963ff74f",
"bb5ca39d-2b41-452d-9bf4-e867adcaed8c",
"a2785fc9-0bd7-493d-ae2c-10ea5d55a3cd",
"2627453a-63d4-4907-9d62-aa9b902b9575"
]
},
{
"@odata.type": "#Sitecore.Commerce.Plugin.Inventory.InventoryComponent",
"Id": "ac79eaf75eb64c52b3255b46b19c0444",
"Name": "",
"Comments": "",
"Policies": [],
"InventoryAssociations": [
{
"Name": "",
"Policies": [],
"InventoryInformation": {
"Name": "",
"EntityTarget": "Entity-InventoryInformation-Habitat_Inventory-6042064-56042064",
"Policies": []
},
"InventorySet": {
"Name": "",
"EntityTarget": "Entity-InventorySet-Habitat_Inventory",
"Policies": []
}
}
]
}
]
}
]
},
{
"@odata.type": "#Sitecore.Commerce.Plugin.ManagedLists.ListMembershipsComponent",
"Id": "4dc2793e255e4d2c987f6ec776e410cf",
"Name": "",
"Comments": "",
"Policies": [],
"Memberships": [
"SellableItems",
"CatalogItems"
],
"ChildComponents": []
},
{
"@odata.type": "#Sitecore.Commerce.Plugin.Workflow.WorkflowComponent",
"Id": "b8ed544dd8de4b7a87a1ea2923db8fef",
"Name": "",
"Comments": "",
"Policies": [],
"Workflow": {
"Name": "DefaultCommerceWorkflow",
"EntityTarget": "Entity-Workflow-DefaultCommerceWorkflow",
"Policies": []
},
"CurrentState": "Draft",
"ChildComponents": []
},
{
"@odata.type": "#Sitecore.Commerce.Plugin.Views.EntityViewComponent",
"Id": "4b63bca302f9448cbab76cf1a9592379",
"Name": "",
"Comments": "",
"Policies": [],
"View": {
"Name": "",
"Policies": [],
"DisplayName": "",
"EntityId": "",
"EntityVersion": 1,
"Action": "",
"ItemId": "",
"Properties": [],
"ChildViews": [
{
"@odata.type": "#Sitecore.Commerce.EntityViews.EntityView",
"Name": "Test",
"Policies": [],
"DisplayName": "Test",
"EntityId": "Entity-SellableItem-6042064",
"EntityVersion": 1,
"Action": "",
"ItemId": "Composer-d536d25ed0aa4346b1663c9912f409d0",
"Properties": [],
"ChildViews": [],
"DisplayRank": 0,
"UiHint": "Flat",
"Icon": "piece"
}
],
"DisplayRank": 500,
"UiHint": "Flat",
"Icon": "chart_column_stacked"
},
"ChildComponents": []
},
{
"@odata.type": "#Sitecore.Commerce.Plugin.Catalog.ItemSpecificationsComponent",
"Id": "6a4c624247144da0b4fc51570bb5ee0b",
"Name": "",
"Comments": "",
"Policies": [],
"AreaServed": {
"Name": "",
"Policies": [],
"TimeZone": "",
"Longitude": "",
"Latitude": "",
"Region": "California",
"AreaCode": "",
"MetroCode": "",
"City": "Gilau",
"PostalCode": "407310",
"BusinessName": "",
"DnsAddress": "",
"IpAddress": "",
"IspName": ""
},
"Weight": 0,
"WeightUnitOfMeasure": "",
"Length": 0,
"Width": 0,
"Height": 0,
"DimensionsUnitOfMeasure": "",
"SizeOnDisk": 0,
"SizeOnDiskUnitOfMeasure": "",
"ChildComponents": []
},
{
"@odata.type": "#Sitecore.Commerce.Plugin.Catalog.DisplayPropertiesComponent",
"Id": "756f6b7aa16f4611ac9c224089bbbcc0",
"Name": "",
"Comments": "",
"Policies": [],
"DisambiguatingDescription": "",
"DisplayOnSite": true,
"DisplayInProductList": false,
"Color": "",
"Size": "",
"Style": "",
"ChildComponents": []
},
{
"@odata.type": "#Sitecore.Commerce.Plugin.EntityVersions.EntityVersionsComponent",
"Id": "c648775d683c4780b8f53c784d2e9eed",
"Name": "",
"Comments": "",
"Policies": [],
"EntityVersions": [
{
"Name": "",
"Policies": [],
"Version": 2
},
{
"Name": "",
"Policies": [],
"Version": 1
}
],
"ChildComponents": []
},
{
"@odata.type": "#Sitecore.Commerce.Plugin.Catalog.RelationshipsComponent",
"Id": "436e841726fb497d852c9d679286426a",
"Name": "",
"Comments": "",
"Policies": [],
"Relationships": [
{
"Name": "TrainingSellableItemToSellableItem",
"Policies": [],
"RelationshipList": []
},
{
"Name": "WarrantySellableItemToSellableItem",
"Policies": [],
"RelationshipList": []
},
{
"Name": "InstallationSellableItemToSellableItem",
"Policies": [],
"RelationshipList": []
},
{
"Name": "RelatedSellableItemToSellableItem",
"Policies": [],
"RelationshipList": [
"92a7d2cc-91ad-5223-26ba-cb88c8612c71",
"50bcae09-fd1b-4d94-10eb-f91d7667d77d",
"b8608a92-c842-110a-efdf-d2d8376ce7cd",
"1298810a-459d-88a3-c786-64e7d36e58cc",
"507f8ed2-ce2d-f426-b211-6033f291bf62",
"8fd88e39-5060-b596-dd58-662f127e9c50",
"ab0b7d24-e450-9629-d5ba-e1606d2857bc",
"428b5292-0ff7-732d-9dfc-b547aa435aba",
"73ad9c55-def7-c1ff-5efc-cb3e9c74d101",
"16eaf2e1-9daa-b7a9-835f-51358e72058b"
]
}
],
"ChildComponents": []
}
]
}

view raw
EntityJson
hosted with ❤ by GitHub

3. Add the Computed Field in the Configuration:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/&quot; xmlns:search="http://www.sitecore.net/xmlconfig/search/&quot; xmlns:role="http://www.sitecore.net/xmlconfig/role/"&gt;
<sitecore role:require="Standalone or ContentDelivery or ContentManagement" search:require="solr">
<contentSearch>
<indexConfigurations>
<defaultSolrIndexConfiguration type="Sitecore.ContentSearch.SolrProvider.SolrIndexConfiguration, Sitecore.ContentSearch.SolrProvider">
<fieldMap type="Sitecore.ContentSearch.SolrProvider.SolrFieldMap, Sitecore.ContentSearch.SolrProvider">
<fieldNames hint="raw:AddFieldByFieldName">
<field fieldName="examplefield" storageType="YES" indexType="UN_TOKENIZED" vectorType="NO" boost="1f" returnType="text" settingType="Sitecore.ContentSearch.SolrProvider.SolrSearchFieldConfiguration, Sitecore.ContentSearch.SolrProvider"/>
</fieldNames>
</fieldMap>
<documentOptions type="Sitecore.ContentSearch.SolrProvider.SolrDocumentBuilderOptions, Sitecore.ContentSearch.SolrProvider">
<fields hint="raw:AddComputedIndexField">
<field fieldName="examplefield" returnType="text">Sitecore.CommerceDemo.Indexing.CommerceComputedIndexFields.ExampleComputedIndexField, Sitecore.CommerceDemo.Indexing</field>
</fields>
</documentOptions>
</defaultSolrIndexConfiguration>
</indexConfigurations>
</contentSearch>
</sitecore>
</configuration>

 

 

Sitecore Commerce Icons

Ten years ago when I started working with Sitecore, one of the first thing I learn it was how to select icons for items templates.

In Sitecore Commerce is a bit different how to choose icons for entity views.You don’t have a picker for icons.

Icons in Sitecore Commerce are stored in Bizfx tools in the root folder in SitecoreIcons.*.svg file.

bizfxtool

To view all the icons from the Bizfx toool you can use  http://fontello.com/

You can drag and drop and select the icons .

fontello

 

After you select the SVG file you will see icons available for Sitecore Commerce.

BizfxIcons

Using Fontello you can search for icons :

calendarclock

To create an entity view with a selected icons you have use :

var newEntityView = new EntityView
{
Name = “CustomEntityView”,
DisplayName = “CustomEntityView”,
Icon = “calendar_clock”,
ItemId = “CustomEntityViewId”
};

 

The result for on Commerce Dashboard is :

CustomEntityView

In the next blog I will explain how to add a new EntityView to CommerceDashboard

 

Field Validators on Experience Editor

On Sitecore 8 I added few fields validators.
Everything works perfectly on Content Editors but not on Experience Editor.

On config files we have next setting:


<!--
  WEB EDIT ENABLE VALIDATION
            If true, the Page Editor will execute item and field validation rules whenever a user tries to save items in the Page Editor.
            Only 'Critical' and 'Fatal' validators are evaluated, and item validation rules are executed for the current context item only.
            Field validation rules are only executed for fields that the current user can modify in the Page Editor.
            Default value: true
      
-->
<setting name="WebEdit.EnableValidation" value="true" patch:source="Sitecore.ExperienceEditor.config"/> 
 

but is ignored on Experience Editor.

I found a new solution switching Experience Editor from Speak UI to Sheer UI.
How to switch it :

Modify on App_Config\Include\Sitecore.ExperienceEditor.config
mvc.renderPageExtenders pipeline

1. Comment out the following line in the:

<pageextender type="Sitecore.ExperienceEditor.Speak.Ribbon.PageExtender.RibbonPageExtender, Sitecore.ExperienceEditor.Speak.Ribbon" />
[/soucecode]
2. Uncomment the following lines:
 
<pageextender type="Sitecore.Layouts.PageExtenders.PreviewPageExtender, Sitecore.ExperienceEditor" />
<pageextender type="Sitecore.Layouts.PageExtenders.WebEditPageExtender, Sitecore.ExperienceEditor" />
<pageextender type="Sitecore.Layouts.PageExtenders.DebuggerPageExtender, Sitecore.ExperienceEditor" />
 

If you have a Mvc Solution you need to change Sitecore.MvcExperienceEditor.config
1. Uncomment the page extenders below and comment the “SPEAK-based” Experience Editor ribbon processors to switch to old SheerUI-based Experience Editor ribbon.

 
        <processor type="Sitecore.Mvc.ExperienceEditor.Pipelines.RenderPageExtenders.RenderPageEditorExtender, Sitecore.Mvc.ExperienceEditor"></processor>
        <processor type="Sitecore.Mvc.ExperienceEditor.Pipelines.RenderPageExtenders.RenderPreviewExtender, Sitecore.Mvc.ExperienceEditor"></processor>
        <processor type="Sitecore.Mvc.ExperienceEditor.Pipelines.RenderPageExtenders.RenderDebugExtender, Sitecore.Mvc.ExperienceEditor"></processor>
    

2. Comment next pipeline

   <processor type="Sitecore.Mvc.ExperienceEditor.Pipelines.RenderPageExtenders.SpeakRibbon.RenderPageEditorSpeakExtender, Sitecore.Mvc.ExperienceEditor"></processor>
   

Goals in a multisite solution

Yesterday it was a question on Community how to implement Goals in a multisite solution: https://community.sitecore.net/developers/f/9/t/1913

And if I write few blog posts about multisite I said to write also a blogpost about implementing goals in a multisite solution.

My site structure is :

Countries

My goals structure is :

Step 1 :

Modify site definition like. I added a new property to sites “goalsFolder”

  <site name="countrythree" patch:before="site[@name='website']" hostName="countrythree.sitecoredemo.com" inherits="sitecoremvc-base" rootPath="/sitecore/content/countrythree" startItem="/" dictionaryDomain="{D8BC2E0C-36C5-4128-8F29-352B55E86676}" language="en" goalsFolder="countrythree" />

  <site name="countrytwo" patch:before="site[@name='website']" hostName="countrytwo.sitecoredemo.com" inherits="sitecoremvc-base" rootPath="/sitecore/content/countrytwo" startItem="/" dictionaryDomain="{D8BC2E0C-36C5-4128-8F29-352B55E86676}" language="en" goalsFolder="countrytwo" />


  <site name="countryone" patch:before="site[@name='website']" hostName="countryone.sitecoredemo.com" inherits="sitecoremvc-base" rootPath="/sitecore/content/countryone" startItem="/" dictionaryDomain="{D8BC2E0C-36C5-4128-8F29-352B55E86676}" language="de-de" goalsFolder="countrytwo" />

  <site name="sitecoremvc-base" patch:before="site[@name='website']" hostName="*" enableTracking="true" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content" startItem="/home" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="50MB" registryCacheSize="0" viewStateCacheSize="0" xslCacheSize="25MB" filteredItemsCacheSize="10MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" cacheRenderingParameters="true" renderingParametersCacheSize="10MB" />
  

Step2:
Change type of command with name “anylitics:opengoals” from Sitecore.Analytics.config
into:

  <command name="analytics:opengoals" type="Sitecore.Analytics.OpenGoals,SitecoreDemo" patch:source="Sitecore.Analytics.config"/>

Step3:
Create a new class OpenGoals:

    using Sitecore;
    using System;
    namespace SitecoreDemo.Analytics
    {
      [Serializable, UsedImplicitly]
      public class OpenGoals : OpenTrackingField
      {
        // Methods
        protected override string GetUrl()
        {
            return "/sitecore/shell/~/xaml/Sitecore.Shell.Applications.Analytics.TrackingField.Goals.aspx";
        }
       }
    }
    

4. Create a new xaml file name it Goals.xaml and add it to folder: /Sitecore/Shell/Override

      <?xml version="1.0" encoding="UTF-8" ?>
<xamlControls xmlns:x="http://www.sitecore.net/xaml" xmlns:ajax="http://www.sitecore.net/ajax" xmlns:rest="http://www.sitecore.net/rest" xmlns:r="http://www.sitecore.net/renderings" xmlns:xmlcontrol="http://www.sitecore.net/xmlcontrols" xmlns:p="http://schemas.sitecore.net/Visual-Studio-Intellisense" xmlns:asp="http://www.sitecore.net/microsoft/webcontrols" xmlns:html="http://www.sitecore.net/microsoft/htmlcontrols" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <Sitecore.Shell.Applications.Analytics.TrackingField.Goals x:inherits="SitecoreDemo.Analytics.GoalsPage,SitecoreDemo">

    <Sitecore.Controls.DialogPage runat="server" Header="Goals" Text="Select the goals that you want to associate with the selected item.">
      <AjaxScriptManager runat="server"/>
      <ContinuationManager runat="server" />
      <Script runat="server" Src="/sitecore/Shell/Applications/Analytics/TrackingField/TrackingField.js" />


<Style runat="server">
        #GoalsList table tr > td {
          padding: 0 0 10px 0;
        }
      </Style>




<table width="100%" height="100%" cellpadding="0" cellspacing="0" border="0">


<tr>


<td height="100%">
            <GridPanel Width="100%" Height="100%" runat="server" Background="White">

              <Border runat="server" GridPanel.Style="height:100%" Height="100%">
                <Scrollbox id="GoalsList" runat="server" Height="100%" Padding="0px"/>
              </Border>
            </GridPanel>
          </td>


        </tr>


      </table>


        
        
    </Sitecore.Controls.DialogPage>
  </Sitecore.Shell.Applications.Analytics.TrackingField.Goals>


</xamlControls>

    

5. Create a new class GoalsPage

using Sitecore;
using Sitecore.Analytics;
using Sitecore.Analytics.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Extensions.XElementExtensions;
using Sitecore.Shell.Applications.Analytics.TrackingField;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Web.UI.Sheer;
using Sitecore.Xml;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI.WebControls;
using System.Xml.Linq;
using SitecoreDemo.Util;

namespace SitecoreDemo.Analytics
{
   [UsedImplicitly]
public class GoalsPage : TrackingFieldPageBase
{
    // Methods
    protected override void OK_Click()
    {
        Packet packet = new Packet("tracking", new string[0]);
        base.AddIgnoreFlag(packet);
        base.AddExistingProfiles(packet);
        base.AddExistingCampaigns(packet);
        if (HttpContext.Current != null)
        {
            List<PageEventItem> pageEventDefinitions = new List<PageEventItem>(from item in Tracker.DefinitionItems.PageEvents.Concat<PageEventItem>(Tracker.DefinitionItems.Goals)
                where item.IsDeployed
                select item);
            base.AddExistingNoneGoals(packet, pageEventDefinitions);
            CheckBoxList checkBoxList = this.GoalsList.Controls[0] as CheckBoxList;
            if (checkBoxList != null)
            {
                TrackingFieldPageBase.GetEvents(packet, pageEventDefinitions, checkBoxList);
            }
            SheerResponse.SetDialogValue(packet.ToString());
            base.OK_Click();
        }
    }

    protected override void OnLoad(EventArgs e)
    {
        Assert.ArgumentNotNull(e, "e");
        Assert.CanRunApplication("Content Editor/Ribbons/Chunks/Analytics - Attributes/Goals");
        base.OnLoad(e);
    }

    protected override void Render(XDocument doc)
    {
        Assert.ArgumentNotNull(doc, "doc");
        this.RenderGoals(doc);
    }

    private void RenderGoals(XDocument doc)
    {
        Assert.ArgumentNotNull(doc, "doc");
        List<string> selected = new List<string>();
        foreach (XElement element in doc.Descendants("event"))
        {
            selected.Add(element.GetAttributeValue("name"));
        }
        CheckBoxList child = new CheckBoxList {
            ID = "GoalsCheckBoxList"
        };
        this.GoalsList.Controls.Add(child);
        System.Web.UI.Page page = TrackingFieldPageBase.GetPage();

        var site = ItemUtil.GetSiteContextForItem(ItemUtil.GetItem(Sitecore.Context.Request.QueryString["id"]));
        var goalsFolder = site.Properties["goalsFolder"];
        if ((page != null) && !page.IsPostBack)
        {
            IOrderedEnumerable<PageEventItem> query;
            if (!string.IsNullOrEmpty(goalsFolder) && Sitecore.Context.ContentDatabase.GetItem(Tracker.DefinitionItems.Goals.Path+"/"+goalsFolder)!=null)
            {
                query = from e in Tracker.DefinitionItems.AllPageEvents
                        where e.InnerItem.Parent.Key.Equals(goalsFolder, StringComparison.InvariantCultureIgnoreCase) && (e.IsDeployed && e.IsGoal) && !e.IsSystem
                        orderby e.DisplayName
                        select e;
            }
            else
            {
                query = from e in Tracker.DefinitionItems.AllPageEvents
                        where(e.IsDeployed && e.IsGoal) && !e.IsSystem
                        orderby e.DisplayName
                        select e;
            }
            TrackingFieldPageBase.RenderCheckBoxList(child, query, selected);
        }
    }

    


    // Properties
    [UsedImplicitly]
    protected Scrollbox GoalsList { get; set; }
}
}

Step6. Create a new class OpenTrackingField

    using Sitecore;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;
using Sitecore.Text;
using Sitecore.Web;
using Sitecore.Web.UI.Sheer;
using Sitecore.Web.UI.WebControls;
using System;
using Sitecore.StringExtensions;

namespace SitecoreDemo.Analytics
{
    [Serializable, UsedImplicitly]
    public class OpenTrackingField : Sitecore.Shell.Applications.Analytics.TrackingField.OpenTrackingField
    {
        CommandContext context;
        // Methods
        public override void Execute(CommandContext context)
        {
            this.context = context;
            base.Execute(context);
        }

        protected virtual string GetUrl()
        {
            return "/sitecore/shell/~/xaml/Sitecore.Shell.Applications.Analytics.TrackingField.aspx";
        }

        [UsedImplicitly]
        protected void Run(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Item item = base.DeserializeItems(args.Parameters["items"])[0];
            if (SheerResponse.CheckModified())
            {
                string str = args.Parameters["fieldid"];
                if (string.IsNullOrEmpty(str))
                {
                    str = "__Tracking";
                }
                if (args.IsPostBack)
                {
                    if (args.HasResult)
                    {
                        using (new StatisticDisabler(StatisticDisablerState.ForItemsWithoutVersionOnly))
                        {
                            item.Editing.BeginEdit();
                            item[str] = args.Result;
                            item.Editing.EndEdit();
                        }
                        if (AjaxScriptManager.Current != null)
                        {
                            AjaxScriptManager.Current.Dispatch("analytics:trackingchanged");
                        }
                        else
                        {
                            Context.ClientPage.SendMessage(this, "analytics:trackingchanged");
                            Context.ClientPage.SendMessage(this, "item:refresh(id={0})".FormatWith(new object[] { item.ID.ToString() }));
                        }
                    }
                }
                else if (item.Appearance.ReadOnly)
                {
                    SheerResponse.Alert("You cannot edit the '{0}' item because it is protected.", new string[] { item.DisplayName });
                }
                else if (!item.Access.CanWrite())
                {
                    SheerResponse.Alert("You cannot edit this item because you do not have write access to it.", new string[0]);
                }
                else
                {
                    UrlString urlString = new UrlString(this.GetUrl());
                    urlString.Add("id", context.Items[0].ID.ToShortID().ToString());
                    UrlHandle handle = new UrlHandle();
                    handle["tracking"] = item[str];
                    handle.Add(urlString);
                    this.ShowDialog(urlString.ToString());
                    args.WaitForPostBack();
                }
            }
        }
    }
}
     

Step7.
I am on countryone site and I want to add a new goal to a page.
Is showing me just the goals from countryone folder.

OpenGoalCountry

This code was tested on Sitecore 8.1.

 

Configuring indexes in a multisite solution

In the previous blogpost I wrote about aliases and how to inherit sites from a base site.
Also for the lucene indexes is posible to do it.

We have a Sitecore solution with 3 country sites that have same structure.
(please see below picture)

Countries

We need to create elegant solution for creating indexes for every country.

First step is create base configuration, I create a config file (Sitecore.ContentSearch.Lucene.Index.ZZZConfiguration.config ) that I add into a “ZZZ” folder to be the last in configuration file.

    <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <contentSearch>
      <configuration type="Sitecore.ContentSearch.ContentSearchConfiguration, Sitecore.ContentSearch">
        <indexTemplates hint="skip">
          <searchIndexTemplate id="$(1)" type="Sitecore.ContentSearch.LuceneProvider.SwitchOnRebuildLuceneIndex,Sitecore.ContentSearch.LuceneProvider">
            <param desc="name">$(1)</param>
            <param desc="folder">$(1)</param>
            <!-- This initializes index property store. Id has to be set to the index id -->
            <param desc="propertyStore" ref="contentSearch/indexConfigurations/databasePropertyStore" param1="$(id)" />
            <strategies hint="list:AddStrategy">
              <!-- NOTE: order of these is controls the execution order -->
              <strategy ref="contentSearch/indexConfigurations/indexUpdateStrategies/rebuildAfterFullPublish" />
              <strategy ref="contentSearch/indexConfigurations/indexUpdateStrategies/onPublishEndAsync" />
              <strategy ref="contentSearch/indexConfigurations/indexUpdateStrategies/remoteRebuild" />
            </strategies>
            <commitPolicyExecutor type="Sitecore.ContentSearch.CommitPolicyExecutor, Sitecore.ContentSearch">
              <policies hint="list:AddCommitPolicy">
                <policy type="Sitecore.ContentSearch.TimeIntervalCommitPolicy, Sitecore.ContentSearch" />
              </policies>
            </commitPolicyExecutor>
            <locations hint="list:AddCrawler">
              <crawler type="Sitecore.ContentSearch.SitecoreItemCrawler, Sitecore.ContentSearch">
                <Database>$(2)</Database>
                <Root>$(3)</Root>
              </crawler>
            </locations>
            <configuration ref="contentSearch/configuration/defaultIndexConfiguration | contentSearch/configuration/DefaultIndexConfiguration | contentSearch/indexConfigurations/defaultLuceneIndexConfiguration">
              <documentBuilderType>Sitecore.ContentSearch.LuceneProvider.LuceneDocumentBuilder, Sitecore.ContentSearch.LuceneProvider</documentBuilderType>
              <IndexAllFields>true</IndexAllFields>
              <exclude hint="list:ExcludeTemplate">
                <patch:delete />
              </exclude>
                <include hint="list:IncludeTemplate">
                 <!--add here your templates --> 
              </include>
              <fields hint="raw:AddComputedIndexField">
                 <!--add here your computed index field --> 
              </fields>
 
             </configuration>
          </eventIndexTemplate>
        </indexTemplates>
      </configuration>
    </contentSearch>
  </sitecore>
</configuration>
    

Now we need to create index config file that inherit from above configuration

 <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
      <contentSearch>
         <configuration type="Sitecore.ContentSearch.ContentSearchConfiguration, Sitecore.ContentSearch">
        <!--searchj-->
        <indexes hint="list:AddIndex">
          <index ref="contentSearch/configuration/indexTemplates/searchIndexTemplate" param1="country1" param2="web" param3="/sitecore/content/countryone" />
          <index ref="contentSearch/configuration/indexTemplates/searchIndexTemplate" param1="country2" param2="web" param3="/sitecore/content/countrytwo" />
	  <index ref="contentSearch/configuration/indexTemplates/searchIndexTemplate" param1="country3" param2="web" param3="/sitecore/content/countrythree" />
        </indexes>
      </configuration>
    </contentSearch>
  </sitecore>
      </configuration>
   

 
You can see I replace $(1),$(2),$(3) from first config file with parameters from second config file.
These are indexes for web database, if we want to create also for master database we need to modify strategy section
Above is the result of our configuration:

Sitecoreindexes

Configuring Aliases in a multi site solution

On the last Sitecore solution I have worked I needed to add aliases for every site.

Default aliases are shared for all sites from the solution.

To acomplish requirements we create under the item  /sitecore/system/Aliases folders for every site.

SitecoreAliases

On site definition for every site we add a new property named alias

<site name=”countrytwo” patch:before=”site[@name=’website’]”
hostName=”countrytwo.sitecoredemo.com”
inherits=”sitecoremvc-base”
rootPath=”/sitecore/content/countrytwo”
startItem=”/”
dictionaryDomain=”{D8BC2E0C-36C5-4128-8F29-352B55E86676}”
language=”en”
alias=”countrytwo”
mvcArea=”countrytwo”

/>

We rewrite AliasResolver to map aliases to sites .

We need to rewrite the entire class Sitecore.Pipelines.HttpRequest.AliasResolver because the methods are private.

   
///<summary>
/// Custom Alias Resolver
/// </summary>


public class AliasResolver : HttpRequestProcessor
{
     ///<summary>
     /// Processes the specified args.
     /// </summary>
     /// <param name="args">The args.</param>
     public override void Process(HttpRequestArgs args)
     {
        Assert.ArgumentNotNull(args, "args");
        //check if aliases are active
        if (!Settings.AliasesActive)
        {
          Sitecore.Diagnostics.Log.Warn("Aliases are not active.",this);
          return;
         }
         Database database = Context.Database;
         if (database == null)
          {
            Sitecore.Diagnostics.Log.Warn("Context database is null",this);
            return;
          }

          if (database.Aliases.Exists(MainUtil.DecodeName('/' +                     Context.Site.Properties["alias"] + '/' + args.LocalPath)) &&                   !this.ProcessItem(args))
           {
              this.ProcessExternalUrl(args);
           }
       }

        ///<summary>
       /// Processes the external URL.
       /// </summary>
       /// <param name="args">The arguments.</param>
        private void ProcessExternalUrl(HttpRequestArgs args)
        {
          string targetUrl = Context.Database.Aliases.GetTargetUrl('/' +    Context.Site.Properties["alias"]+'/' +                                         args.LocalPath);
          if (targetUrl.Length > 0)
            {
              this.ProcessExternalUrl(targetUrl);
            }
         }

         ///<summary>
         /// Processes the external URL.
         /// </summary>
         /// <param name="path">The path.</param>
         private void ProcessExternalUrl(string path)
            {
              if (Context.Page.FilePath.Length > 0)
               {
                 return;
               }
                Context.Page.FilePath = path;
             }

          ///  <summary>
          /// Processes the item.
          /// </summary>
          /// <param name="args">The arguments.</param>
          /// <returns></returns>
          private bool ProcessItem(HttpRequestArgs args)
           {
             ID targetID = Context.Database.Aliases.GetTargetID(MainUtil.DecodeName('/'  + Context.Site.Properties["alias"]                     + '/' + args.LocalPath));
               if (!targetID.IsNull)
                {
                  Item item = args.GetItem(targetID);
                  if (item != null)
                   {
                    this.ProcessItem(args, item);
                   }
                   return true;
                  }
               return false;
             }

          ///<summary>
          /// Processes the item.
          /// </summary>
          /// <param name="args">The arguments.</param>
          /// <param name="target">The target.</param>
           private void ProcessItem(HttpRequestArgs args, Item target)
            {
              if (Context.Item == null)
              {
                 Context.Item = target;
               } 
                    }
       }

We replace default AliasResolver with new one, using a patch file:

   <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"     xmlns:set="http://www.sitecore.net/xmlconfig/set/">
      <sitecore>
         <pipelines>
          <httpRequestBegin>
             <processor type="Sitecore.Pipelines.HttpRequest.AliasResolver, Sitecore.Kernel">
              <patch:attribute name="type">Namespace.AliasResolver,Assembly</patch:attribute>
            </processor>
           </httpRequestBegin>
          </pipelines>
      </sitecore>
  </configuration>   
  

Next blog will be about how to create lucene indexes in a multisite solution.