Ollie Philpott
Apr 18, 2019
  2162
(12 votes)

A property viewer plugin for multilingual sites

A challenge I have found when working on large multilingual Episerver builds is that there are often times where you want to review the value of a property across all languages - for example when debugging an issue on the site or comparing how a togglable feature is configured for different languages.

Doing this using the Epi CMS UI can be a slightly laborious task – load the page in the CMS, select a language, navigate to correct tab, find the property, rinse and repeat for each language. This approach works fine when reviewing a few languages but becomes unworkable when managing as many as 35 languages, as we do for some clients at Zone.

To provide a speedier approach, I created a custom admin plugin that allows you to compare the values of a property across on languages on a single screen.

It works by navigating to a page using the content tree, and then selecting the property you want to view:

Configuration

The plugin is built using the Episerver GuiPlugin attribute with an Authorize attribute to ensure that it is only accessible to admins

[GuiPlugIn(
    DisplayName = "Property Viewer",
    Area = PlugInArea.AdminMenu,
    Url = "~/plugins/propertyviewer")]
[Authorize(Roles = "Administrators,WebAdmins")]
public class PropertyViewerController : Controller

and a custom route to allow MVC actions to be called in the standard way.

[InitializableModule]
public class CustomRouteInitialization : IInitializableModule
{
    public void Initialize(InitializationEngine context)
    {
        RouteTable.Routes.MapRoute(
            null,
            "plugins/propertyviewer/{action}",
            new { controller = "PropertyViewer", action = "Index" });
    }

    public void Uninitialize(InitializationEngine context)
    {
    }
}

Displaying a content tree

Although Episerver has a Web Forms component available (PageTree) to render the site content tree, there is no out-of-the-box solution for displaying the tree in an MVC plugin. I used jsTree, a third-party jQuery plugin, to render the content which was quick to setup and easy to configure.

To display the tree, I provide a url for an AJAX endpoint of the plugin which will return the first two levels of content, and is called again when subsequent children in the tree are expanded.

<script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.3.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.2.1/jstree.min.js"></script>
…
<div id="jstree" class="epi-paddingVertical-small episerver-pagetree-selfcontained"></div>
<script>
    $(function() {
        $("#jstree").jstree({
            "core": {
                "data": {
                    'url': "/plugins/propertyviewer/getcontenttree/",
                    "dataType": "json",
                    "data": function(node) {
                        // jstree requests # for root of tree
                        return { "id": node.id === "#" ? "1" : node.id };
                    }
                }
            },
            "root": "1"
        });
    });
</script>

The GetContentTree endpoint in the plugin takes a page id as a parameter and returns details about the page and its children, using IContentLoader to fetch content from Episerver. jsTree expects a JSON object with a node’s id, name and an array of children with the same information.

Setting the children property to true indicates that the node has children and means that it will be closed initially. When it is expanded, another request is sent to the endpoint to fetch the next levels of the tree.

public JsonResult GetContentTree(int id = 1)
{
    var page = _contentLoader.Get<PageData>(new ContentReference(id));

    return Json(new
    {
        id,
        text = page.Name,
        children = _contentLoader.GetChildren<PageData>(page.ContentLink)?.Select(s => new
        {
            text = s.Name,
            id = s.ContentLink.ToReferenceWithoutVersion().ID,
            children = _contentLoader.GetChildren<PageData>(s.ContentLink)?.Any()
        })
    }, JsonRequestBehavior.AllowGet);
}

Displaying page properties

To display a dropdown list of page properties, I bind to the select_node event (triggered when a node in the tree is selected) and call the GetProperties endpoint. The selected node id is passed as a parameter, and the returned html for the dropdown list is rendered below the content tree. The selected node id is also stored in a hidden input for use with other js events.

$("#jstree").on("select_node.jstree",
    function (e, data) {
        $("#PageId").val(data.node.id);
        $.ajax("/plugins/propertyviewer/getproperties/?pageId=" + data.node.id).done(
            function (data) {
                $("#blockPropertyList").html("");
                $("#results").html("");
                $("#propertyList").html(data);
        });
});

