A Not So Quick Sitecore Search Primer: Part 1

After a quick foray into Sitecore Search, I decided to jot down a more thorough step-by-step of the things needed to make Sitecore Search from start to finish. All this stuff is probably very well known by Sitecore veterans, but I wanted to get something down for somebody just starting to venture into Sitecore Search.

Note: This post will use Lucene as the Search Provider.

First things first – setting up the index

Before doing any kind of search, you have to make configuration files for the index. We will start with a very basic index that will start the search. For that, you need two basic parts – first, you have to setup what items you are indexing:

      <indexConfigurations>
      <MySearchConfiguration type="Sitecore.ContentSearch.LuceneProvider.LuceneIndexConfiguration, Sitecore.ContentSearch.LuceneProvider">
          <indexAllFields>true</indexAllFields>
          <initializeOnAdd>true</initializeOnAdd>
          <analyzer ref="contentSearch/indexConfigurations/defaultLuceneIndexConfiguration/analyzer" />
          <fieldMap type="Sitecore.ContentSearch.FieldMap, Sitecore.ContentSearch">
            <fieldNames hint="raw:AddFieldByFieldName">
              <field fieldName="_uniqueid" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.String" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider">
                <analyzer type="Sitecore.ContentSearch.LuceneProvider.Analyzers.LowerCaseKeywordAnalyzer, Sitecore.ContentSearch.LuceneProvider" />
              </field>
              <fieldType fieldName="_id" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.String" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider">
                <Analyzer type="Sitecore.ContentSearch.LuceneProvider.Analyzers.LowerCaseKeywordAnalyzer, Sitecore.ContentSearch.LuceneProvider" />
              </fieldType>
              <field fieldName="category" storageType="YES" indexType="TOKENIZED" vectorType="YES" boost="1f" type="System.String" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider">
                <analyzer type="Sitecore.ContentSearch.LuceneProvider.Analyzers.LowerCaseKeywordAnalyzer, Sitecore.ContentSearch.LuceneProvider" />
              </field>
              <field fieldName="tags" storageType="YES" indexType="TOKENIZED" vectorType="YES" boost="1f" type="System.String" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider">
                <analyzer type="Sitecore.ContentSearch.LuceneProvider.Analyzers.LowerCaseKeywordAnalyzer, Sitecore.ContentSearch.LuceneProvider" />
              </field>
            </fieldNames>
          </fieldMap>

          <fields hint="raw:AddComputedIndexField">
            <field fieldName="customcontent">MyLibrary.CustomContentField, MyLibrary</field>
          </fields>

          <include hint="list:IncludeTemplate">
            <NewsTemplateID>{B179CB04-3ACC-4737-ADA0-B45D7E98C213}</NewsTemplateID>
          </include>

          <fieldReaders ref="contentSearch/indexConfigurations/defaultLuceneIndexConfiguration/fieldReaders"/>
          <indexFieldStorageValueFormatter ref="contentSearch/indexConfigurations/defaultLuceneIndexConfiguration/indexFieldStorageValueFormatter"/>
          <indexDocumentPropertyMapper ref="contentSearch/indexConfigurations/defaultLuceneIndexConfiguration/indexDocumentPropertyMapper"/>
          <documentBuilderType>Sitecore.ContentSearch.LuceneProvider.LuceneDocumentBuilder, Sitecore.ContentSearch.LuceneProvider</documentBuilderType>
        </MySearchConfiguration>
        
      </indexConfigurations>

Let’s break this down into the different sections:

Starting from the indexConfigurations, we open up a new configuration node. You can name this whatever you want, but note the name of the node, so we can refer to it later.

<MySearchConfiguration type="Sitecore.ContentSearch.LuceneProvider.LuceneIndexConfiguration, Sitecore.ContentSearch.LuceneProvider">

The next part are some options:

<indexAllFields>true</indexAllFields>
<initializeOnAdd>true</initializeOnAdd>

Next comes the reference to the analyzer:

<analyzer ref="contentSearch/indexConfigurations/defaultLuceneIndexConfiguration/analyzer" />

If you look in the Sitecore.ContentSearch.Lucene.DefaultConfigurations.config file in the App_Config folder, you’ll find that it has default index settings for a bunch of the references that are needed. In your index, you can create your own (if you need your own analyzer) and refer to it here, or you can just refer directly to the one in the default configuration node. The ref attribute points directly to that node.

Next comes all the different fields that should be indexed. Even though we have a true as an option, what this will do is index all the text in a field called _content, but if you want separate fields to refer to and search on, you’ll need to add them here.

<fieldMap type="Sitecore.ContentSearch.FieldMap, Sitecore.ContentSearch">

This is one field you have to have – this assigns a unique ID to the document in the index, so when the item in Sitecore gets updated, it doesn’t add a new document to the index – instead it just updates it. In some older versions, this is not needed.

