Our monthly Virtual Happy Hour is happening this week, Fri Feb 23!

Try our conversational search powered by Generative AI!

Only show custom tree node icon when a property is set

Vote:
 

For a client I have developed a feature where they set a checkbox on a page of a certain type (a product detail) which causes it to be marked as "Unavailable" and then serves a different template on the website communicating this to the visitor and giving some alternative options. We want this feature to replace the "Expired content" feature that EPiServer natively has, so that the page will still exist (for SEO purposes).

Previously the client kept track of which products had been 'removed' by looking for the orange clock icon (i.e. "Expired content") in the EPiServer site tree. Now that we won't be using that anymore, the client would like an alternative to prevent having to click through all of the nodes looking for the unavailable products.

Using Google and this forum I did find many articles on using the UIDescriptor class to set a page type's IconClass. But the problem is, it doesn't seem possible to dynamically alter the icon based on each node's properties. So every single product node receives the same icon which is set in said UDIescriptor. What I would like to know, is if it is possible to go at this from a different angle and only have the icon altered when that checkbox is set on a product node?

Alternatively, I tried to look into possibly hooking into the same logic which determines whether or not to display the expired content icon but could not really find much about that feature at all on Google or this forum...

Any help would be much appreciated!

[Pasting files is not allowed]

#182942
Oct 02, 2017 11:36
Vote:
 

Hi William,

One way of doing it, maybe not the right one, is to intercept the page model and add your property

https://world.episerver.com/blogs/Shamrez-Iqbal/Dates/2014/12/adding-startpublishdate-to-pagetree-tooltip/

and then do some CSS magic to add content :after

:after {
    content: " (" attr(title) ")";
}

http://jondjones.com/learn-episerver-cms/episerver-frontend-developers-guide/how-to-add-your-own-custom-css-in-the-episerver-editor

http://devblog.gosso.se/2017/04/override-css-in-episerver-editor-mode/

Regards

#182946
Oct 02, 2017 12:16
Vote:
 

Thanks a lot for taking the time to answer, GOSSO! I've spent a while studying that blog post of Shamrez and implementing it in my project. I have run into a few problems which I thought I would share here to see if we could figure it out.

I have added an class which inherits from PopulatePageDataModel, like so:

using EPiServer.Cms.Shell.UI.Rest.Models;
using EPiServer.Cms.Shell.UI.Rest.Models.Transforms;
using EPiServer.Core;

namespace Epi.Extensions.Models.Custom
{
    public class AlphabetPopulatePageDataModel : PopulatePageDataModel
    {
        private static readonly string[] PropertyNames = new string[]
        {
            "PageLanguageBranch",
            "PageExternalUrl",
            "PageChildOrderRule",
            "PageSaved",
            "PageChangedBy",
            "PageCreated",
            "PageChanged",
            "PageTypeID",
            "PageVisibleInMenu",
            "PageShortcutType",
            "PageShortcutLink",
            "PageLinkURL",
            "Unavailable"
        };

        public override void TransformInstance(IContent source, StructureStoreContentDataModel target, IModelTransformContext context)
        {
            PropertyDictionary properties = target.Properties;
            IContent content = source;

            foreach (string key in PropertyNames)
            {
                PropertyData propertyData = content.Property[key];

                if (propertyData != null)
                {
                    properties.Add(key, propertyData.Value);
                }
            }
        }
    }

}

In this, the property I have added is "Unavailable" (this is the checkbox which the product detail page type has). I put the class in my Epi.Extensions project inside the custom models namespace. Next, I went into the DependencyResolverConfiguration class in our Epi.Extensions.Rendering namespace where there was already a method ConfigureContainer which accepted an instance of ConfigurationExpress, so I added the last couple of lines of code to that:

private static void ConfigureContainer(ConfigurationExpression container)
{
        //Swap out the default ContentRenderer for our custom
        container.For<IContentRenderer>().Use<ErrorHandlingContentRenderer>();
        container.For<ContentAreaRenderer>().Use<SiteContentAreaRenderer>();

        // Intercept the default PopulatePageDataModel object for transforming data into Model objects through REST
        // and replace with our custom object so that certain page properties can be accessed after doing so
        AlphabetPopulatePageDataModel pageModel = new AlphabetPopulatePageDataModel();
        container.For<PopulatePageDataModel>().Use<AlphabetPopulatePageDataModel>();
}