The GetProperties action of the controller fetches the list of standard page properties for the specified ID, and returns a partial view containing the dropdown list of properties.

public PartialViewResult GetProperties(int pageId)
{
    var page = _contentLoader.Get<PageData>(new ContentReference(pageId));
    var model = new PropertyListModel
    {
        PageProperties = page.Property
                            .Where(x => x.IsPropertyData)
                            .Select(x => x.Name)
                            .OrderBy(x => x)
    };

    return PartialView(PropertyListView, model);
}

Displaying property values

Once a page has been selected and a property chosen, we need to display a table containing the values of the property for each language.

The plugin can handle properties in two ways:

  • in the case of most properties, it simply displays the string representation for the property
  • for local blocks (i.e. a fixed block added as a property), it provides an additional dropdown to choose the block property to view

When a property is selected from the dropdown, we call the GetPropertyValues endpoint with the page id and property name, and either display the values for the property, or display the block property list.

$("#propertyList").on("change", "select", function () {
    $.ajax("/plugins/propertyviewer/getpropertyvalues/?pageId="
        + $("#PageId").val() + "&propertyname=" + this.value).done(
        function (data) {
            if (data.indexOf("BlockPropertyName") > -1) {
                $("#blockPropertyList").html(data);
                $("#results").html("");
            } else {
                $("#blockPropertyList").html("");
                $("#results").html(data);
            }
    });
});

The GetPropertyValues action checks whether the specified property is a block type using propertyData.Type and returns the list of block properties if it is block. For other property types, the property values are returned.

public PartialViewResult GetPropertyValues(PropertyReference reference)
{
    if (IsBlock(reference))
    {
        var blockModel = BuildBlockPropertyListModel(reference);
        return PartialView(BlockPropertyListView, blockModel);
    }

    var model = BuildPropertyValuesModel(reference);

    return PartialView(PropertyValuesView, model);
}

To fetch the values of a property for each language, we need to use IContentRepository.GetLanguageBranches to get the language-specific instances of the page, and then fetch the value of the property for each instance.

private PropertyValuesModel BuildPropertyValuesModel(PropertyReference reference)
{
    var languageVersions = _contentRepository.GetLanguageBranches<PageData>(new ContentReference(reference.PageId));
    var propertyValues = languageVersions.Select(x => new PropertyValueModel
    {
        Language = x.Language.Name,
        Value = x.GetPropertyValue(reference.PropertyName)
    });

    return new PropertyValuesModel
    {
        PropertyValues = propertyValues
    };
}

In the situation where the property is a local block, we return a partial view containing a dropdown of the block properties, constructed in a similar way to the page property dropdown.

private BlockPropertyListModel BuildBlockPropertyListModel(PropertyReference reference)
{
    var page = _contentLoader.Get<PageData>(new ContentReference(reference.PageId));
    var property = page.Property.Get(reference.PropertyName);

    return new BlockPropertyListModel
    {
        BlockProperties = ((BlockData) property.Value).Property
            .Where(x => x.IsPropertyData)
            .Select(x => x.Name)
            .OrderBy(x => x)
    };
}

Finally, we bind to the change event of the block property dropdown and call a GetBlockPropertyValues endpoint that builds a table of the block property values in the same way as the page property values, but fetching the value from the property on the block.

var propertyValues = languageVersions.Select(x => new PropertyValueModel
{
    Language = x.Language.Name,
    Value = x.Property
            .GetPropertyValue<BlockData>(propertyName)
            .GetPropertyValue(blockPropertyName)
});

The results are rendered in a simple HTML table using the default Epi table styling with a special case to display a tick for true Boolean properties.

@model AlloyTemplates.Plugins.PropertyViewer.Models.PropertyValuesModel

<table class="epi-default">
    <colgroup>
        <col style="width:10%">
        <col style="width:90%">
    </colgroup>
    <thead>
        <tr>
            <th>Language</th>
            <th>Value</th>
        </tr>
    </thead>
    @foreach (var item in Model.PropertyValues)
    {
        <tr>
            <td>
                @item.Language
            </td>
            <td>
                @RenderValue(item.Value)
            </td>
        </tr>
    }