<field fieldName="_uniqueid" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.String" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider">
 <analyzer type="Sitecore.ContentSearch.LuceneProvider.Analyzers.LowerCaseKeywordAnalyzer, Sitecore.ContentSearch.LuceneProvider" />
</field>

You’ll need to choose the indexType and storageType for each field:

storageType = “YES” or “NO“: pretty straightforward – in the sense that the value of the field is either stored in the index, or not. This is useful for when you don’t want to go back to the database to retrieve the item for values that you want to display.

indexType = “TOKENIZED” or “UN-TOKENIZED” or “NO” or “NO_NORMS

  • TOKENIZED: Any phrases with multiple words will be split up
  • UN-TOKENIZED: Phrases will be stored as a whole – the entire value of the field, essentially
  • NO_NORMS: Phrases will not be split up, same as UN-TOKENIZED and also will not be analyzed, which means that it won’t store boost factors.
  • NO: The field value won’t be searchable, and the only reason to have this option is if you have storageType = “YES”, so you can retrieve the value.

If you have indexAllFields set to true, you don’t need to specify the fields – however, if you want to refer to the fields directly as members (for custom search result classes), they need to be added.

You can add fields by name, by type, or exclude them by name or by type (in our example, we added by name):

<fieldNames hint="raw:AddFieldByFieldName">

To add by type:

<fieldTypes hint=”raw:AddFieldByFieldTypeName”>

Include fields, or exclude fields:

<include hint=”list:IncludeField”>
  <fieldId>{B179CB04-3ACC-4737-ADA0-B45D7E98C213}</fieldId>
</include>

OR

<exclude hint=”list:ExcludeField”>
  <fieldId>{B179CB04-3ACC-4737-ADA0-B45D7E98C213}</fieldId>
</exclude>

The next section is computed fields. Computed fields are great for when a field value possibly points to another field, or multiple fields, and you have to derive a value based on some specific logic. It’s also useful for if you want have related items of some sort. You can calculate the related documents’ values on the fly and add it as a one-to-one field with the document. I’ll get into this in Part 2.

There are a bunch of fields that get added by Sitecore regardless of your config:

  • _content
  • _created
  • _creator
  • _database
  • _datasource
  • _displayname
  • _editor
  • _fullpath
  • _group
  • _indexname
  • _language
  • _latestversion
  • _name
  • _parent
  • _path
  • _template
  • _templatename
  • _updated
    _
  • version

I called out _latestversion because this will be important when you do the searches – when you have multiple versions of the same item, it gets indexed as separate documents, so when you search, you have to make sure you get the latest one. This only really matters on the CM server for previewing, because the web database always only has one version always.

<fields hint="raw:AddComputedIndexField">

Next step is to add the type of templates you want to index. The node name doesn’t really matter – you can name it anything, just include the GUID of the template in the node.

<include hint="list:IncludeTemplate">
     <NewsTemplateID>{B179CB04-3ACC-4737-ADA0-B45D7E98C213}</NewsTemplateID>
</include>

Alternatively, you can choose to include all template, and put a directive to exclude the templates you don’t want to index.

<Exclude hint=”list:ExcludeTemplate”>
     <NewsTemplateID>{B179CB04-3ACC-4737-ADA0-B45D7E98C213}</NewsTemplateID>
</include>

And then, you have to define some other values, such as the field readers, valueformatters, document property mappers, and builder type. You can point them all to the default config node.

<fieldReaders ref="contentSearch/indexConfigurations/defaultLuceneIndexConfiguration/fieldReaders"/>
<indexFieldStorageValueFormatter ref="contentSearch/indexConfigurations/defaultLuceneIndexConfiguration/indexFieldStorageValueFormatter"/>
<indexDocumentPropertyMapper ref="contentSearch/indexConfigurations/defaultLuceneIndexConfiguration/indexDocumentPropertyMapper"/>
<documentBuilderType>Sitecore.ContentSearch.LuceneProvider.LuceneDocumentBuilder, Sitecore.ContentSearch.LuceneProvider</documentBuilderType>

Once you’ve setup what you are indexing, next is to step define the actual index:

<configuration type="Sitecore.ContentSearch.ContentSearchConfiguration, Sitecore.ContentSearch">
        <indexes hint="list:AddIndex">
          <index id="my_index_name" type="Sitecore.ContentSearch.LuceneProvider.LuceneIndex, Sitecore.ContentSearch.LuceneProvider">
            <param desc="name">$(id)</param>
            <param desc="folder">$(id)</param>
            <!-- This initializes index property store. Id has to be set to the index id -->
            <param desc="propertyStore" ref="contentSearch/indexConfigurations/databasePropertyStore" param1="$(id)" />
            <configuration ref="contentSearch/indexConfigurations/MySearchConfiguration" />
            <strategies hint="list:AddStrategy">
              <strategy ref="contentSearch/indexConfigurations/indexUpdateStrategies/rebuildAfterFullPublish" />
             </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>master</Database>
                <Root>/sitecore/content/User Content/Site Level/Mindshift/Resources</Root>
              </crawler>
            </locations>
            <enableItemLanguageFallback>false</enableItemLanguageFallback>
            <enableFieldLanguageFallback>false</enableFieldLanguageFallback>

          </index>
        </indexes>
      </configuration>

This starts out by naming the index:

<indexes hint="list:AddIndex">
  <index id="my_index_name" type="Sitecore.ContentSearch.LuceneProvider.LuceneIndex, Sitecore.ContentSearch.LuceneProvider">

Some requisite parameters for index locations, etc.

<param desc="name">$(id)</param>
<param desc="folder">$(id)</param>

Next thing to note is the reference provided to what the index should store. Here is where we point the ref attribute to the index configuration we made earlier:

      <configuration ref="contentSearch/indexConfigurations/MySearchConfiguration" />

Next is the add strategy section – this section defines how the indexes are updated, for both CM and CD. Essentially, it defines how/when indexes are updated when items are added/updated. For basic indexing, I’ve added rebuildAfterFullPublish which will rebuild the index on all remote servers after a publish.

          
<strategies hint="list:AddStrategy">
      <strategy ref="contentSearch/indexConfigurations/indexUpdateStrategies/rebuildAfterFullPublish" />
</strategies>  

For all the different index update strategies, go here: http://bit.ly/2h6FDCv

The next important part is the crawler. We will is the default crawler – there are many implementations of crawlers out there, and if you have a need to index your items in a very specific way, you can inherit from the default crawler and build upon it. In which case, that is the crawler type you would specify here.

   
<locations hint="list:AddCrawler">
              <crawler type="Sitecore.ContentSearch.SitecoreItemCrawler, Sitecore.ContentSearch">
                <Database>master</Database>
                <Root>/sitecore/content/User Content/Site Level/Resources</Root>
              </crawler>
</locations>

You also have to specify the root of the content tree where indexing will start. The crawler will traverse from there.

   
<locations hint="list:AddCrawler">
<Root>/sitecore/content/User Content/Site Level/Resources</Root>
          

Last but not least, you need to surround both of these with:

   
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <contentSearch>

Once this is done, you can deploy and you should see your indexes in the /data/indexes folder. You can use SPE (Sitecore Powershell Extensions) or a program like Luke Index Viewer to check the indexes and the fields being indexed.

Before you deploy to CM and CD, you must make sure that you follow the configuration file setup, as it has a bunch of indexes that need to be disabled for CD – if they aren’t disabled, errors get thrown, and interferes with your custom indexes. Go here for the configuration options: http://bit.ly/2fYtJ8y

In Part 2, we’ll get into the code on how to perform basic searches.

A Very Quick Sitecore Search Primer

I must admit, I don’t write searches for our Sitecore sites that often. Search got totally revamped in 7.0, and introduced LINQ based providers, with Sitecore ContentSearch. I recently had to rewrite the search for a site that is being re-done in 8.2, and since the information is a little old, I had a hard time finding a top-to-bottom information on how to set this up. Luckily, the community came to the rescue once again. I was able to find all the information from different resources, and I wanted to jot it down all in one place, for anyone trying to implement a basic Lucene based search for the first time. So, here it is.

First things first. A soup to nuts overview of the ContentSearch API by Søren Engel

https://soen.ghost.io/a-re-introduction-to-the-contentsearch-api-in-sitecore-part-1/
https://soen.ghost.io/a-re-introduction-to-the-contentsearch-api-in-sitecore-part-2/
https://soen.ghost.io/extending-the-default-contentsearch-functionality-in-sitecore/
http://www.bekagool.com/news-and-insights/sitecore-7-search-a-quickstart-guide

Setting up an Index – getting all the settings correct in the index configuration

http://www.newguid.net/sitecore/2015/configure-sitecore-content-search-indexes/
http://www.mattburkedev.com/sitecore-7-contentsearch-tips/
https://sdn.sitecore.net/upload/sitecore7/75/sitecore_search_and_indexing_guide_sc75-a4.pdf
http://bit.ly/2dKNJKB

Doing the actual search – this includes computed Fields, FieldReaders, and using ContentSearch for more real world use cases.

Indexing DateTime fields – Sitecore 7 Content Search
http://getfishtank.ca/blog/building-dynamic-content-search-linq-queries-in-sitecore-7
https://laubplusco.net/custom-field-indexing-new-sitecore-7-content-search-api/