This is where my uncertainty arose first. The code demonstrated in the blog post seems to be obsolete (implementing .IfTypeMatches() and .InterceptWith()), in favour of the methods I've used here (.For() and .Use()). I think I've rewrote it accordingly, but am not 100% sure.

So after successfully building and reloading the CMS, I inspected all XHR requests being made for a product detail page which has the "Unavailable" checkbox set. Again, it appears this has changed slightly since that blog post as the request appears to now be:

/EPiServer/cms/Stores/contentstructure/8249?dojo.preventCache=1507019557194

In this, 8249 is the page ID. The JSON response lists the nested "properties" object, but it doesn't contain my "Unavailable" property. I noticed there is another XHR reqeust (/EPiServer/cms/Stores/contentdata/8249_14759?dojo.preventCache=1507019557164) which does list that property but that probably has nothing to do with my code as it lists all of the properties associated with that page.

So could any of my differing code have something to do with the property not showing up in the JSON response? I tried to work with the IfTypeMatches and InterceptWith methods first but couldn't get them to be recognised as available methods, not even after adding the namespace EPiServer.ServiceLocation.Compatibility which it appears to descend from (associated with StructureMap). This page convinced me that they are in fact obsolete: https://world.episerver.com/forum/developer-forum/-Episerver-75-CMS/Thread-Container/2016/8/structuremap-v2-vs-v3-syntax/

One more thing (if we manage to solve the above): at the end of the blog post there is talk of the ContentNavigationTree.js file and that it can be used to incorporate the added property. However, I can't seem to find that file anywhere in my Developer Toolbar. Could it also be something which has recently been reworked in EPiServer? And once I find the correct location, how would I go about extending it to use my property? I'm guessing it's going to be a core file which I don't have in my solution but is loaded at runtime through metadata.

Thanks a lot for any additional help.

#182999
Oct 03, 2017 10:57
Vote:
 

that was a long post ;)

This is not meant to be done... but keep on...

Checked this https://world.episerver.com/blogs/Shamrez-Iqbal/Dates/2014/12/changing-the-tooltip-in-the-page-tree/

also, use container.WhatDoIHave() to check what you have you have in container

http://structuremap.github.io/diagnostics/whatdoihave/

#183023
Oct 03, 2017 13:28
Vote:
 

Thanks again GOSSO.

I just spent most of the morning playing around with the JavaScript code and am now at the point where if my custom property from the (derived from) PopulatePageData class that I added would show up in the "properties" property, I would be done :-)

But unfortunately, no matter what I do it won't seem to show up. Once again, some code:

namespace Epi.Extensions.Models.Custom
{
    public class AlphabetPopulatePageDataModel : PopulatePageDataModel
    {
        private static readonly string[] PropertyNames = new string[]
        {
            "PageLanguageBranch",
            "PageExternalUrl",
            "PageChildOrderRule",
            "PageSaved",
            "PageChangedBy",
            "PageCreated",
            "PageChanged",
            "PageTypeID",
            "PageVisibleInMenu",
            "PageShortcutType",
            "PageShortcutLink",
            "PageLinkURL",
            "Unavailable"
        };

        public override void TransformInstance(IContent source, StructureStoreContentDataModel target, IModelTransformContext context)
        {
            PropertyDictionary properties = target.Properties;
            IContent content = source;

            foreach (string key in PropertyNames)
            {
                PropertyData propertyData = content.Property[key];

                if (propertyData != null)
                {
                    properties.Add(key, propertyData.Value);
                }
            }
        }
    }
}

I now realise that the "Unavailable" property may not be accessible the way I'm trying to get it there as it is a property added to a specific page model and the rest of the properties are non-specific properties which all pages have (i.e. non-page model properties). However, I can't be sure that's the problem because even if I add the "PageStartPublish" property as the example in the blog post does, it still doens't show uo.

Using your most recent link (Shamrez Iqbal) I managed to override the CustomNavigationWidget like so:

public class CarVariantPageDescriptor : PageRepositoryDescriptor
{
    public override string CustomNavigationWidget
    {
        get
        {
            return "epi-cms/component/ContentNavigationTree";
        }
    }
}