</table>

@helper RenderValue(string propertyValue)
{
    if (propertyValue == "True")
    {
        <img src="/App_Themes/Default/Images/Tools/png/Check.png" alt="True" align="center" />
    }
    else
    {
        @propertyValue
    }
}

Improvements

This version of the plugin works well for simple properties – text, numbers, Booleans and any property that provides a comprehensive ToString method. It is also useful for Content References where it will show the content ID, and values stored as JSON where it will display the JSON object. It doesn’t currently work for complex property types or Content Areas since it uses the string representation of the property.

Future improvements I would like to make are:

  • display a thumbnail for images
  • allow the user to select a block in a content area and a property from that block
  • handle blocks nested to more than 1 level
  • create as an add-on so it can be easily shared and added to projects

However, in its current form it is still an extremely useful tool to have available as a first port of call for quickly auditing a property on a large multilingual site.

Apr 18, 2019

Comments

KennyG
KennyG Apr 18, 2019 03:37 PM

This is super cool! And there's a lot that can be learned from your breakdown. Thanks!

valdis
valdis Apr 21, 2019 05:24 PM

nice! can't see from video (I'm old) - but would be cool if you can add link to actual content as well. so that editors / admins could jump straight to the content and make necessary edits if needed.

Per Nergård
Per Nergård Apr 22, 2019 02:12 PM

Haven't tried it yet but this looks very nice!

I love tools that make life easier!

Jake Jones
Jake Jones Apr 22, 2019 07:42 PM

Really nice!

Henrik Fransas
Henrik Fransas Apr 23, 2019 07:38 AM

Very nice!
Like it a lot!

Ollie Philpott
Ollie Philpott Apr 23, 2019 10:53 AM

@valdis iljuconoks Thanks for the feedback, that's a great idea - I'll add it to the list of enhancements I want to implement.

Luc Gosso (MVP)
Luc Gosso (MVP) Apr 23, 2019 11:14 AM

Nice!! Seems like you could use it to find properties that is not used too. Just by choosing startpage and then a property, is that so? or maybe it is not recursive...

Have you opensourced it, can i contribute? 

regards

Ollie Philpott
Ollie Philpott Apr 24, 2019 09:43 AM

@GOSSO Thanks! Do you mean properties that have no values set for any language? You could manually search through page properties for those ones using the plugin if so.

I'm currently setting it up as an add-on module, and will share the code with you once I have finished that.

Ollie Philpott
Ollie Philpott Sep 9, 2019 11:04 AM

Latest source code is avaiable on Github https://github.com/zone/Zone.Episerver.PropertyViewer/

Please login to comment.
Latest blogs
Plug-in manager is back in CMS 12

Plug-in manager is back in the UI, what is it and how can i use it?

Luc Gosso (MVP) | Oct 6, 2022 | Syndicated blog

Display Child Pages in Content Delivery API Response

The below example will implement an instance of IContentConverterProvider to customise the serialisation of PageData and output child pages in the...

Minesh Shah (Netcel) | Oct 4, 2022

Bring the Report Center back in Optimizely CMS 12

The Report Center has been a part of Optimizely CMS since its first debut in version 5R2 in 2008, but in CMS 12, it's removed! Don't despair! Make...

Tomas Hensrud Gulla | Oct 4, 2022 | Syndicated blog

Customizing Property Lists in Optimizely CMS

Generic property lists is a cool editorial feature that has gained a lot of popularity - in spite of still being unsupported (officially). But if y...

Allan Thraen | Oct 2, 2022 | Syndicated blog

Optimizely names Luminary Senior Developer, Ynze Nunnink, OMVP

Luminary Senior Developer and Optimizely Lead, Ynze Nunnink has secured the coveted position of Optimizely MVP. Earning a Platinum badge for his...

Ynze | Oct 2, 2022 | Syndicated blog

Content Delivery API – The Case of the Duplicate API Refresh Token

Creating a custom refresh provider to resolve the issues with duplicate tokens in the DXC The post Content Delivery API – The Case of the Duplicate...

David Lewis | Sep 29, 2022 | Syndicated blog