In addition, there is a massive list of help links for Sitecore: http://sitecore.link/ – which also includes a TON of links on search. After you are done with the above, go ahead and peruse the Search section of the site for even more goodies!

#Protip: Reading Fields from Template Field

If you add any fields to the Template Field item (because you want to customize something for all fields), you’ll need to read individual fields from the item that defines the field – which is Template Field. Very easy – just read from InnerItem.Fields:

Item i = //get your item
foreach (Field field in i.Fields)
{
     var fieldValue = field.InnerItem.Fields["FieldName"].Value;
}

Sitecore Xdb Security/Connectivity Details

What’s xDB Cloud?

As of version 7.5, Sitecore introduced a whole new chapter in its Experience Management Repertoire, which is known as xDB. xDB changed how Sitecore interacts with Customer interactions, and opened up a whole lot of opportunities, and increased scalability exponentially. The key factor in how this was done is the backend of xDB, which is MongoDB. MongoDB is a NoSql database which allows a very high throughput, allowing Sitecore to increase the speed, and scale up data read/write by crazy amounts. For enterprise level sites that get a lot of visitors, this is very good news as Sitecore steps into the world of Big Data.

Given Sitecore is built on top of the Microsoft Technology Stack, there could be some resistance from some companies to jump into a different technological area. Sitecore thought about this, and as of 8.0, they introduced xDB Cloud, a cloud based service for Sitecore to implement xDB, without having to host MongoDB themselves, or go for a cloud solution where they would have to do additional setup for processing and aggregation. This makes it easier to bridge the gap for some customers who were hesitant initially, as they don’t really have to deal with the details of adding on a different type of database to their existing infrastructure and do not need to acquire additional licensing for processing/aggregation/reporting.

This post is not really about xDB Cloud and its features, but if you are interested, there are two very good articles on this.

Firewall Security

Being a cloud service, Sitecore provisions xDB Cloud via a combination of cloud providers (Azure, Mongo Lab, and sometime AWS), so you will need to add a few firewall rules to make this work. For most hosting designs, the CD servers (which interact with Xdb Cloud), are situated in the DMZ, and according to best practices, access (both inbound and outbound) to servers in the DMZ are completely locked down, i.e. nothing is open unless otherwise specified. So one thing you will need to is give your network techs information about what firewall rules to create.

