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!

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s