Skip to content
Nick Airdo edited this page Apr 24, 2015 · 43 revisions

NOTICE! ‼️

🕙 THIS DOCUMENT IS SCHEDULED FOR DELETION.

You can find most of this material in the 101 Launchpad book except these specific sections:

  • Regarding External Facing Blocks
  • Relative Paths
  • Sharing Objects Between Blocks
  • Liquid Markup and Text/Report Templating
  • Implementing IDetailBlock
  • Performance Considerations

So don't delete me until these sections find a new home.


Blocks are instances of BlockTypes that can have configurable properties (aka "Attributes"). Changing the values of those properties will typically change the behavior or default functionality of the Block. For simplicity we'll refer to BlockTypes as "Blocks".

Besides using the REST API, Blocks are the primary mechanism for accessing and changing all the data in Rock. They are the primary building blocks, and by creating and combining Blocks together on one or more pages, you can do just about anything you can imagine.

You already created an ultra-simple Hello World block, but let's continue with more details on how to really tap into the power of the RockBlock.

Developing Custom Blocks

As mentioned in the Basic Rock Concepts section, Blocks should inherit from Rock.Web.UI.RockBlock.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.UI;
using Rock;
using Rock.Attribute;
using Rock.Web.UI;
using Rock.Web.UI.Controls;

namespace com.mychurch.Blocks
{
    [DisplayName("Potluck Dinner List")]
    [Category( "Benevolence" )]
    [Description( "Provides a way for administrators to see all potluck dinners." )]
    public partial class PotluckDinnerList : RockBlock
    {

Note: Read the Naming Conventions to avoid collisions between your stuff and everyone else's stuff.

RockBlock Class Methods and Properties

Inheriting from RockBlock gives your block access to important methods and properties.

Properties

  • RockPage - The page the block is currently on. More good details about what you can do with it can be found in the RockPage section.
  • BlockId - The Id of the current block.
  • CurrentPageReference - URL for the page where the current block is located.
  • CurrentPerson - The currently authenticated (logged in) person.
  • CurrentPersonId - The currently authenticated (logged in) person's ID.
  • CurrentTheme - The relative path to the current theme and layout folder.
  • CurrentUser - The currently authenticated (logged in) user.
  • RootPath - The root URL path to the Rock application.
  • BlockValidationGroup - The unique validation group that the framework assigned to all the controls on the block
  • BreadCrumbs - The Breadcrumbs created by the page. A block can add additional breadcrumbs as described on the Breadcrumbs section.
  • RootPath - The fully qualified path to the root of the website running Rock

Methods

  • GetAttributeValue( string name ) - Gets the value for the given attribute name. This is covered in greater detail in the next section called Block Attributes.
  • IsUserAuthorized( string action ) - Checks if the CurrentPerson is authorized to perform the requested action name. See Securing Access below for details.
  • NavigateToParentPage() - will redirect the user to the "parent" page of the current block.
  • PageParameter(string name) - Checks the page route parameters and query string parameters for a parameter with the given name and returns the value if found.
  • LinkedPageUrl(string attributeKey, Dictionary<string, string> queryParams) - Returns the a url to use to navigate to a page specified in a LinkedPage attribute of the block.
  • NavigateToLinkedPage(string attributeKey, Dictionary<string, string> queryParams) - Redirects user to a page specified in a LinkedPage attribute of the block.
  • NavigateToParentPage(dictionary<string, string> queryString) - Redirects user to the current page's parent page.

Block Attributes

The Rock framework gives you an amazingly easy way to have _configuration settings _for each Block you develop with minimal coding. This feature lets you develop Blocks that are flexible, generic and configurable. We call these Block Attributes.

When a Block class is decorated with one or more Rock.Attribute field attributes, administrators can set values for each instance of the Block. (The framework automatically builds the admin UI for setting the attribute values.)

For example, let's say you wanted to let the administrator decide how many minutes to keep something cached. Just add an IntegerField attribute:

    using Rock.Attribute;

    [IntegerField( "Cache Duration", "Number of seconds to cache the content.", false, 0 )]
    public partial class HtmlContent : RockBlock
    {
        // ...

Rock's Framework UI allows admins to configure block attributes

Then you use the GetAttributeValue( attributeName ) method to get the value.

    int duration = Convert.ToInt32( GetAttributeValue( "CacheDuration") );

NOTE: You can learn much more about Block Attributes and get a full list of each available type over on the Block Attributes page.

Adding Items to the Block Configuration Slide-Out Bar

You can insert your own controls into the slide-out bar that opens when you click on the Block Configuration toolbar by overriding the GetAdministrateControls() method. This is how the HtmlContent block is able to put the edit pencil into that bar.

the HtmlContent block administration control bar

This example code from the HtmlContent block illustrates the details:

        public override List<Control> GetAdministrateControls( bool canConfig, bool canEdit )
        {
            List<Control> configControls = new List<Control>();

            // add edit icon to config controls if user has edit permission
            if ( canConfig || canEdit )
            {
                LinkButton lbEdit = new LinkButton();
                lbEdit.CssClass = "edit";
                lbEdit.ToolTip = "Edit HTML";
                lbEdit.Click += new EventHandler( lbEdit_Click );
                configControls.Add( lbEdit );
                HtmlGenericControl iEdit = new HtmlGenericControl( "i" );
                lbEdit.Controls.Add( iEdit );
                lbEdit.CausesValidation = false;
                iEdit.Attributes.Add("class", "fa fa-pencil-square-o");

                ScriptManager.GetCurrent( this.Page ).RegisterAsyncPostBackControl( lbEdit );
            }

            configControls.AddRange( base.GetAdministrateControls( canConfig, canEdit ) );

            return configControls;
        }

In a nutshell, here's what's going on:

  1. Create an empty list of controls.
  2. Use the canConfig and canEdit boolean flags to decide if you want to add your items
  3. Create an appropriate control with an event handler
  4. Add it to the list of controls
  5. Register the control with the script manager
  6. IMPORTANT: add the standard set of controlls by calling the base.GetAdministrateControls() method (unless you decide it's not appropriate for your block).
  7. Return the list of controls.

That's all there is to it.

Other UI Components To Make Your Life Easier

Rock has tons of other UI components you can use in your blocks. Whereas the Block Attributes are primarily used by the administrators, these other UI components are intended to be used by the regular users of your blocks. Be sure to read the UI Toolkit section for all the details.

User Preferences

The Rock framework has a mechanism for storing and retrieving preferences (settings) for the current user. For example, let's say your block has options for sorting or filtering, and you'd like to "remember" how the user sets them each time they use the block. This is an ideal case for using this mechanism. See the user's preferences section of the 101 Launchpad book for more details.

Fetching Values from "QueryString"

We're only guessing, but your custom block is probably going to need some data from the QueryString. Use the PageParameter( string ) method when fetching values from the QueryString or route. This method hides the complexity of having to find the value in either the QueryString or the URL route.

            if ( !Page.IsPostBack )
            {
                string itemId = PageParameter( "categoryId" );
                if ( !string.IsNullOrWhiteSpace( itemId ) )
                {
                    ShowDetail( "categoryId", int.Parse( itemId ) );
                }
                else
                {
                    pnlDetails.Visible = false;
                }
            }

Securing Access

Securing functionality access within your block is easy to do. To test whether the current user (if there is one) is allowed to perform the requested action just use the IsUserAuthorized (string action) method where action is one of "View", "Edit", or "Administrate" as seen here:

    if ( ! IsUserAuthorized( "View" ) )
    {
        message = "You are not allowed to see this content.";
        ...

    if ( IsUserAuthorized( "Edit" ) || IsUserAuthorized( "Administrate" ) )
    {
        rGrid.Actions.ShowAdd = true;
        ...

If you need to define additional custom action names to control your custom functionality, you can simply decorate your block with [AdditionalActions()] like this:

    [AdditionalActions( new string[] { "Approve" } )]

NOTE: You will need to include using Rock.Security; in your block. Once you do this, you can then use the IsUserAuthorized(string action) method to verify user authorization.

Standard Security Action Meanings

  • "View" - grants the ability to view the item's public properties
  • "Edit" - includes view access and the ability to change the item's name and other properties
  • "Administrate" - means the block's security and block's settings can be changed.

Validation

When validating a user's input, you'll need to provide some feedback to let them know when they've entered something incorrectly. Use a ValidationSummary control at the top of an edit panel with the Bootstrap standard class:

    <asp:ValidationSummary ID="ValidationSummary1" runat="server" CssClass="alert alert-danger" />

The RockBlock base class will automatically add a ValidationGroup property unique to each block instance for any RockControls, Validators, ValidationSummary controls, and Buttons that you have on your block. If one of these has already had a ValidationGroup declared, the RockBlock will update it so that it is prefixed with it's unique ValidationGroup name.

Because of this, you should only have to add a ValidationGroup to any areas of your block that are validated separately from the main block (i.e. Modal Dialogs, or Panels that are shown and hidden).

NOTE: See the GroupTypeDetail block for a good example of how to use validation group for modal dialogs.

Also, while the ASP.NET validators will perform client-side validation, any validation done by Entity Framework (i.e. data annotations and the DataValidator used by the DataTextBox, and DataDropDownList controls) is only done server-side. So if you are validating input from a ModalDialog, you may need to handle keeping that dialog shown through a postback so that the validation summary can be displayed to the user.

Preventing Validation

You can prevent a button, link, etc. from causing validation by setting the CausesValidation property to false:

<asp:LinkButton ID="btnCancel" runat="server" Text="Cancel" 
  CssClass="btn btn-link" CausesValidation="false" OnClick="btnCancel_Click" />

You'll usually want to do this on cancel buttons, etc.

Example

It will look something like this when the user runs into trouble with their input:

a standard validation summary error

The standard data field controls (DataTextBox, DataDropDownList, etc.) in your UI Toolkit will also render appropriate error conditions with the proper Bootstrap validation states as seen here:

a standard error validation state

If using a custom or regular input control, be sure to follow the Bootstrap documentation on Form control Validation states.

Regarding External Facing Blocks

Anti-Cross Site Scripting

Any blocks you create that are intended for use by the public (or even authenticated users) should be considered for hostile input such as cross-site scripting (XSS) attacks or other HTML/javascript injection attempts. Therefore, always minimally encode or scrub what the user entered when displaying it by using HttpUtility.HtmlEncode( string ) or Microsoft.Security.Application.Sanitizer.GetSafeHtmlFragment( string ) or by using another AntiXSS library.

Bad Input

Users will inadvertently make mistakes when filling out forms. Remember to:

  • Trim() white spaces when appropriate since users frequently will include them in their cut-and-paste text.
  • Always validate things like email address, phone numbers, etc. before you let them into an entity (and the database).

Relative Paths

The RockBlock, RockPage, and RockMasterPage objects all have a public ResolveRockUrl(string url) method that can be used in either a block or template file to get the resolved path to the current theme or application root folder. This method will first replace all instances of ~~/ with the path to the current theme's root folder, and then replace all instances of ~/ with the path to the current application root folder. Here's an example of how to use this property to reference a resource that is in the theme folder:

Markup:

 <img src='<%= ResolveRockUrl( "~~/Images/avatar.gif" ) %>'>

Code Behind:

 myImg.ImageUrl = ResolveRockUrl( "~~/Images/avatar.gif" );

Here's an example of how to use this property to reference a resource that is not in the theme folder:

 <link type="text/css" rel="stylesheet" href='<%# ResolveRockUrl("~/Styles/reset-core.css") %>' />

Adding References to the HTML Document Head

When a block needs to add a reference into the page Head for another asset (JavaScript, CSS, etc.) it should use one of these methods (AddCSSLink, AddScriptLink, or AddHtmlLink) from the RockPage class. The path should be relative to the layout template.

    protected override void OnInit( EventArgs e )
    {
        base.OnInit( e );

        RockPage.AddCSSLink( "~/css/bootstrap-switch.css" );
        RockPage.AddScriptLink( "~/scripts/jquery.switch.js" );

Example AddHtmlLink Usage:

 System.Web.UI.HtmlControls.HtmlLink rssLink = new System.Web.UI.HtmlControls.HtmlLink();
 rssLink.Attributes.Add( "type", "application/rss+xml");
 rssLink.Attributes.Add( "rel", "alternate" );
 rssLink.Attributes.Add( "href", blog.PublicFeedAddress );
 rssLink.Attributes.Add( "title", "RSS" );
 RockPage.AddHtmlLink( rssLink );

Sharing Objects Between Blocks

Blocks can communicate with each other through the sharing of objects. The base RockBlock class has a RockPage object that has two methods for saving and retrieving shared objects specific to current page request. Within your block, you can call:

   RockPage.SaveSharedItem( string key, object item )`
   RockPage.GetSharedItem( string key )

Example Usage:

 // try loading the blog object from the page cache
 MyBlog blog = RockPage.GetSharedItem( "blog" ) as MyBlog;
 
 if ( blog == null )
 {
     blog = blogService.GetBlog( blogId );
     RockPage.SaveSharedItem( "blog", blog );
 }

It's worth noting that the order in which loaded blocks modify these shared objects **cannot be guaranteed **without further preparation and coordination.

Caution When Saving Then Attempting to View Data

When you save a new entity using the service layer, be aware that Entity Framework will not automatically hydrate any related entities unless you use a new service. For example, a PrayerRequest has a relationship to a Category entity, and when we save a new PrayerRequest after setting its CategoryId property as shown below, the Category property is not automatically populated/hydrated -- even if you try to Get it using the same service after saving it:

    prayerRequest.CategoryId = 9;
    prayerRequestService.Save( prayerRequest, CurrentPersonId );
    prayerRequest = prayerRequestService.Get( prayerRequest.Id ); // Warning!
    var category = prayerRequest.Category; // THIS IS NULL

Instead, you need to use a new service object as shown here:

    // prayerRequest = prayerRequestService.Get( prayerRequest.Id );
    prayerRequest = new PrayerRequestService().Get( prayerRequest.Id ); // Good.
    var category = prayerRequest.Category; // Now it's there.

Liquid Markup and Text/Report Templating

Rock includes DotLiquid which makes it easy to merge field values into string templates. That means you can store a template-string and Rock (using DotLiquid) can replace things like {{ person.FullName }}) with actual values of your object (such as "John Smith").

To tap into this feature, Rock includes an string extension method called ResolveMergeFields() so any string can easily be merged with a collection of objects.

Let's look at a simple example where we have the body text of an email and a person that we're sending it to. Here a mergeObjects dictionary is passed into ResolveMergeFields and the tokens are replaced with the person's given name.

Example 1

    string emailBody = "{{person.GivenName}}, pretend this msgBody was stored somewhere.";
    Person person = new PersonService().Get( 1 );

    var mergeObjects = new Dictionary<string, object>();

    // Add a person to the mergeObjects dictionary
    mergeOjects.Add( "person", person );

    // output will be "Admin, pretend this msgBody was stored somewhere."
    var output = emailBody.ResolveMergeFields( mergeObjects );

You can read more about DotLiquid Templates on the web, but simply put, there are two types of Liquid markup called "Output" and "Tags". Output is surrounded by {{ two curly brackets }} and Tags are surrounded by {% a curly bracket and a percent %}. The example above shows only Output markup. Tags are where you can put code logic to control aspects of the template. Using this you can do some pretty complex things -- including looping.

Here is an example string template from the check-in system that will be merged to become a list of location names.

Example 2

    <ul>
        {% for location in groupType.Locations -%}
        <li>{{ location.Name }}</li>
        {% endfor -%}
    </ul>

See https://wiki.shopify.com/UsingLiquid and https://dotliquidmarkup.org/ for additional reference information.

Filesystem Location

The standard location for all custom blocks is in the Plugins folder. Create your own folder under the Plugins folder to hold any custom blocks created by your team. Put your javascripts in a Scripts folder and your CSS stylesheets in a Styles folder.

RockWeb
\---Plugins
    \---com_mychurch
        +---MyProject
        |   |   Widget.ascx
        |   |
        |   +---Assets
        |   +---Scripts
        |   |       widget.js
        |   |
        |   \---Styles
        |           widget.css
        |
        +---Scripts
        \---Styles
                myshared.css

Share your common styles and scripts across all your projects by putting them into a Styles and Scripts folder at your domain root folder.

Implementing IDetailBlock

If your custom block is a "Detail" block then you should also implement the IDetailBlock interface.

    public partial class PledgeDetail : RockBlock, IDetailBlock
    {

As seen in the above code example, we're encouraging the use of a ShowDetail( string parameter, int value ) method when appropriate to to handle the displaying of the detail of your block's 'item'. This method is required if your block implements the IDetailBlock interface. See the List Detail Pattern in the UI Guidelines section for more details about this pattern.

Performance Considerations

Page_Init vs. OnInit

There's not really any big difference besides preference. Overriding the base method (OnInit) may be slightly faster than invoking an event delegate (Page_Init), and it also doesn't require using the AutoEventWireup feature, but essentially it comes down to preference. My preference is to override the event. (I.e. use OnInit or OnLoad instead of Page_Init or Page_Load). This article discusses this in detail.

OnInit vs. OnLoad

There's a significant difference between putting code into the OnInit (Page_Init) method compared to the OnLoad (Page_Load) method, specifically in how it affects ViewState. Any change you make to a control in the Init portion of the page life cycle does not need to be added to ViewState, however, if changed in the Load portion it does. Consider a dropdown box of all the states. If you load the dropdown in the OnLoad method, all of the 50 items of the dropdown box will be added to the ViewState collection, but if you load it in the OnInit method, they will not. For performance sake, we want to keep ViewState as small as possible. So whenever possible set the properties of controls in the OnInit method. Please read this article on Understanding ViewState.

Caching

To cache methods (AddCacheItem(), GetCacheItem(), FlushCacheItem()) can be used to cache custom data across requests. By default the item's cache key will be unique to the block but if caching several items in your block you can specify your own custom keys for each item.

    // Store into cache
    int cacheDuration = 0;
    if ( Int32.TryParse( GetAttributeValue( "CacheDuration" ), out cacheDuration ) && cacheDuration > 0 )
    {
        AddCacheItem( entityValue, html, cacheDuration );
    ...

    // Elsewhere, pull from cache
    string entityValue = EntityValue();
    string cachedContent = GetCacheItem( entityValue ) as string;


    // When finished, flush the cache
    FlushCacheItem( entityValue );

How to Add Blocks To Pages?

Blocks are added to a page by adding them a zone on a page or by adding them to a zone in a layout. Adding a block to a zone in a layout will cause all pages which use that layout to automatically include that block instance.

TODO: This should be moved into an Administrative section/guide.

Clone this wiki locally