This becomes a little tricky, because xDB Cloud doesn’t connect to one endpoint, and all those endpoints are cloud based, so they are not static. A lot of the documents from Sitecore will say that you need to open outbound connectivity to the azure dynamic Ip range (as stated by http://www.microsoft.com/en-us/download/details.aspx?id=41653). However, any security conscious network department will balk at doing something like that from a DMZ server, so we need to get more specific. Below are the endpoints xDB Cloud needs, and you will need to make a firewall rule for each endpoint:

1. The Discovery service

The discovery service is the endpoint that Sitecore first connects to – to find the information for all the other assets it needs. CM and CD directly connects to the discovery service. This endpoint is located at discovery-xdb-cloud.sitecore.net, and connects on port 443, with HTTPS protocol.

2. Collections Repository

These are the main Mongo DB collections. Every deployment gets a separate set of hosts and ports. These hosts are usually set at the time of deployment and is unique to each deployment. After provisioning a new deployment, you will need to use the REST API Web Service for xDB to find your deployment information.

Some caveats – a collections repository contains three endpoints:

  1. The primary host
  2. The secondary host
  3. The arbiter

Note: protocol here is TCP/IP and TSL/SSL – more on that below.

However, the REST webservice will only tell you about the hosts and their ports, and doesn’t return any information about the arbiter. You will need open a support ticket to get the host and port of the arbiter for your deployment.

3. Reporting Service

The reporting service fetches reporting data from various data sources (for example, the collection or reporting databases) to use in Sitecore reporting applications, such as Experience Analytics. This is also unique per deployment. Also not available from the REST Webservice. You need a support ticket for this as well. This usually connects on port 443, with HTTPS protocol.

4. Search Service

The Search Service provides indexing capability and retrieval of information from the collections. Another endpoint that is unique per deployment, and needs a support ticket. This usually connects on port 443, with HTTPS protocol.

Additional Connectivity for the Collections Repositories

As noted, the connections to the collections repositories uses TCP/IP and TSL/SSL protocol. A client (Mongo Driver) communicates to MongoDB server through TCP/IP connection using SSL encryption. In order to do this, it must install an SSL cert on the local machine (which it does the first time when trying to connect) Whenever any TLS/SSL connection goes out from a windows machine based on the SSL cert installed, it checks the master revocation list location for that certificate, to make sure that certificate is still valid. Since all outbound traffic on the DMZ servers are closed by default, this specific connectivity is blocked.

For SSL-enabled MongoDB deployments, mLab/MongoLab signs all certificates certificate using a root certificate of the DigiCert Global Root CA, and so we need the certificate revocation lists for Digicert.

The URLs of CRL servers are embedded in the certificate (also listed below – but check the certificate on your machine to make sure they are the same):

CRL Distribution Points:
URL = http://crl3.digicert.com/ssca-sha2-g3.crl
URL = http://crl4.digicert.com/ssca-sha2-g3.crl

Authority Information Access
URL = http://ocsp.digicert.com
URL = http://cacerts.digicert.com/DigiCertSHA2SecureServerCA.crt

Check the CRLs (Certificate Revocation Lists) and Revoked Certificates section in the following article for port information: https://www.digicert.com/util/utility-test-ocsp-and-crl-access-from-a-server.htm (lest it changes from when this post is published).

Conclusion

xDB Cloud is a very easy way to foray into Experience Analytics as offered by Sitecore, but when getting into network security of DMZ servers, there are a few hurdles to cross. This is not documented as clearly as it should be, so hopefully some of the points here will help you set your networks techs at ease, and smoothly get you all the access you need for Sitecore.

#Protip: Implement permissions to read/write/delete an item of a certain type in Sitecore

I recently had to implement security where users couldn’t delete items, except an item of a certain type. I never had to implement anything like that before, and I was trying all sorts of ways to do this, but the answer is very simple. I’m sure seasoned Sitecore developers know this already, but if you didn’t – configure the permissions in the Security section of the Standard Values of the template. Done.

SV_security

My Year as a Sitecore MVP

Last year, I was fortunate to be awarded my first Sitecore MVP award. The Sitecore MVP Award celebrates the most active Sitecore community members from around the world who provide valuable online and offline expertise that enriches the community experience and makes a difference. As you can guess, it’s a big deal for me.

I wanted to write something about this last year when I first received it, but I wanted to experience a year as an MVP and then write my experiences, hopefully giving an insight into the life of a Sitecore MVP.

Overall Experience

Sitecore treats their MVP’s really well. From early release previews to future roadmaps, MVPs are a very integral part of the Sitecore Product Evolution. There is a constant communication about feature development, enhancements, etc, and MVPs get to pitch in with their feedback to great extents. There are plenty of meetings/seminars and expert panels throughout the year that we can participate in, see new features in beta, etc. In general, the MVPs are part a very big part of the Sitecore Community. As part of the community involvement, I was quoted in the Sitecore White Paper for the initial review of Sitecore 8.1

Sitecore MVP Summit

One of the perks, amongst many others is an annual Sitecore MVP Summit that occurs with all the MVPs and the key personnel from Sitecore. This event is a two to three day event with all sorts of goodness. There is product roadmaps, upcoming features, breakout sessions, plenty of treats. The 2015 MVP Summit was in New Orleans, LA – home of Mardi Gras. We were unfortunately not there at the right time, but we were treated to a night at alligator farm, complete with a band and great food. We spent three days in New Orleans, got to mingle with other MVPs from around the world, got a mountain of information and some cool swag.

On day one, it was mostly meet-n-greet, and I got to meet (in person) all the MVPs I met online. Also got to take in a bit of the French Quarter.

IMG_7380

IMG_7580

IMG_7393

IMG_7415

IMG_7409

IMG_7429

IMG_7434

IMG_7580

On day two, the sessions took up most of the day and in the evening we went to the alligator farm, and went on a safari tour of the bayou.

IMG_7495

IMG_7491

IMG_7462

Day three was also full of sessions, and that was just the beginning, as we segwayed into the SUGCON US.

IMG_7396

IMG_7540

Onwards..

I was recently awarded the Sitecore MVP award 2016, for the second time. I am honored and grateful that I am fortunate enough to be a part of this esteemed group and that I get to interact with this group day in day out. Looking forward to another year of awesome activities and even more knowledge!

A Richer Experience Editor Editing Experience: Part 2

This is the second part of a two part series post, where we look into maximizing the experience of a content editor, by providing them with on screen help and guidance. This is a remake of an older approach that I originally wrote about (using webforms). The idea is the same, but the approach is a little different. The end result for both are the same – you can see more about the goal of the endeavor in the original posts:

For Sitecore MVC, we changed it around a little bit to add some more flexibility, and update for MVC:

Where we left off…

We left off where we created all the views – so, we created our main view (the one we hook up to the View Rendering in Sitecore). We then created the Display View, and the Edit View. We also created the items based on the Help Template.

template5

 

template2

 

CustomView.cs

Now, we will then create a Base class that all views will inherit from, called CustomView.cs. We will use this view to centralize the logic:


public abstract class CustomView<T> : WebViewPage<T>
    {
        
	private string _viewName;
        private string _viewFolder;

	public MvcHtmlString ShowModalView(string displayViewName, string editViewPath, string displayViewPath, object theModel, bool useSeparateViews, string helpViewName = null)
        {
            return ShowModalView("PageEditHelp", "Common", "ShowHelp", displayViewName, editViewPath, displayViewPath, theModel, useSeparateViews, helpViewName);
        }

       
	public MvcHtmlString ShowModalView(string controller, string area, string action, string displayViewName, string editViewPath, string displayViewPath, object theModel, bool useSeparateViews, string helpViewName = null)
        {
            //if Sitecore is in page editing mode, then execute the pageedit controller action and return the result to the screen
            //this will check what help needs to be displayed, and then return the appropriate help view
            if (IsInPageEditingMode)
            {
		return Html.Action(action, controller, new { Area = area, model = theModel, viewname = displayViewName, editViewPath = editViewPath, useSeparateViews = useSeparateViews, ViewFolder = ViewFolder, helpViewName = helpViewName });
        
            }
            //if Sitecore is NOT in page editing mode, and the item has a datasource, then show the regular display view - we are probably in just a regular preview mode.
            else if (HasDataSource)
            {
                return Html.Partial(displayViewPath, theModel);
            }
            //if Sitecore is not in page editing more, and there is no datasource, return empty string - there is nothing to display (also in preview mode)
            else
            {
                return new MvcHtmlString("");
            }
        }

     
        private void GetViewProperties()
        {
            /*
                If the view being used is: /Areas/Common/Views/Media/RichText.cshtml
                 - this.VirtualPath would be: /Areas/Common/Views/Media/RichText.cshtml
                 - viewFileName would be: RichText.cshtml
                 - _viewName would be: RichText
                 - _viewFolder would be: /Areas/Common/Views/Media/
            */

            string[] segments = this.VirtualPath.Split('/');
            string viewFilename = segments.Last<string>();

            _viewName = viewFilename.Split('.').First<string>();
            _viewFolder = this.VirtualPath.Replace(viewFilename, "");
        }

        public string ViewName
        {
            get
            {
                if (String.IsNullOrEmpty(_viewName))
                {
                    GetViewProperties();
                }

                return _viewName;
            }
        }

        public string ViewFolder
        {
            get
            {
                if (String.IsNullOrEmpty(_viewFolder))
                {
                    GetViewProperties();
                }

                return _viewFolder;
            }
        }

        public string DisplayViewPath
        {
            get
            {
                return (ViewFolder + ViewName + "Display.cshtml");
                
            }
        }

        public string EditViewPath
        {
            get
            {
                return (ViewFolder + ViewName + "Edit.cshtml");
            }
        }

        public string GenericPageEditHelp
        {
            get
            {
               return ("GenericPageEditHelp");
            }    
        }

    }

To make sure your views inherit from this class, you need to set the base inherit class in the web.config inside the Views folder:


<system.web.webPages.razor>
		<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
		<pages pageBaseType="MyNamespace.CustomView">
      <namespaces>
	<add namespace="Sitecore.Mvc" />
        <add namespace="System.Web.Mvc" />
        <add namespace="System.Web.Mvc.Ajax" />
        <add namespace="System.Web.Mvc.Html" />
        <add namespace="System.Web.Routing" />
      </namespaces>
    </pages>
</system.web.webPages.razor>


What’s going on in CustomView.cs?

The Properties

In the base class for the Views, we implemented a few properties – these properties derive from the existing View – e.g. if the View file is HoverTitlePanel.cshtml:

  • DisplayViewPath: the value of this property will be /Areas/[folder structure]/]HoverTitlePanelDisplay.cshtml
  • EditViewPath: the value of this property will be /Areas/[folder structure]/]/HoverTitlePanelEdit.cshtml
  • ViewName: the value of this property will be HoverTitlePanel
  • ViewFolder: the value of this property will be whatever folder this View is placed in
  • GenericPageEditHelp: this is just a static string property to designate a Help View that will be used by default, if no specific View is set for the Help View

