Block Upgrades for 7.0

Author:JonK
Last Updated:October 07, 2019 2:23 PM

The Display implementation is made up of the following code elements:

  • Block Processor - Typically a specialized .NET class that inherits from the CmsComponentBase class and implements the IBlockCmsComponent interface. The purpose of the Block Processor is to interpret the BlockData XML and perform the required initialization in order to render the HTML output through a View. An option exists however for simple blocks to use a default processor for XSL-only blocks (e.g. Freeform, RawHTML, etc). 
  • XSL or Razor View - The HTML output for a block is rendered with either an XSL or Razor View. The Titan Display-side supports either, though for the purpose of upgrading Blocks, the XSL views are the only views that should be implemented. 
  • Ajax Support - In cases where aysncronous server calls are required, an ajax web service and client-side javascript provide that functionality.

The Workstation implementation is made up of the following code elements:

  • Block Definition - A series of three .NET classes that define the model for the block, provide for the hydration of the model from BlockData XML, and communicate the definition of the Block to the Workstation UI.
  • Block Editor - A Razor View that controls the editor interface for a Block
  • JS Support - The JS Module that ties into the Workstation UI for validation and packaging of the Block configuration
  • Ajax Support - In some cases, block editors have their own Ajax support for interactive UI elements
  • CmsControls / Shared UI - Many blocks used shared UI code for common interface elements. During the upgrade process, you may need to leverage an existing CmsControl or develop a new one in order to port old shared components into the new code base.

 

Before You Start

If you haven't read it yet, please take time to review our Architectural Principles. These aren't ground breaking concepts, but they help establish the mindset you need.

The most critical concept is code simplicity and consistency.

"One less-than-optimal, yet consistent paradigm in a sea of 99 matching implementations is better than 100 optimal, yet unique design paradigms."

- Carol Tumey

To this end, keep in mind the following:

  • Maintenance Coding is so important. Take great effort to make the code you add "look like" the code that is there. You may have a different approach to NULL checking, String concatenation, looping, or any other common task... but if it doesn't match the way you see it done, you are adding confusion for the next person. 
  • Functional Consistency.  Keep in mind that the old logic was sound, and that hasn't changed. We want behaviors in the new system that match people's expectations from the old system. You're safest using the old code as-is, with maybe minor tweaks to bring pattern consistency. 
  • Simplicity. Don't over engineer a solution, especially when you are upgrading one that worked fine the way it was. Many times, the most straight forward way is the best.  Especially if it keeps the code looking the same.
  • There are always exceptions to these rules.  Please take time to ask, and feel free to promote a different way... but also be willing to accept No as an answer. 
  • No new 3rd Party libraries. That includes any NuGet Package with any dependency on anything not directly sourced by Microsoft.   No Newtonsoft. No WebGrease. No jQuery. No Unobtrusive JS.

If you are new to MVC, and are having trouble seeing how the code ports from the WebForms concepts, the following tips may help bridge the gap:

  • First, you are still coding in C#, HTML, JavaScript, XSL, CSS and T-SQL... it is the separation of concerns that is changing, and the addition of Razor.
  • Razor files are pretty much like Classic ASP programming... HTML with interwoven server-side code that heavily uses Extension Methods for generating HTML.
  • If you commonly think in WebForms concepts, you'll need to realize that for our purposes, we don't have: 
    • Web and User Controls
    • Page.Controls (the Controls collection and heirarchy of nested controls being accessible via this.Parent)
    • Page Lifecycle Events
    • ViewState
    • Postback
    • ScriptManager and the registration of Web Services
  • Our use of MVC doesn't include the "automagical" model binding to send data back to the server, so a basic knowledge about Models, Views, and Controllers should be enough to get you started.
  • Generally, when you port an ASCX control:
    • The exposed Properties of the control (as with inputs that runat="server") are moving to a class that is your "Model". 
    • The content side is moving to a Razor file, which is the "View".
    • In the case of Block Editor controls, there is already base code that acts as the "Controller", so you don't need to worry about that part.
    • Any initialization code found in page lifecycle events in the code behind (e.g. OnInit, OnPreRender, OnLoad, etc) needs to move to a new class called a "Hydrator"
  • If you ever need to port an ASMX Web Service, this is becoming a "Controller". You essentially are discarding the asmx file, and changing the asmx.cs file to inherit from a different base class and modify some method Annotations/Attributes. 

 

 

Block UPGRADE PROCESS

Start with the Display implementation. The pattern for implementing the Display-side is a much closer match to the old code and will ease you into the MVC patterns we're using in version 7. Additionally, by implementing the Display first, you get the benefit of also seeing the block render in the Workstation, which is a nice confidence boost and a helpful tool as it relates to functional testing.