At first I had it use a custom string for the to-be-injected JS file as the example did, but that only resulted in a lot of console errors and my JS code not being executed. What I've done with the above is replace the ContentNavigationTree.js file completely, with my file stated in module.config. As it is an exact copy of the original ContentNavigationTree.js file, it still works. And custom code I put in (e.g. console.log() statements) actually work.

The only thing is, I still can't do much as I can't access any of the page properties. As mentioned above, nothing I add to AlphabetPopulatePageDataModel ever ends up in the "properties" property in JS. In my DependencyResolverConfiguration I have the following in the ConfigureContainer method:

container.For<PopulatePageDataModel>().Use<AlphabetPopulatePageDataModel>();

And in my DescriptorInitialization I have the following in the ConfigureContainer method:

container.For<PageRepositoryDescriptor>().Use<CarVariantPageDescriptor>();

I assume the last line works as my custom file is being used in EPiServer, but the first one doesn't seem to do anything. I've spent a huge amount of time on this whole thing so far, so it would be nice to get the last piece of the puzzle within reach :-)

Seeing as what I'm trying to do is change the icon, I'm working in the getIconClass method of my overridden ContentTreeNavigation file. Could there perhaps be a way to retrieve a page's model properties from there? I've been snooping around the EPiServer JavaScript documentation but getting lost real quick there... I have tried the following but that only gives you an object with an ID and some other irrelevant (for what I need to do) properties.

var reference = new ContentReference(item.contentLink);

I think what I really need is a JS equivalent to the ContentRepository and it appears to exist from looking at the docs, but it isn't available in this JS file I am working in.

#183069
Oct 04, 2017 11:45
Vote:
 

Not sure about this but check this:

var registry = dependency.resolve("epi.shell.store.Registry"),
    store = registry.get("epi.cms.content.light"),
    query = store.query({
        id: 3,
        language: "en"
    });

Source: https://world.episerver.com/documentation/developer-guides/CMS/user-interface/Store-architecture/

Please post the solution when you got it, for future reference.

#183073
Oct 04, 2017 13:41
Vote:
 

I got it working, did you?

Override GetIconClass

define([
    "dojo",
    "dojo/_base/lang",
    "epi-cms/component/PageNavigationTree"
], function (
    dojo,
    lang,
    pageNavigationTree
) {
    pageNavigationTree.prototype.getOriginalIconClass = pageNavigationTree.prototype.getIconClass;
    pageNavigationTree.prototype.getIconClass = function (/*dojo/data/Item*/item, /*Boolean*/opened) {

        var getIconClass = this.getOriginalIconClass(item, opened);
        if (item.properties && item.properties.unavailable)
            return "epi-iconObjectTruck";

        return getIconClass;

    }
});

Add your property to Model in Rest API:

   public class AlphabetPopulatePageDataModel : PopulatePageDataModel
    {
        private static readonly string[] PropertyNames = new string[13]
        {
            "PageLanguageBranch",
            "PageExternalUrl",
            "PageChildOrderRule",
            "PageSaved",
            "PageChangedBy",
            "PageCreated",
            "PageChanged",
            "PageTypeID",
            "PageVisibleInMenu",
            "PageShortcutType",
            "PageShortcutLink",
            "PageLinkURL",
            "Unavailable"
        };

        public override void TransformInstance(IContent source, StructureStoreContentDataModel target, IModelTransformContext context)
        {
            PropertyDictionary properties = target.Properties;
            IContent content = source;
            foreach (string propertyName in PropertyNames)
            {
                PropertyData propertyData = content.Property[propertyName];
                if (propertyData != null && !properties.ContainsKey(propertyName))
                    properties.Add(propertyName, propertyData.Value);
            }
        }
    }

Changing Model:

        private static void ConfigureContainer(ConfigurationExpression container)
        {
            container.For<IModelTransform>().Use<AlphabetPopulatePageDataModel>();
        }

Let me know how it worked, or mark as solved. 

#183264
Oct 09, 2017 22:24
Vote:
 

GOSSO, you're a lifesaver! One little subtlety from your code examples did the trick. Before in the ConfigureContainer() method I was using .For<PopulatePageDataModel> and once I substituted this for .For<IModelTransform> I was able to access my property ("Unavailable") in the JavaScript file! So from there on I was able to do had set out to do all along. See the result here:

Custom node icon in EPiServer

Custom node icon in EPiServer

The only thing I couldn't incorporate from your final set of code examples was the structure of the JS file. After I got everything working I decided to try and make it even more elegant by following that example, but EPiServer fails to load after using that script, throwing erros about "multipleDefine" for dojo. So I reverted back to my complete overload of the ContentNavigationTree file from epi-cms/component.

So in conclusion, this is how I got this to work (only relevant code, so some obfuscating has been done for clarity's sake). For future reference :)

Custom model:

using EPiServer.Cms.Shell.UI.Rest.Models;
using EPiServer.Cms.Shell.UI.Rest.Models.Transforms;
using EPiServer.Core;

namespace Epi.Extensions.Models.Custom
{
    public class AlphabetPopulatePageDataModel : PopulatePageDataModel
    {
        private static readonly string[] PropertyNames = new string[13]
        {
            "PageLanguageBranch",
            "PageExternalUrl",
            "PageChildOrderRule",
            "PageSaved",
            "PageChangedBy",
            "PageCreated",
            "PageChanged",
            "PageTypeID",
            "PageVisibleInMenu",
            "PageShortcutType",
            "PageShortcutLink",
            "PageLinkURL",
            "Unavailable"
        };

        public override void TransformInstance(IContent source, StructureStoreContentDataModel target, IModelTransformContext context)
        {
            PropertyDictionary properties = target.Properties;
            IContent content = source;

            foreach (string propertyName in PropertyNames)
            {
                PropertyData propertyData = content.Property[propertyName];

                if (propertyData != null && !properties.ContainsKey(propertyName))
                {
                    properties.Add(propertyName, propertyData.Value);
                }
            }
        }
    }
}

Configuration module:

using Epi.Extensions.Models.Custom;
using EPiServer.Cms.Shell.UI.Rest.Models.Transforms;
using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using EPiServer.ServiceLocation;
using StructureMap;

namespace Epi.Extensions.Rendering
{
    [InitializableModule]
    [ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
    public class DependencyResolverConfiguration : IConfigurableModule
    {
        public void ConfigureContainer(ServiceConfigurationContext context)
        {
        }

        private static void ConfigureContainer(ConfigurationExpression container)
        {

            // Intercept the default PopulatePageDataModel object for transforming data into Model objects through REST
            // and replace with our custom object so that certain page properties can be accessed after doing so
            container.For<IModelTransform>().Use<AlphabetPopulatePageDataModel>();
        }

        public void Initialize(InitializationEngine context)
        {
        }

        public void Uninitialize(InitializationEngine context)
        {
        }

        public void Preload(string[] parameters)
        {
        }
    }
}

JavaScript overload file:

define("epi-cms/component/ContentNavigationTree", [
    // dojo
    "dojo/_base/array",
    "dojo/_base/connect",
    "dojo/_base/declare",
    "dojo/_base/lang",
    "dojo/dom-class",
    "dojo/topic",
    "dojo/when",

    // epi
    "epi/dependency",
    "epi/string",
    "epi/shell/ClipboardManager",
    "epi/shell/command/_WidgetCommandProviderMixin",
    "epi/shell/selection",
    "epi/shell/TypeDescriptorManager",
    "epi/shell/widget/dialog/Alert",
    "epi/shell/widget/dialog/Confirmation",
    "epi-cms/_ContentContextMixin",
    "epi-cms/_MultilingualMixin",
    "epi-cms/ApplicationSettings",
    "epi-cms/contentediting/PageShortcutTypeSupport",
    "epi-cms/command/ShowAllLanguages",
    "epi-cms/component/_ContentNavigationTreeNode",
    "epi-cms/component/ContentContextMenuCommandProvider",
    "epi-cms/contentediting/ContentActionSupport",
    "epi-cms/core/ContentReference",
    "epi-cms/widget/ContentTree",
    "epi/shell/widget/ContextMenu",
    "./command/_GlobalToolbarCommandProvider",
    "../command/CopyContent",
    "../command/CutContent",
    "../command/DeleteContent",
    "../command/PasteContent",

    // resources
    "epi/i18n!epi/cms/nls/episerver.cms.components.createpage",
    "epi/i18n!epi/cms/nls/episerver.cms.components.pagetree",
    "epi/i18n!epi/cms/nls/episerver.shared.header"
],

    function (
        // dojo
        array,
        connect,
        declare,
        lang,
        domClass,
        topic,
        when,

        // epi
        dependency,
        epistring,
        ClipboardManager,
        _WidgetCommandProviderMixin,
        Selection,
        TypeDescriptorManager,
        Alert,
        Confirmation,
        _ContentContextMixin,
        _MultilingualMixin,
        ApplicationSettings,
        PageShortcutTypeSupport,
        ShowAllLanguagesCommand,
        _ContentNavigationTreeNode,
        ContextMenuCommandProvider,
        ContentActionSupport,
        ContentReference,
        ContentTree,
        ContextMenu,
        _GlobalToolbarCommandProvider,
        CopyCommand,
        CutCommand,
        DeleteCommand,
        PasteCommand,
        // resources
        resCreatePage,
        res
    ) {

        return declare([ContentTree, _ContentContextMixin, _WidgetCommandProviderMixin, _MultilingualMixin], {

            // MANY PROPERTY AND METHOD SETTING HERE! OBFUSCATED FOR THIS CODE EXAMPLE

            getIconClass: function (/*dojo.data.Item*/item, /*Boolean*/opened) {
                // summary:
                //      Overridable function to return CSS class name to display icon,
                // item: [dojo.data.Item]
                //      The current contentdata.
                // opened: [Boolean]
                //      Indicate the node is expanded or not.
                // tags:
                //      public, extension

                var inherited = this.inherited(arguments);

                if (!item || !item.properties) { return inherited; }

                // Check whether or not the current node has the Unavailable property added on the AlphabetPopulatePageDataModel and
                // set using the TransformInstance method of that class which becomes available here through injection on 
                // DependencyResolverConfiguration.ConfigureContainer(); if so, use a custom icon for this node
                if (typeof item.properties.unavailable !== 'undefined' && item.properties.unavailable) {
                    return inherited + " epi-iconObjectUnavailable";
                }

                if (!item.properties.pageShortcutType) { return inherited; }

                var shortcutTypeIcon = "";

                switch (item.properties.pageShortcutType) {
                    case PageShortcutTypeSupport.pageShortcutTypes.Shortcut:
                        shortcutTypeIcon = "epi-iconObjectInternalLink";
                        break;
                    case PageShortcutTypeSupport.pageShortcutTypes.External:
                        shortcutTypeIcon = "epi-iconObjectExternalLink";
                        break;
                    case PageShortcutTypeSupport.pageShortcutTypes.Inactive:
                        shortcutTypeIcon = "epi-iconObjectNoLink";
                        break;
                    case PageShortcutTypeSupport.pageShortcutTypes.FetchData:
                        shortcutTypeIcon = !item.hasTemplate ? "epi-iconObjectContainerFetchContent" : "epi-iconObjectFetchContent";
                        break;
                    default:
                        break;
                }

                return shortcutTypeIcon ? inherited + " " + shortcutTypeIcon : inherited;
            },

            // SOME MORE METHODS HERE, OBFUSCATED FOR THIS EXAMPLE

        });
    });

Custom CSS:

.dijitTreeContent .dijitTreeIcon.epi-iconObjectUnavailable { 
  background: url('/Static/images/icon-carvariantpage-unavailable.png') no-repeat 0 0; 
}

Adding the custom JavaScript file to EPiServer (module.config):

<?xml version="1.0" encoding="utf-8"?>
<module clientResourceRelativePath="/Static/">
  <assemblies>
    <add assembly="Epi.Site" />
  </assemblies>
  <clientResources>
    <add name="epi-cms.widgets.base" path="/Static/css/episerver.css" resourceType="Style" />
    <add name="epi-cms.widgets.base" path="/Static/js/episerver.js" resourceType="Script" />
  </clientResources>
</module>

So I'm going to use this result for now, getting it to work the way you have above with the more concise JavaScript would be a nice-to-have :) Thanks again for your help!!

#183275
Oct 10, 2017 10:18
This topic was created over six months ago and has been resolved. If you have a similar question, please create a new topic and refer to this one.
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.