Block Development

Author:JonK
Last Updated:August 29, 2017 1:31 PM

A block requires two pieces. The first is an editor for use within the workstation; the second is a control for rendering the display, both in the workstation preview and on the display side.  All block editors must be .ascx controls written for interaction with the Titan Workstation.  Display-side renderers are almost always specialized compiled .Net controls, although simple display-sides like the Freeform block can be implemented with only an XSL.

 

Block Editor Development

In the current implementation the only supported block editor is an .ascx control.  All workstation block editor controls must:

  • Inherit from WkstBlockBase (see Step 1)
  • Provide the base class with the JavaScript name of the block (see Step 1)
  • Implement the JavaScript functions [blockName]_PackageXml and [blockName]_ChangeDetector (see Step 2)

 

In the following discussion, we will walk through the implementation of “SampleBlock”.  SampleBlock lets the author drop in arbitrary text.  This text is then displayed along with information about the logged in user.  

All of the blocks supplied with the Titan workstation implement the same workstation integration design pattern described below.  Some are as simple as the SampleBlock editor and other have more server/database interaction.  The reader is referred to the Titan code base for examples of more complex blocks.

.Net Development Note: Due to the nature of the workstation, packaging, validation and change detection must occur on the client.  ASP.Net PostBack is not a design pattern used by block editors.   In addition, ViewState is always disabled in the workstation and Session is read-only.

 

Reference: Workstation Block Data

Very similar to previous versions, the Titan workstation uses the following BlockXML structure:

<Block BlockID="##">
    <BlockTypeID>##</BlockTypeID>
    <DocID>##</DocID>
    <SiteID>##</SiteID>
    <PagePosition>##</PagePosition>
    <ClientCssFile><!-- path and name --></!--></ClientCssFile>
    <ChangesHaveOccurred>0|1</ChangesHaveOccurred>
    <NewPosition>##</NewPosition>
    <CssClassName><!-- class name --></CssClassName>
    <ContentZone>Center|Left|Right</ContentZone>
    <BlockData>
        <Elements>
            <!-- block-specific data -->
        </Elements>
    </BlockData>
</Block>

The SampleBlock block that we will use in our examples has a single XML element nested under /Block/BlockData/Elements named SAMPLEBLOCK/Data.

 

Step 1: Create the Block Folder

In CustomUISupport, under CustomBlocks, create a folder for your Block code. Either through _LocalWkst, or the Workstation of your integration environment, add a new Block in Titan Admin, providing all the required information, including:

  • Block Name: This is a single word that should describe your block.  It is the prefix you use with your JavaScript functions.   Keep it small and meaningful.  Always start it with a capital letter.   During your editor’s OnLoad, the blockName is provided to the base class. 
  • Display Block Processor or Display Transform File: Use the latter if your block contains all of the information required to render your display and that rendering can occur 100% within an XSL.  On the other hand, if you need to interact with a data store or perform C# code, you will need to use a Display Block Processor.  Most blocks use a Display Block Processor; the Freeform, FAQ and RawHTML are XSL-based.  For a block processor, provide the namespace-qualified name of your block followed by a comma and then the name of the DLL (do not include the .DLL extension).  Refer to the Admin Workstation’s on-line help (under the blue icon) for more information.
  • Editor ASCX:  Your block editor must be written as an .ascx control.  The naming convention is that all of the files associated with your block will reside in a single directory named the same as your block.  To remain consistent, create a name for your editor that is [your block name]/[your block name].ascx. 
  • Javascript Source:  Your block editor requires Javascript code and that Javascript code must reside in an external .js file.  This file is loaded by the workstation on startup.  This file should only include functions.  It should never include any top level javascript as that code will execute only once and not during the initialization of your editor.

Important Notes:

  • If the directory that you specify does not begin with a ‘/’, then it will be assumed to reside under the Blocks directory as defined by the App Control variables, DisplayBlockPath and WkstBlockPath.  On the other hand, if the directory begins with a ‘/’ then it is assumed that the directory exists in the root of both the workstation and the display. 
  • The directory in which your block data resides must be within the .Net application space of the workstation/display.  The directory cannot create its own .Net application.  If it did, .Net would not allow Titan to load your control.
  • Both the workstation and the display will need pieces of your block code.  The display only needs the display renderer; the workstation needs both the display renderer and the editor.  Make sure you upload the files to both locations if a virtual directory is not being used to share the files.
  • Your binary DLL must reside in the execution path of both the workstation and display.  This usually means the bin/customerBin directory, but your sysadmin may have configured an alternative location.  Check before installation.

 