Finish with the Workstation implementation, including any database scripts required to upgrade configuration and/or implemenation.  Be sure to update the point release upgrade script, and modify any related stored procedures as needed. That said, make it a priority to avoid any functional changes to stored procedures, and structural changes to BlockData XML. We want the DB upgrade to be trivial.  

In both steps, you will be porting code from the old v6 code base into the MVC pattern established for v7. You may find it helpful to have two Visual Studio environments open to cross reference. I keep the old UI code open in VS2013 while working on the new code in VS2017.

 

Upgrade Display-Side

Step 1: Block Processor

Start with the Block Processor for the Block you are upgrading. Typically, the CS file is named after the block. It will contain a class that inherits from CmsComponentBase and implements IBlockCmsComponent. 

Example v6 Block Processor
public class SampleBlockDisplay : CmsComponentBase, IBlockCmsComponent
{
    private XmlNode _blockData;

    public void InitializeCmsComponent(short appID, string applicationPath, string userLogin, string userPassword, int docID, XmlNode block, int blockNumber)
    {
        _blockData = block ;
    }

    protected override void CmsRenderContents(HtmlTextWriter writer)
    {
        XslCompiledTransform xslFile = new XslCompiledTransform();	
        xslFile.Load(Page.MapPath(StateData.ExpandBlock("SampleBlock/XSL/SampleBlock.xsl"))) ;
         
        XsltArgumentList objXslArgList = new XsltArgumentList() ;
        objXslArgList.AddParam("userCN", "", StateData.UserName);

        xslFile.Transform(_blockData, objXslArgList, writer);
    }
}

To port this code to the  v7 paradigm, understand that:

  • Each CmsComponentBase has an property called Model that is of the type object, and this is the data source.
    • Your implementation will be simpler if you use the generic CmsComponentBase<TModel> to  define a more specific type for the model.
    • If your presentation will always use an XSL (as is the case in the above example), then the specific type you should use is XslViewData which provides a way to specify the Xml data and an XsltArgumentList for the transform.
  • Each CmsComponentBase has a property called ViewPath that is the path the default View file. If none is provided, nothing will be rendered.
  • The v7 version of CmsComponentBase defines the default implementation of CmsRenderContents so you don't need to. All you need to do is write the implementation of the Initialize and most of the time the base implementation for rendering will suffice.
    • Because you don't typically need to override the rendering process, you also don't need to persist a lot of data through private Properties. In this case, the XsltArgumentList can be initialized with the parameters, and the private XmlNode isn't required to hold the Xml data until the CmsRenderContents is called
    • The v7 method signature for InitializeCmsComponent uses XElement as the type for incoming block data.
Example v7 Ported Block Processor
public class SampleBlockDisplay : CmsComponentBase<XslViewData>, IBlockCmsComponent
{
    public override string ViewPath { get; set; } = "SampleBlock/XSL/SampleBlock.xsl";

    public void InitializeCmsComponent(short appID, string applicationPath, string userLogin, string userPassword, int docID, XElement block, int blockNumber)
    {
        Model = new XslViewData { XmlData = block.CreateNavigator(), ArgList = new XsltArgumentList() };
        Model.ArgList.AddParam("userCN", "", StateData.UserName);
    }
}

 

Step 2: XSL

Most of the time, if your block processor is upgraded properly, you should have no work to do to upgrade the XSL. It should interpret and transform the XML as it did before. 

If you use XslViewData as your model, take note of the underlying Type for the XmlData property; it is IXPathNavigable. This is a more general type used for passing navigable xml. Depending on the source type (XDocument vs XElement) the XSL transform's root template may need to change the match value. This isn't prefered. Take caution so that the navigable data is rooted properly for the XSL to remain unchanged.

Step 3: Ajax Support

Blocks that have an ASMX Web Service that support client interactions, were previously registered with the ScriptManager within the Block Processor. This pattern is no longer valid. Instead, the URL to the service is handled by the MVC framework directly. These web services need to be refactored to Controller classes to allow proper Routing. 

Block Controller classes should be implemented as a .cs file in the same Block folder as all the other code files.

For Controllers, you need to define the Routes for the action methods using the Route annotation:

  • If the action method is intended to support a "Workstation only" behavior, specify the route so it starts with "Blocks/Wkst/{BlockName}"
  • If the action method is intended to support a "Display only" behavior, specify the route so it starts with "Blocks/Display/{BlockName}"

 

