January 05, 2024 Web Development

Avoiding Code Repetition in 2SXC Razor Templates in the DNN CMS

One of the most important and impactful ways to improve the maintainability of a codebase is to avoid code repetition as much as possible. By not keeping multiple copies of the same code in different places, you reduce the number of instances that must be updated if needed. In a framework like React, this can be achieved easily by simply placing the reusable code into a component that can be used and referenced elsewhere.

In the context of 2SXC, things work differently. Views are rendered via Razor templates and do not support nesting in the same way as a JavaScript framework. Luckily, there are still ways that we can separate reusable logic & markup to achieve this goal.

C# Functions in Razor

If the portion of code being reused is simply a block of logic handling basic data types, like for a mathematical calculation or string manipulation. In that case, we can define and call a local C# function within the Razor template. This approach is best for situations where the defined function is only needed within that template. To do this, simply create a @function block at the top of the file and define them as you would in C#. Below is an example that strips HTML tags from a WYSIWYG input:

@inherits Custom.Hybrid.RazorTyped
@using ToSic.Razor.Blade;
@using System.Text.RegularExpressions;

@functions
{
    string StripHTMLTags(string input)
    {
        string pattern = @"(<([^>]+)>)";
        Regex rgx = new Regex(pattern);
        string result = rgx.Replace(input, "");
        return result;
    }
}>

<div @Kit.Toolbar.Default(MyItem)>
    @{
        string unsanitizedString = MyItem.Get<string>("Text");
    }

    Unsanitized: @(unsanitizedString)
    <br />
    Sanitized: @StripHTMLTags(unsanitizedString)
</div>


C# Objects in Razor

If the function you wish to reuse needs to be used in multiple components, you may be better off putting it into an external class. Referencing this class allows us to access the same function from multiple templates without defining it several times. The example below shows a C# class (LinkHelper.cs) meant to dynamically determine whether links should open in the same tab or a new tab:

using System.Collections.Generic;
using System.Dynamic;
using System.IO;
using ToSic.Razor.Blade;
using ToSic.Sxc.Data;

public class LinkHelper: Custom.Hybrid.Code14
{
    // Process Link entity and pass relevant properties to the actual method
    public dynamic LinkInfo(ITypedItem item) {
        return LinkInfo(item.String("URL"), item.String("Target"));
    }

    // check a link, prepare target window, icon etc. based on various settings
    public dynamic LinkInfo(string link, string window) {
        var found = Text.Has(link);

        if(found) {
            // try to find out if it's a local link
            bool isInternal = link.Contains(Link.To())
            || link.StartsWith("/") // absolute link in same site
            || link.StartsWith("#") // hash-link on same page
            || link.StartsWith("."); // relative link from this page

            // optionally auto-detect the window
            if(string.IsNullOrEmpty(window) || window == "auto")
            window = (isInternal) ? "_self" : "_blank";
        }

        // Return a dynamic object with these properties
        // It must be dynamic, otherwise the other page cannot use it
        return AsDynamic(new {
            Found = found,
            Window = window
        });
    }
}

To use this class, we can call it within our template, assign it to a variable, and reference the method we defined through that variable:

var btn = MyModel.Item("btn");
var linkHelper = GetCode("../helpers/LinkHelper.cs");
var linkInfo = linkHelper.LinkInfo(btn);


Helpers

For cases when you need to reuse markup rather than logic that handles C# data types, we have to take a slightly different approach. We can define a Razor Helper at the top for markup that is only used in that template. Helpers work very similarly to functions, with the primary difference being what they are designed to return. While C# functions naturally return C# data types, Helpers are meant to return markup. The example below shows a helper that returns a list item, preventing the template from defining the logic for handling the item’s class in multiple places.

@inherits Custom.Hybrid.RazorTyped
@using ToSic.Razor.Blade;
@using System.Text.RegularExpressions;

@helper ListItem(int index) {
    string itemClass = string.Format("list-item-{0}", index);
    <li class="@(itemClass)">Item #@(index)</li>
}

<div>
    <ul>
        @for (var i = 0; i < 5; i++) {
            @ListItem(i)
        }
        @for (var i = 5; i > -1; i--) {
            @ListItem(i)
        }
    </ul>
</div>


HTML Partials

When a piece of markup must be used across several templates, we can turn to Partials. In some ways, Partials are the closest we can get in 2SXC to the nested templates we see with frameworks like React. However, there are limitations to how they can be used, so it is generally best to keep them simple. Below is a Partial used to generate a button, a common subcomponent that tends to be reused across many templates:

@inherits Custom.Hybrid.RazorTyped
@using ToSic.Razor.Blade

@*
    This partial is designed to consistently handle the markup for buttons.

    ----------------------------------------------
    - Parameters
    ----------------------------------------------

    style = Modifier class for the button variant to be displayed
    data = Link entity to be used as data for the button
    enableToolbar = Optional boolean that determines whether the toolbar should show, true by default

    ----------------------------------------------
    - Usage
    ----------------------------------------------

    @Html.Partial("partials/_button.cshtml", new { data = Content.Link, style = "btn-primary", enableToolbar = false })
*@

@{
    var btn = MyModel.Item("btn");
    var style = MyModel.String("style", required: false);
    bool enableToolbar = (MyModel.Bool("EnableToolbar") == false) ? false : true; // Default to true

    string styleClasses = (style == "") ? "" : string.Format("btn {0}", style);

    var linkHelper = GetCode("../helpers/LinkHelper.cs");
    var linkInfo = linkHelper.LinkInfo(btn);

    var displayText = btn.String("DisplayText");
    var linkUrl = btn.Url("URL");
    string linkTarget = linkInfo.Window;

    var toolbar = (enableToolbar) ? Html.Partial("_toolbar.cshtml", new { data = btn, type = "edit-or-remove", color = (style == "btn-primary") ? "var(--bs-secondary)" : "" }) : null;
}

<a
    class="@(styleClasses)"
    href="@(linkUrl)"
    target="@(linkTarget)"
    @(toolbar)
>
    @(displayText)
</a>

To use this partial, we can call it within our main templates like this:

@foreach(var link in AsItems(links)) {
    @Html.Partial("_button.cshtml", new { btn = link, enableToolbar = true })
}


Summary

To summarize, the tool that should be used to separate a code block depends on the nature of its reuse and what it needs to return. These tools can be used to prevent code duplication and improve your templates' readability by splitting the template into smaller pieces with easily understandable names.

Which option to use, based on return data type and where it is reused
Code is used multiple times in the same template Code is used across multiple templates
The return value is a C# data type C# Function C# Class
The return value is markup Razor Helper HTML Partial

That's bananas!