We also implemented a method, called ShowModal. The main purpose of this method is to decide what view to show. If you remember, we created three different views, and we call this method in the main View – the one that is connected to the Sitecore View Rendering (see below).

The ShowModal Method

The ShowModal method is responsible for deciding whether to call the controller to show the help instructions, or just display the view. For each rendering/module .cshtml file, instead of showing the data from the Model, we call this method:


@model Hover_Title_Panel
@{
    Layout = null;
}

@ShowModalView(ViewName, EditViewPath, DisplayViewPath, Model, false, GenericPageEditHelp)

The parameters for the method are based on the properties as part of CustomView – the other parameters are:

  • UseSeparateViews: a boolean value to designate whether to use separate views for the help views i.e. one for No-Datasource, one for RegularEdit. This is completely optional, as stated earlier. Pass in false to use a single view.
  • HelpViewName: this specifies a specific view name to use for the help views. If this is not passed in, it uses the GenericPageEditHelp view, which can be passed in explicitly (or not, because it takes a default null value)

 

PageEditHelp Controller

We have a PageEditHelp controller, which has the logic of what help view to show:


public class PageEditHelpController : CustomController
 {
 //
 // GET: /Common/PageHelp/
 public ActionResult Index()
 {
 return View();
 }

 public PageEditHelpController(ISiteService siteService) : base(siteService)
 {

 }

 public ActionResult ShowHelp(BaseModel model, string viewname, string editViewPath, bool useSeparateViews, string viewFolder, string helpViewName = null)
 {
 //do editing logic here
 //if there is no data source, then display no datasource help and pass the "[viewname]-nodatasource.cshtml"
 //if there IS a datasource, but it needs items, then display the "[viewname]-nodataitems.cshtml"
 //if there are data item, then just show the basic editor instructions "[viewname]-editinstructions.cshtml"

 //instantiate new RenderingHelp Model
 RenderingHelp rh = new RenderingHelp();

 //set the RenderingHelp Model property to the actual model (the one needed for the view)
 rh.Model = model;

 //set boolean to see if it has a datasource or not (will be used to decide whether to show the regular edit or display rendering)
 rh.HasDataSource = RenderingContext.Current.Rendering.DataSource.IsGuid();

 //Get help item
 Page_Editor_Help helpItem = GetHelpItem<Page_Editor_Help>(viewname, viewFolder);

 //if no help item was found, fail with showing just the edit view path 
 // todo: if helpItem is not found and there is no datasource. then return a return helpitem
 // todo: if helpItem is not found and there is a datasource, them return the editviewPath

 if (helpItem == null &amp;amp;&amp;amp; !rh.HasDataSource)
 {
 return View(editViewPath, model);
 }
 else
 {
 //no help item found. Set a default empty help item
 helpItem = Sitecore.Context.Database.GetItem(SiteSettings.GlobalPageEditDefaultHelpPath);

 //set the help text based on what step the view is in.
 //use GetHelpContent() method to get the item that holds the help content, and get the field that we need
 rh.HelpText = GetHelpContent(helpItem, false, ref viewname, useSeparateViews);

 

 //pass the edit view path (id HasDataSource is 'true', then show the edit rendering)
 //Note: if the creator of the view decides that they won't need a separate view for editing, then they will just pass the display view path for the editviewpath
 ViewBag.EditViewPath = editViewPath;

 if (!String.IsNullOrWhiteSpace(helpViewName))
 {
 return View(helpViewName, rh);
 }
 else
 {
 return View(viewname, rh);
 }
 }
 }

 private Tmodel GetHelpItem<Tmodel>(string viewname, string viewFolder) where Tmodel : BaseModel, new()
 {
 try
 {
 Item item = Sitecore.Context.Database.GetItem(GenericHelper.EnsureSlashesAtEnd(SiteSettings.GlobalPageEditHelpPath) + viewname);

 //didn't find item in the root directory, so look in first level folder
 if (item == null)
 {
 //if we have a view folder, then try to look at least one level up
 if (!String.IsNullOrEmpty(viewFolder))
 {
 //remove beginning and trailing slashes
 viewFolder = viewFolder.Trim('/');

 //find the first level folder, and look up the item there
 string[] folders = viewFolder.Split('/');
 string folder = folders.Last<string>();

 item = Sitecore.Context.Database.GetItem(GenericHelper.EnsureSlashesAtEnd(SiteSettings.GlobalPageEditHelpPath) + folder + "/" + viewname);
 }
 //if STILL not found, then return null
 if (item == null)
 {
 //TODO: log that help item was not found
 return null;
 }
 }

 Tmodel obj = new Tmodel();
 obj.SetModel(item);

 return obj;
 }
 catch
 {
 //TODO: log that help item was not found
 return null;
 }
 }

 
 public string GetHelpContent(Page_Editor_Help helpItem, bool needDataitems, ref string viewname, bool useSeparateViews)
 {
 

 /*
 
 if there is no datasource, then display the no-datasource field - so that they can add in the datasource
 if there IS a datasource, but this rendering needs data (such as other items), then show no-dataitems field - so that they can add in the items needed (add one by one)
 if both the datasource and at least one data item is there, show the regular page editor instructions, so they can edit the content.
 
 This also using a separate view for when there is no datasource, vs when there is no dataitems, vs when there is a regular edit.
 However, it is possible that not all views are going to be complicated enough that there will be a need to show three separate views - 
 * Chances are most of the views will be simple single datasource item views. 
 * So added a boolean parameter to see if the different datalogic needs to be performed to show the appropriate view.
 * If the parameter is set to 'true', then we perform the logic to set the viewname to the appropriate view.
 * If the parameter is set to 'false', then we just use the viewname as it is, and the same view should exist within the 'PageEditHelp' view folder
 
 If you need to have separate views, name them accodingly:
 * If the name of the view is "HoverTitlePanel", the corresponding viewnames should be:
 * HoverTitlePanel-NoDatasource
 * HoverTitlePanel-NoDataItems
 * HoverTitlePanel-RegularEdit
 
 * When not using separate view, it will just be HoverTitlePanel
 */


 if (!RenderingContext.Current.Rendering.DataSource.IsGuid())
 {
 if (useSeparateViews)
 {
 viewname = viewname + SiteSettings.ViewSuffixForNoDatasource;
 }

 return helpItem.No_DataSource_Help.Raw;
 }
 else if (RenderingContext.Current.Rendering.DataSource.IsGuid() &amp;amp;&amp;amp; needDataitems)
 {
 if (useSeparateViews)
 {
 viewname = viewname + SiteSettings.ViewSuffixForNoDataItems;
 }
 return helpItem.No_Data_Help.Raw;
 }
 else
 {
 if (useSeparateViews)
 {
 viewname = viewname + SiteSettings.ViewSuffixForRegularEdit;
 }
 return helpItem.Page_Editor_Instructions.Raw;
 }
 
 }
 }