Step 2: Create a .ASCX Control

Create an .ascx control for your block editor.  Change the base class in your code behind to WkstBlockBase and implement the marker interface INamingContainer.  WkstBlockBase provides your control with basic editor framework initialization.  In almost all circumstances your interaction with WkstBlockBase will be limited to setting the required property BlockBaseName and requesting your XML BlockData.  WkstBlockBase also exposes read-only properties for the current DocID, SiteID, BlockTypeID and ClientCssFile. 

Important Note: because more than one instance of your block editor control can appear on a single content page, your control must ensure that all control names are unique on the client.  If you are using .Net server controls and implement the marker interface INamingContainer this will be taken care of for you by the .Net framework; if you are using vanilla HTML controls (without a runat=”server” attribute) or generating the content of your block with XSL, you must provide a mechanism to guarantee unique client names, names that can be found by your JavaScript code.  For this example, we will rely on the INamingContainer and the default .Net behavior.

 

Example .ASCX

The following shows .ascx implementation the SampleBlock block editor:

<%@ Assembly Name="CustomUISupport" %>
<%@ Control Language="C#" EnableViewState="false" ... %>

<div id="<%=ClientID%>">
  <fieldset>
    <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="left">
        <label>Provide arbitrary HTML within this text area. </label>
      </div>
      <div class="right">
        <div class="checkbox_none"></div>
        <div align="center">
          <textarea class="textinput" id="textBox" runat="server" rows="20" style="width:95%"></textarea>
        </div>			        
      </div>
    </div>
  </fieldset>
</div>

Notes:

  • The first line of the .ascx file is an assembly declaration.  This tells .Net in what DLL module it can find your code behind.  Normally this is not needed, but we will deploy to a directory other than the /bin directory.  Without this, .Net will not find your .DLL.
  • The entire control is wrapped in a single <div> using the block editor’s ClientID.  The use of a containing <div> with an accessible unique ID is a best practice for AJAX and is required for proper callback behavior.
  • The SampleBlock block has only a single textarea control for data input.  In this case, the textarea control maps directly to the single XML element of data.     
  • In addition to the <div class=”fieldwrapper”> for the textarea, a second <div class=”fieldwrapper”> is used to display error information to the user.  By default it is hidden from the display.   

 

Example .ASCX code-behind

The following is the code-behind for the SampleBlock editor

public partial class SampleBlock : WkstBlockBase, INamingContainer
{
    protected void Page_Load(object sender, EventArgs e)
    {
        base.BlockBaseName = "SampleBlock" ;

        XmlNode ourData = BlockData.SelectSingleNode("BlockData/Elements/SAMPLEBLOCK/Data") ;
        if (ourData != null)
           textBox.Value  = ourData.InnerText ;
    }
}

Notes:

  • We are inheriting from WkstBlockBase, but also implementing the marker interface INamingContainer.  The latter guarantees that the client-side ID for any controls marked as runat=”server” will be unique on the display and be prefixed with our ClientID. 
  • The first step in our Page_Load is to tell the WkstBlockBase our name.  This name is used by the base class when registering our client-side JavaScript functions.  If we don’t provide the base class with our name a run-time error will occur.  The name supplied here must match the name provided for Block Name in the Wkst and is the prefix you will use with all JavaScript functions. 
  • The second operation that we need to perform is to initialize our block editor with data.  This step is block-specific.  In this case we have only a single control to initialize so we ask the base class for our BlockData, find our data and pass it off to our textarea control.
  • Your block editor will never be called during a PostBack so there is no need to conditionally perform actions based on the PostBack state.

 

Step 3: Write Workstation Integration Client-Side JavaScript

Integration between the workstation framework and your block editor is performed by the four JavaScript functions described below.  Of the four, only two, [BlockName]_PackageData(baseID) and [BlockName]_ChangeDetector(baseID), are required.  

All of the functions take a single argument, baseID.  This string field contains the clientID of your block editor control.  If you follow the design pattern of implementing the marker interface, INamingContainer, and create your controls with runat=”server”, then your controls can be found on the client as baseID + “_” + [serverControlID].   Note: The baseID value provided to your javascript functions in BlockTestHarness will not match the value of baseID provided to your javascript functions in the actual workstation.  Do not rely on a known value for the baseID variable.  Instead, treat it is a blackbox prefix.