Sample ASMX.CS File 
[WebService(Namespace="http://www.northwoodsoft.com/")]
[ScriptService]
public class SampleService : System.Web.Services.WebService
{
    [WebMethod(EnableSession=true)]
    public string SampleMethod(int docID, string data)
    {
        ...
    }
}
Ported Controller File 
[Route("Blocks/SampleBlock/Support/SampleService/{action}")]
public class SampleServiceController : Controller
{
    [HttpPost]
    public string SampleMethod(int docID, string data)
    {
        ...
    }
}

 

Upgrade Workstation

Step 1: New CS File - Block Editor Definition Classes

In the old system we stored the path to the Block Editor ASCX control in the database. With the switch to MVC we aren't using ASCX controls. As a result, we've introduced new base classes to help define the various elements that make up Block Editors, including Model, View, and Hydrator. 

Regarding "Hydrator" Classes

If you are new to MVC, you may be questioning the "Hydrator" concept since it clearly isn't a part of the acronym.  Suffice it to say, if a Model is a simple definition of a data structure, a Hydrator knows how to fill that structure with actual content. 

In Titan, content and configuration information is communicated through XML. In v6, initializing an interface from XML content required intimate knowledge of XML organization to build proper XPath statements to query the document. In v7, a Model class will expose properties for the user interface elements, and a Hydrator class will perform the initialization of the proerties from the source XML.

The XmlBasedModelHydrator base class provides a simple API for retrieving content from XML via XPath strings:

  • ExtractValue - returns the string content of an element matching an  XPath query
  • ExtractElements - returns an enumerable of XElements matching an XPath query
  • Exists - boolean indicating whether an XPath query produced a node-set

There are many examples of model hydration from XML data in the v7 code that serve as an example of how to implement your own. Please ask if you have questions. 

Block Editor Definition

Start by creating a new CS file named for the block - [BlockName]Editor.cs - and in this file create three public classes: 

  • [BlockName]Editor - must inherit from XmlBasedEditor
    • Define public constructor that takes XElement that is the BlockData XML, passing a new instance of the block editor's view model to the base class constructor along with the XML data
    • Override the base property getter for ViewPath and initialize with the path to the Razor View that will render the block editor
  • [BlockName]EditorViewModel - must inherit from BlockEditorViewModel<THydrator>
    • This is a simple model class for the editor. You can make this whatever you choose, though there is a benefit to using a CmsControl for each property.
      • The Base class uses generics to communicate what Hydrator class is used to automatically hydrate the View Model.
    • Review the old ASCX and the codebehind to determine the Properties you need.
      • Anything being populated from the BlockData XML will likely become a property of the Model in the form of a CmsControl
      • Many times hidden inputs were used for persisting configuration for access by JavaScript. 
      • If the old editor used other custom controls, like "StylePicker", there needs to be a v7 version implemented as a CmsControl to provide that functionality through a reusable component. 
        • The StylePicker, for example, has been re-implemented as "CmsViewPicker" in the new code. 
    • There is a custom annotation that must be used on each block, BlockEditorMetadataAttribute, that provides a way to define metadata used for connecting client-side scripting code.
      • BlockBaseName - appended to "NWS.BlockEditors." to create a reference to the JavaScript object that contains the client-side implementation of the block editor.
      • EditorJsClass - if provided, sets the specific JavaScript object name that contains the client-side implementation of the block editor.
  • [BlockName]EditorHydrator - must be able to cast as IModelHydrator, and the simplest way is to inherit from XmlBasedModelHydrator<TModel>
    • The base class uses generics to communicate to the hydrator the specific View Model type that it needs to hydrate
    • This is where the BlockData XML is parsed and used to populate the Model class for the block editor

 

Example - WhatsNewEditor.cs
public class WhatsNewEditor : XmlBasedEditor
{
    public WhatsNewEditor(XElement blockData) : base(new WhatsNewEditorViewModel(), blockData) { }

    public override string ViewPath => StateData.ExpandBlock("WhatsNew/WhatsNewEditor.cshtml");
}

[BlockEditorMetadata("WhatsNew")]
public class WhatsNewEditorViewModel : BlockEditorViewModel<WhatsNewEditorHydrator>
{
    [Display(Name = "")]
    public CmsNavPicker RootDocID { get; set; }

    [Display(Name = "")]
    public CmsViewPicker ViewPicker { get; set; }

    public WhatsNewEditorViewModel(XElement data) : base(data) { }
}

public class WhatsNewEditorHydrator : XmlBasedModelHydrator<WhatsNewEditorViewModel>
{
    public WhatsNewEditorHydrator(WhatsNewEditorViewModel model, XElement data) : base(model, data, "BlockData/Elements/WhatsNew") { }