How it connects together

Now that we have all the views, and the PageEditHelpController, and CustomView.cs base class for all the views, we can step through what happens:

1. In the View Rendering view, call the ShowModal Method

Majority of the parameters are available in the base class, and doesn’t need to be passed in, but we are passing it explicitly for the purpose of this example. Note that we can pass in the DisplayViewPath for the editing view path parameter as well, if we have a simple view that doesn’t require a different structural layout.

2. ShowModal Executes

When the ShowModal method is called, it checks to see if Sitecore is in Page Editing mode. If this is true, it executes the PageEditHelp controller’s ShowHelp action. If it is not in Page Editing mode, and the rendering/module has a datasource set, then just show the regular display view. If a datasource is not set, return empty string, so the rendering/module doesn’t error out.

3. PageEditHelp/ShowHelp Executes

This method first initializes the model for the help views and then it tries to get the help item, based on the ViewName parameter. If it finds the item, it then sets the appropriate values for the Model for the rendering help views. If it doesn’t find a help item, but a datasource has been set, it returns the editing view. If it doesn’t find a help item, it sets default help item before returning the help view.

The help views are very simple:


@model RenderingHelp
@{
    Layout = null;
}


<div class="pagehelp">
    @Html.Raw(Model.HelpText)
</div>


@{ var newModel = ConvertModel<Hover_Title_Panel>(@Model.Model);}