Important Note: Your JavaScript functions should be placed in an external .js file, not in-line to your .ascx control.  If you place your functions in-line, they will not be loaded consistently by the AJAX parser.  By placing your JavaScript functions in an external .js file and registering that file via the Admin Workstation, your .js file will be loaded by the workstation framework on startup and it will be available when authors use your editor.

The four functions are:

  • [BlockName]_ChangeDetector(baseID) 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.
  • [BlockName]_ValidateData(baseID) returns true if the block is in a state that can be safely saved to the database.  You can also use this function to display error information such as missing field data to the user.  If this function is not provided, it is assumed that the block data is always valid and ready to be saved.
  • [BlockName]_ChangeMessage(baseID) 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 [BlockName]_ChangeDetector(baseID) returns true.
  •  [BlockName]_PackageData(baseID) needs to return the edited, block-specific XML fragment.  That is, the XML that will be placed in the /Block/BlockData/Elements area.

 

Example .JS
function SampleBlock_ChangeDetector(baseID)
{
    var ctl = document.getElementById(baseID + "_textBox");             
    return (ctl.defaultValue != ctl.value); 
}

function SampleBlock_ChangeMessage(baseID)
{
    return " has changed.";
}

function SampleBlock_ValidateData(baseID)
{
    var infoText = document.getElementById(baseID + "_infoText");
    tic_FormUtilities.HideField(infoText);

    var ctl = document.getElementById(baseID + "_textBox");    
    if (!ctl.value)
    {    
        infoText.innerHTML = "Content cannot be empty.";    
        tic_FormUtilities.ShowField(infoText);
        
        return false;
    }
             
    return true;
}

function SampleBlock_PackageData(baseID)
{
    var retVal = tic_Utilities.MakeXmlStartTag("SAMPLEBLOCK", 0);
    retVal += tic_Utilities.PackageXml("Data", document.getElementById(baseID + "_textBox").value, true);  
    retVal += tic_Utilities.MakeXmlEndTag("SAMPLEBLOCK", 0);
	
    return retVal; 
}

Notes:

  • No error checking is being performed.  If our controls are not present, then something seriously wrong has occurred.  Throwing an error is appropriate.
  • 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. 
  • PackageData is using a utility function to format the XML.  In this case, the arguments are first the name of the element, then the data, and then a flag indicating that the data for the element should be wrapped in a CDATA section. 
  • ValidateData is making use of a utility classes to hide and expose the <div class=”fieldwrapper”> when an error is encountered.   It is good practice to always clear the error flags before testing.  In this case it would have been just as easy to have had a fixed error message display.  However, this design pattern lends itself to more complex blocks.

 

Step 4: Write Block-Specific Server and Client-Side Support

Hook up any client-side interaction for your controls.  Note that your control cannot callback to the server except to a web service or to another page (e.g., dynamic drop-down processing).  See the TOC block editor for an example of how to implement server callback interaction.

 

Step 5: Develop the Client-Side

Block display code can be a simple as a .XSL file that transforms the user-supplied block data into HTML or it can be a specialized compiled .Net component.  Most blocks are implemented as .Net components.

 

Step 5A: XSL-based Block Display

Blocks such as the Freeform and FAQ contain everything they need to know within their block data.  These blocks don’t need to reach into the document to extract additional information (e.g., the page’s custom URL or site) nor do they need to reach into the display session to extract information (e.g., information about the logged in user).  As such, these blocks do not supply a .Net control as Display Block Processor, but instead supply a .XSL file as the Display Transform File. 

Example Display Transform File
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" omit-xml-declaration="yes" indent="yes" />

    <xsl:template match="/">
	    <xsl:apply-templates select="/Block/BlockData/Elements/FREEFORM" />	
    </xsl:template>

    <xsl:template match="FREEFORM">
	    <xsl:value-of select="." disable-output-escaping="yes" /> 
    </xsl:template>
</xsl:stylesheet>

Notes:

  • There is nothing special about this XSL.  It receives the block data and simple returns the contents of the FREEFORM node as its rendered content.  The FAQ block’s Display Transform File is only slight more complex.
  • The output of your Display Transform File must be well-formed XHTML.

 

Step 5B: .Net Control Block Display

