Thursday, October 10, 2013

UI Extensions - Ensuring an Item is Full Loaded before isAvailable and isEnabled are executed

Recently I saw a few questions in Tridion Stack Exchange related to GUI Extensions and how to load items in the _isAvailable and _isEnabled methods. The question here would be. Are the _isAvailable and _isEnabled methods good places to load an item? The answer is NO.

The reason why they are no good places to load items is because those methods return values (true or false) so that we cannot manage asynchronous calls inside them because the code execution won’t be sequential. I was thinking on a good solution for that since I have been myself in such situation when a button or context menu should be enabled or disabled based on information that is just available if the item if fully loaded.

In order to solve this issue I have use several techniques related to GUI extensions, some of them are not documented and of course not supported by SDL Tridion R&D but I think it is worth to show them in this post.

SOLUTION:

<1.       Extend an existing Resources Group

It is known for all the Tridion Implementers that SDL Tridion R&D does an excellent job while designing and making things extensible even if they are not documented, one of those extensible features are the possibility to add or inject javascript files to a pre-existing group of files at runtime.

In order to use a Resources Group we need to configure it in the configuration file used by your GUI Extension. In my sample implementation I have done it in the following way.

<cfg:extensiongroups>
  <cfg:extensiongroup name="ListExtension">
    <cfg:extension target="Tridion.Web.UI.Editors.CME.Views.Dashboard">
      <cfg:insertafter>ListExtension</cfg:insertafter>
    </cfg:extension>
  </cfg:extensiongroup>
</cfg:extensiongroups>

Let’s explain it a little bit.
  • The name attribute will identify your extension group make sure it is unique in the whole system.
  • The target attribute will define which existing group you want to extend, in this case I want to extend Tridion.Web.UI.Editors.CME.Views.Dashboard because this one contains call to Dashboard.js which is the javascript file that will load the Tridion Items List control.


The idea behind this solution is to extend the OnSelectionChange event handler for the Tridion Items List Control so that we can pre-load an item when we change the current selection in the view.
  • The insertafter element will specify the group that will contain the javascript files that will be executed right after the Tridion.Web.UI.Editors.CME.Views.Dashboard files are executed.

<cfg:group name="ListExtension" merge="always">
  <cfg:fileset>
    <cfg:file type="script">/Client/ListExtension.js</cfg:file>
  </cfg:fileset>
</cfg:group>

                In this sample I am defining a single javascript file called ListExtension.js that will be executed right after the extended resources group. Following this approach my execution chain will look like this.

<cfg:file type="style">/Views/Dashboard/Dashboard.sprited.css</cfg:file>
<cfg:file type="style">{ThemePath}/Views/dashboard.sprited.css</cfg:file>
<cfg:file type="script">/Views/Dashboard/Dashboard.js</cfg:file>

And

<file type="script">/Client/ListExtension.js</cfg:file>

                Finally we need to register the resources extension.

<resourceextensions>
  <resourceextension>ListExtension</resourceextension>
</resourceextensions>

  2. Override a DashboardView.js method

Having the fact that ListExtension.js is executed right after Dashboard.js, then we can override or extend the methods defined inside Dashboard.js since they are already enabled. For this sample I have overridden the onListSelectionChanged method. This would the content for my ListExtension.js file.

Tridion.Cme.Views.DashboardBase.prototype._oldListSelectionChanged = Tridion.Cme.Views.DashboardBase.prototype.onListSelectionChanged;
Tridion.Cme.Views.DashboardBase.prototype.onListSelectionChanged = function DashboardBase$onListSelectionChangedNew(event) {

    this._oldListSelectionChanged(event);

    var list = event.source;
    var selection = list.getSelection();

    // We are just pre-loading single selections
    if (selection.getCount() == 1) {
        var item = $models.getItem(selection.getItem(0));

        var updateControlsDelegate = this.getDelegate(this.updateControls);

        function onListSelectionChanged$itemLoaded(event) {
            $evt.removeEventHandler(item, "load", onListSelectionChanged$itemLoaded);

            updateControlsDelegate(); // This call will trigger _isAvailable and _isEnabled for all the commands in the current view
        };

        $evt.addEventHandler(item, "load", onListSelectionChanged$itemLoaded);

        if (!item.isLoaded()) {
            item.load($const.OpenMode.VIEW);
        }
        else {
            onListSelectionChanged$itemLoaded({ source: item });
        }
    }
}

In this method I am saving the original onListSelectionChanged method in a variable called _oldListSelectionChanged. This will ensure that I can call the original one and then execute extra logic. My overridden version of this method will call the original one and also will access to the current list selection and pre-load the item that has been selected.  Additionally I am executing the updateControls method, this method will call the _isAvailable and _isEnabled methods of all the commands available in the current view including our custom ones J

   3.      Implement _isAvailable and _isEnabled

The final part will be to implement _isAvailable and _isEnabled, for this sample I have implemented a very basic custom button in the Dashboard Toolbar but that will check if the item has been fully loaded in order to return true

Type.registerNamespace("ListExtension");

ListExtension.CustomButton = function ListExtension$CustomButton(name) {
    Type.enableInterface(this, "ListExtension.CustomButton");
    this.addInterface("Tridion.Core.Command", [name || "CustomButton"]);
};

ListExtension.CustomButton.prototype._isAvailable = function CustomButton$_isAvailable(selection, pipeline) {
    if (pipeline) pipeline.stop = false;

    if (selection.getCount() == 1) {
        var item = $models.getItem(selection.getItem(0));

        if (item.isLoaded()) {
            console.debug("Item is fully loaded.");
            return true;
        }
    }
    return false;
};

ListExtension.CustomButton.prototype._isEnabled = function CustomButton$_isEnabled(selection, pipeline) {
    return this._isAvailable(selection, pipeline);
};

ListExtension.CustomButton.prototype._execute = function CustomButton$_execute(selection, pipeline) {
    console.debug("You have clicked in a custom buttom");
};

                In the code above I am returning true just in case my item is fully loaded which will always be the case if I have selected it from the Tridion Items List

               

No comments:

Post a Comment