    public override void HydrateModel()
    {
        base.HydrateModel();
        string blockTypeID = base.Data.Element("BlockTypeID").Value;
        string siteID = base.Data.Element("SiteID").Value;
        var options = StateDataWkst.GetTocFormats(blockTypeID, siteID);
        var baseElement = base.Data.XPathSelectElement(base.BaseXPath);

        Model.RootDocID = new CmsNavPicker { Value = ExtractValue("/RootDocID") };
        Model.ViewPicker = new CmsViewPicker { };
        new ViewPickerHydrator(Model.ViewPicker, baseElement, options).HydrateModel();
    }
}

 

Step 2: Update Database

Modify the database point release upgrade script to change the value of the EditorInformation column in BlockTypeTemplate. The only way to correctly update is to filter to the old path of the block editor control. The new value is the .NET class name, fully qualified with namespace and assembly, of the definition class that knows about the model, view, and hydrator for the block editor. 

UPDATE BlockTypeTemplate
   SET EditorInformation = 'NorthwoodsSoftwareDevelopment.Cms.Display.WhatsNewEditor, UISupport'
 WHERE EditorInformation = 'WhatsNew/WhatsNewEditor.ascx';

 

Step 3: New CSHTML File - Editor View

Migrating an ASCX user control to a CSHTML Razor view should be very straightforward.

The following shows the v6 implementation of WhatsNewEditor.ascx. Note the following:

  • The Control declaration will be replaced by the Model declaration
  • Two tags have been registered for use in this editor - NavPicker and StylePicker. These two controls have been reimplemented (or are in the process) as CmsControl objects and make up the entriety of the configuration of a What's New block.
  • The HTML structure, and corresponding CSS styles, for Block Editors has changed. For the most part, CmsControl objects will provide the html for each field, and the only HTML scaffolding required is for creating columns and sections.
    • Wherever possible, avoid fixed value dimensions, like height or width. 
<%@ Control Language="C#" EnableViewState="false" AutoEventWireup="true" CodeBehind="WhatsNewEditor.ascx.cs" Inherits="NorthwoodsSoftwareDevelopment.Cms.Display.WhatsNewEditor" %>
<%@ Register TagPrefix="wc" TagName="NavPicker" Src="../Support/NavPicker/NavPicker.ascx" %>
<%@ Register TagPrefix="wc" TagName="StylePicker" Src="../Support/StylePicker/StylePicker.ascx" %>

<div id="<%=this.ClientID%>">
    <div class="leftcolumn">
        <fieldset style="height:695px">
            <div class="h4legend">Select Root</div>
            <div class="fieldwrapper error" style="display:none">
                <div class="left" >
                    <label>Error:</label>
                </div>
                <div class="right" >
                    <div class="checkbox_none"></div>
                    <span id="infoText" runat="server"></span>
                </div>
            </div>
            <div class="fieldwrapper">
                <div class="right">
                    <wc:NavPicker id="navPicker" runat=server Height="560px" Width="99%"></wc:NavPicker><br />
                </div>
            </div>    
        </fieldset>
    </div>
    <div class="rightcolumn">
        <wc:StylePicker id="stylePicker" runat=server />
    </div>
    <div class="clear"></div>
</div>

The proceeding example shows the v7 implementation of WhatsNewEditor.cshtml. Note the following:

  • The Model declaration should reference the class defined in the Block Editor Definition file.
  • The Layout property for all block editors should be "~/Areas/Tab/Views/Shared/EditorTemplates/BlockEditorBase.cshtml"
  • The HTML used for layout of columns is a simple div with class="w50" (meaning "width 50%"). We use a flex based layout and the columns will automatically grow to the correct height and width.
  • The Html Helper object is used whenever possible for requesting the Editor for a given model property. In the case of Blocks, these properties should almost always be a CmsControl, with their own respective EditorTemplates.
    • "ViewPicker" is the v7 name for the v6 control called "StylePicker"
@using NorthwoodsSoftwareDevelopment.Cms.Wkst
@model WhatsNewEditorViewModel

@{
    Layout = "~/Areas/Tab/Views/Shared/EditorTemplates/BlockEditorBase.cshtml";
}

<div class="w50">
    @Html.EditorFor(model => model.NavPicker)
</div>
<div class="w50">
    @Html.EditorFor(model => model.ViewPicker)
</div>

 

Step 4: Upgrade Editor JavaScript

As with Blocks in v6, the integration between the workstation framework and your block editor is handled by JavaScript.