If your block requires any information outside of its block data, you must implement a compiled .Net control.  Since the SampleBlock displays the user’s name in its output, our SampleBlock supplies a .Net control for its Display Block Processor.

A Display Block Processor must inherit from CmsComponentBase and must implement IBlockCmsComponent.  IBlockCmsComponent declares the method that will be called to provide your processor with your block’s BlockData XML.  CmsComponentBase handles common initialization and rendering.  CmsComponentBase’s overridable virtual method CmsRenderContents allows your processor to return its rendered contents at the correct point of page rendering. 

Example Display 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);
    }
}

Notes:

  • The actual rendering of our data is the result of an XSLT transform.  During initialization we store off our state data and use this information during the actual rendering. 
  • Our block assumes that our XSL is stored relative to our root; it makes no assumptions about its full path.  Instead, we delegate to the StateData.ExpandBlock call to determine the full path to our block’s XSL. 
  • InitializeCmsComponent receives the user name and password data to allow a block to make additional calls into the database (e.g., the TOC block pulls out a small Nav tree).  The password should never be rendered onto the display. Other arguments:
    • ppID – required for most calls into the database
    • applicationPath – legacy argument.  Since Titan v4 does not support installation into a sub-application, this value will always be “/”
    • userLogin and userPassword – required for most calls into the database
    • docID – the current document ID
    • block – your block data
    • blockNumber – the position of your block in the page’s block stack.
  • CmsRenderContents renders its HTML data into the provided HtmlTextWriter.  In our case we’re performing a transform directly into the HtmlTextWriter.  Other controls may opt to use the interface of the HtmlTextWriter to directly generate HTML.
  • CmsRenderContents is called from the base class’s implementation of RenderContents.  Do not override RenderContents as it contains workstation-specific code.  Also, do not call base.RenderContents from CmsRenderContents or you will enter an infinite loop. 
  • RenderBeginTag and RenderEndTag have special implementations in CmsComponentBase.  Do not override these methods.
  • The value returned from CmsRenderContents must be well-formed XHTML.

 

Step 6: Test

Test your block using the steps described in the TitanTestHarness documentation.

 

Step 7: Register and Deploy

Once your block is tested, deploy it to the Titan Admin Workstation. 

To register your block in the Titan Admin Workstation, from the Titan Administration Blocks tree item, select the ‘New’ icon in the global button area.  The workstation prompts for the name of your block.  Keep the name small, but meaningful, as it will be displayed in the menu of new block choices.

Upon clicking OK, the system creates default names for each of the required components.  Replace these system-supplied values with the values you used on your code files.   Upload your non-compiled files into the appropriate Blocks subdirectory and your DLL into /bin/customerBin directory for the workstation.  [Note: This can be performed with the Workstation upload button on the Blocks screen in the Admin Workstation].  From the Admin Workstation’s global toolbar, refresh the AppVars for the workstation.

Retest your block’s behavior in the workstation.  If you’re satisfied, copy your binary DLL to the /bin/customerBin directory of the display perform a RefreshAppVars for the display.  [Note: This can be performed from the Admin Workstation using the upload button on the Blocks screen in the Admin Workstation.  If you are not deployed as a single server system, the workstation must contain a virtual directory that points to the customerBin directory on the display].

 

Block Development Best Practices
  • Fit in.  Don’t Stand Out.  Make your editor look like the other editors.  Use the fieldset/fieldwrapper design pattern and the default styles.
  • Don’t style your display-side HTML.  Your display output should be as plain HTML as possible.  All style should be controllable from external CSS files.  Avoid unnecessary classes.  Use div’s and span’s, not tables.  Don’t use inline styles. 
  • If you’re unsure where to start, find a Base block that is similar to what you need to implement, get the sources from UISupport, read the sources and start from there. 
  • Use the standard script libraries.  In particular, /script/utilities.js has functions for manipulating XML; /script/radwrappers.js had functions for manipulating windows and web service calls; and /script/FormEditorSupport.js has significant support for parsing and manipulating html controls.
  • Don’t invent new design and/or user-interaction paradigms.  Remember that one less-than-optimal, yet consistent paradigm in a sea of 99 matching implementations is better than 100 optimal, yet unique design paradigms.  The world, both developers and users, will understand your block if you develop it using the patterns described.
  • 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.
  • Make sure that your display and your editor are generating well-formed HTML.  If the HTML is poorly formed, it may work in the harness but fail in the actual workstation. 

 

top