@if (Model.HasDataSource)
{
    @* For simple views, the edit panel is empty, and we are using the display view*@
    @* For complicated views, switch this to use the edit view.*@
    @Html.Partial((string)ViewBag.EditViewPath, newModel)
}

As mentioned, you don’t have to make a specific help view for each View Rendering view. There is a generic help view that can be used for all renderings. Don’t pass in any parameter for HelpViewName in ShowModal. The generic page edit help view looks like this:


@using System.Reflection

@model RenderingHelp
@{
	Layout = null;
}


@if (Model.HelpText.Trim().Length > 0)
{

<div class="pagehelp">@Html.Raw(Model.HelpText)</div>

}

@{
	MethodInfo convertModelMethod = typeof(RenderingHelp).GetMethod("GetActualModel");
	MethodInfo generic = convertModelMethod.MakeGenericMethod(Model.Model.GetType());
	var newModel = generic.Invoke(Model, null);
}

@if (Model.HasDataSource)
{
	@* For simple views, the edit panel is empty, and we are using the display view*@
	@* For complicated views, switch this to use the edit view.*@

	@Html.Partial((string)ViewBag.EditViewPath, newModel)
}

Conclusion

This may seem complicated, but once the foundation is done, the only work needed to maintain this is to make two views per View Rendering. You can have as little or as much control over the views:

  • Required: You have just the two views per View Rendering
  • Optional: You can have three views per rendering (if you want to have a separate view for editing)
  • Optional: You can have a specific help view for help for each View Rendering
  • Optional: You can have a specific help view for each step in the editing process (no datasource, no data, and regular edit)

All of the views are created based on naming convention (like asp.net MVC) and so there is really no coding involved, unless you want to get granular in your display.

We have had really good feedback from our users with this approach, as even a novice can walk through a content editing process without a lot of training. Eventually I would like to improve this to hook into a pipeline that adds the ‘Display’ and ‘Edit’ suffix automatically and have this at a more foundational level. I would also like to make sure this works for controller renderings. Would definitely love some input – maybe all this can be done in a much smoother way, so fire away!