With v7, we are re-implementing our JavaScript to take advantage of namespacing and more modern EMCAScript functionality. A number of supporting modules have been built to provide a simper API for integrating into the Workstation.  

The four primary JavaScript functions are still utilized (2 required, 2 optional), and a fifth (required) has been added. Additionally, the arguments to the functions has changed to pass DOM node references rather than ID strings, to reduce the need for DOM traversal. With the namespacing, we no longer need to prefix the public functions with the Block Name.  The five supported functions are as follows:

  • Init (required) - used to establish a ControlCollection for simpler access to the CmsControl that makes up the editor. 
  • PackageData (required) - needs to return the edited, block-specific XML fragment.  That is, the XML that will be placed in the /Block/BlockData/Elements area.
  • ValidateData (optional, but recommended) - returns true if the block is in a state that can be safely saved to the database.  If this function is not provided, it is assumed that the block data is always valid and ready to be saved.
    • You can also use this function to display error information such as missing field data to the user. However, if you are using a CmsControl, it typically will support automatic validation if the Model is provided with appropriate validation annotations.
  • ChangeDetector (required) - returns true if any changes have been made to the block.  If not provided, it is assumed that no changes have ever been made and your block will never be saved.
  • ChangeMessage (optional, but recommended) - returns a custom change string that can be displayed in the edit history area in place of the default “ changed” message.   It will only be called if ChangeDetector(baseNode) returns true.

All of the functions take a single argument, baseNode. This is a change from v6 (which was baseID). It is a reference to the DOM node that is the root of the block being edited.

The JavaScript namespace for all block editor JavaScripts is "NWS.BlockEditors.[BlockName]"

Example JS - Upgraded WhatsNew.js
NWS.initNamespace("NWS.BlockEditors.WhatsNew", function () {
    var _fieldCollection = null;

    return {
        Init: function (baseNode) {
            _fieldCollection = new NWS.CmsControls.ControlCollection("WhatsNew", null);
            _fieldCollection.InitField("RootDocID", ".BlockEditor.WhatsNew .Field.CmsNavPicker");
            _fieldCollection.InitField("", ".BlockEditor.WhatsNew .Field.CmsViewPicker");
        },

        PackageData: function (baseNode) {
            return _fieldCollection.PackageData(); // pass "json" to test output, otherwise XML will be returned
        },

        ValidateData: function (baseNode) {
            return _fieldCollection.ValidateData();
        },

        ChangeDetector: function (baseNode) {
            return _fieldCollection.ChangeDetector();
        },

        ChangeMessage: function (baseNode) {
            return _fieldCollection.ChangeMessage();
        }
    };
});

Notes:

  • All the core tasks are performed by the ControlCollection object. If you can use all CmsControl objects in your editor, this should be how easy it is. If you need to implement your own inputs, additional code will be needed to do field level validation and data extraction. 
  • PackageData only returns the block-specific data.  The caller is responsible for wrapping the XML fragment and supplying common block information such as DocID, pagePosition, etc. 
  • If you need to format your own XML, there are v7 equivalents to tic_Utilities.PackageXml and the other XML-formatting functions in the NWS.Xml  JavaScript namespace.  

 

Step 5: Testing
Fringe Cases

As you code, consider fringe cases.  What will happen if 3000 special characters are entered in a textbox that has no business holding symbols, let alone a short dissertation?

Considering fringe cases at the outset helps determine what content is appropriate for a particular field and leads you to apply reasonable limitations. Provide proper validation by setting a maxlength and/or regex pattern to a field. 

Cross-browser Unit Testing

The workstation needs to work in four browsers:

  • Chrome
  • Firefox
  • Edge
  • IE 11

With the refactoring and rearchitecting of our JavaScript framework comes many opportunities to leverage modern capabilities of EMCAScript. Unfortunately some of the most useful native features aren't (and won't be) implemented in IE11. My preference is that we not make extensive use of polyfills unless the workaround is utterly miserable to code and maintain.  Polyfills are useful, but need to be phased out eventually. I recommend coding first for Chrome and then testing in the other browsers.

Edge, surprisingly isn't too bad.

IE11 is, comparitively, a major point of frustration. Feel free to vent in your JS comments. A hearty "IE Sucks." prefacing an otherwise unnecesary piece of code, is good for the soul... and the next developer. It also marks code that could be refactored later if IE11 is no longer a requirement.

Multiple Block Tests

Remember to test more than one instance of your block on a single page.  If you follow the naming convention outlined here, your block should work well within the workstation when multiple instances of it are placed on a single page.

top