3

How do I resolve the issue of the browser being asynchronous and each new event being added to the event stack?

In my previous question it was suggested that, because of caching, I should wait until the full component. Also this answer was giving to Robert in another topic, see below.

My current code looks like this now;

/// <summary>
///
/// </summary>
/// <returns>boolean</returns>
Extensions.Unlocalize.prototype.isEnabled = function Unlocalize$isEnabled(selection) {
    var item = $models.getItem(selection.getItem(0)),
        itemTCM = item.getId(),
        pubTCM = null,
        isAvailable = false,
        itemRegex = /.?tcm:(.*?)\-/,
        pubRegex = /.*?\-(.*)-.*/;

    console.log("Extensions.Unlocalize.prototype.isEnabled; Started."); 
    console.log("Info of parameter item at start: ", item.getInfo());     

    //Make sure the component is loaded before using getOwningPublicationId();
    var onItemLoaded = function Extensions$Unlocalize$_onItemLoaded() {
        console.log("Extensions$Unlocalize$_isEnabled; Started."); 
        $evt.removeEventHandler(item, "load", onItemLoaded);

        var itemInfo = item.getInfo();
        console.log("Info of parameter item in eventHandler: ", itemInfo);

        pubTCM = item.getOwningPublicationId(); 
        pubTCM = pubTCM.match(pubRegex);
        itemTCM = itemTCM.match(itemRegex);

        if (itemTCM[1] == pubTCM[1]) {
            isAvailable = true;
        }        
        console.log("Extensions$Unlocalize$_isEnabled; ", isAvailable);         
    };

    console.log("Extensions.Unlocalize.prototype.isEnabled; addEventHandler"); 
    $evt.addEventHandler(item, "load", onItemLoaded);

    console.log("Extensions.Unlocalize.prototype.isEnabled; item.load()"); 
    item.load(item.isLoaded(true), $const.OpenMode.VIEW);

    console.log("Extensions.Unlocalize.prototype.isEnabled; Return."); 
    return isAvailable;
}

This works all good, the only issue is the isEnabled function is finished before _onItemLoad is done, this way return isAvailable is always false. Where I would like to check if the component is in the same publication created as being selected.

The outcome of the console looks like this:

Extensions.Unlocalize.prototype.isEnabled; Started. 
Info of parameter item at start: Tridion.ContentManager.ItemInfo {ID: "tcm:24-88", Title: undefined, Icon: "T16L0P0", ItemType: "tcm:16", ItemTypeName: "Component"…}
Extensions.Unlocalize.prototype.isEnabled; addEventHandler 
Extensions.Unlocalize.prototype.isEnabled; item.load() 
Extensions.Unlocalize.prototype.isEnabled; Return. 
Extensions$Unlocalize$_isEnabled; Started.
Info of parameter item in eventHandler: Tridion.ContentManager.ItemInfo {ID: "tcm:24-88", Title: "Pie dessert wypas", Icon: "T16L0P0", ItemType: "tcm:16", ItemTypeName: "Component"…}
Extensions$Unlocalize$_isEnabled;  false 

This is of a component that is not selected in the publication it was created. If I select a component that is created in this publication the last log will be true.

How do I resolve the issue that the item is always disabled(false) in my menu.

  • Having the requirement to fully load the item for the isEnabled() function is perhaps a bit heavy on the UI, since that function is called quite a lot of times in the background. – Bart Koopman Sep 13 '13 at 9:00
  • I understand that, but the isAvailable() function before isEnabled() filters, so that only components trigger the full load item. – user219 Sep 13 '13 at 9:17
  • That still leaves the item.load() method being asynchronous, so I don't think there is a way to wait for its result. Probably better to simply return true from isEnabled() and handle the exceptions in the execute() call. – Bart Koopman Sep 13 '13 at 9:51
4

The methods _isEnable or _isAvailable are not good places to start loading an item asynchronously it is the same for every method that will return a result because in an asynchronous execution model we cannot have a sequential execution.

SOLUTION:

_isAvailable and _isEnable are always called after you change a selection in the CMS, for instance when you select a new item in a List or in a Tree the CME will call toolbar.updateState(selection). This method will call _isAvailable and _isEnable for all the commands available including yours.

OK, how you can execute some custom logic when you select a new item in a List or in a Tree, well there are few options there, you can use a domain level javascript or my preferred way you can extend a resources group and bind a change event to the current list object or tree control, in that custom change event handler that should be executed at the end you can load your current selected item and then call toolbar.updateState(selection) once your item is already loaded, it will call your _isEnable and _isAvailable methods but your item will be already loaded so you will get access to all the information.

If you are looking for a sample about how to extend a resources group, check it out Extend a resources group

I hope this helps.

UPDATE

I have written a post about this issue with an implementation of this answer. Full Load an Item before _isAvailable and _isEnabled are executed

  • Hi Eric, I think it might help ... So you are revering to a sort of constructor class that's being called before the actual constructor of my extension ? Is there some documentation/blog that describes your approach in more words (details) ? Also its about the menu when you right click a component that show up, not the ribonbar, not sure if that changes anything. – user219 Sep 13 '13 at 20:16
  • No really, a resources group extension will allow you to load a javascript file right after other javascript files, you should find the group that adds the change events for lists and trees and extend that group, then you can add other change event handler to load the current selection and update the context menu again. – Eric Huiza Sep 13 '13 at 20:21
  • Refer to the link in my answer to find information about how to configure it. – Eric Huiza Sep 13 '13 at 20:22
  • Eric. The resources group extension is not completely clear to me, but that might come... How do you suggest to find the group that adds the change events for lists and trees? Maybe we can have a chat on google hangout. – user219 Sep 20 '13 at 20:19
1

Indeed the _isEnabled and _isAvailable method are not meant to be used to evaluate complex logic, rather for data evaluation which is already loaded, such as the item type, organizational item, etc... When you need to perform more evaluations and you need to use asynchronous calls for such you have few options, but here is one nobody has discussed just yet.

You can basically extend the "Anguilla" model by:

Start writing a data extender where you add more information to the item list xml such as an "MyAtt" attribute in the xml and then you can leverage that information in your isEnabled/isAvailable. In this case the right way to leverage this information is by write a model extension to create your own "ExtendedComponent". Here is how you would implement that:

The data list extender should add an attribute with the information you might wanna evaluate in your extension, you need to keep the code for such evaluation as simple as possible since it will impact the UI lists load time.

/// <summary>
    /// Processes the response.
    /// </summary>
    /// <param name="reader">The reader.</param>
    /// <param name="context">The context.</param>
    /// <returns></returns>
    public override System.Xml.XmlTextReader ProcessResponse(System.Xml.XmlTextReader reader, PipelineContext context)
    {   

        XmlDocument list = null;            
        try
        {

            var command = context.Parameters[COMMAND_PARAM].ToString().ToLower();

            switch (command)
            {
                case "getlist":
                    list = ProcessListResponse(reader, context, true);                        
                    break;
                case "getlistsearch":
                case "getlistusingitems":
                case "getlistcheckedoutitems":
                    list = ProcessListResponse(reader, context, false);                        
                    break;
                case "getitem":
                case "saveitem":
                case "savenewitem":
                case "copypasteitem":
                case "checkoutitem":
                case "checkinitem":
                case "localizeitem":
                case "unlocalizeitem":
                case "undocheckoutitem":
                    list = ProcessItemResponse(reader, context);                        
                    break;
            }
            if((list != null && !String.IsNullOrEmpty(list.OuterXml) && list.HasChildNodes)){
                return new XmlTextReader(new StringReader(list.OuterXml));
            }else{
                return reader;
            }


        }
        catch (Exception ex)
        {                
            return reader;   
        }                        
    }

In here you can see that I use two methods, one to extend the lists xml (ProcessListResponse) and another one for the item itself (ProcessItemResponse).

The logic in the "list method" looks like:

#region Folder List Manipulation
                foreach (XElement node in listXml.Descendants(ns + "Item").Where(w => (w.HasAttributes && w.Attribute(TYPE_ATT) != null && w.Attribute(TYPE_ATT).Value == "2")))
                {

                    String currentURI = node.Attribute(ID_ATT).Value;
                    if (TcmUri.IsValid(currentURI))
                    {
                        if (IsDamFolder(currentURI))
                        {
                            if (node.Attribute(ICON_ATT) != null)
                            {
                                                                    node.SetAttributeValue("MyAttribute", "MyExtendedInfo");                                    
                            }
                        }
                    }

                }

The logic in the "ProcessItemResponse@" will modify the item Xml.

Specifically here, I am targeting a "Folder", so basically whenever a folder is loaded in a list, I add my custom information. So far so good, next thing is to register the Data List extender and of course tailor it to your needs. I blogged about this in Data List Extenders Sample

Now the system has the information you'll need in your command, but first you need to give Anguila the details of what would be the "classes" extending the model. You can do that in your model's extension configuration file (remember we are extending the model and for such reason, we need to do it within the "models" scope. To register the model extensions add the following to your config file:

<ext:modelextensions>
  <cfg:itemtypes>
    <cfg:itemtype id="tcm:2" implementation="Tridion.Extensions.DAM.Model.DAMAssetFolder" />        
  </cfg:itemtypes>
</ext:modelextensions>

Next thing, implement your class Tridion.Extensions.DAM.Model.DAMAssetFolder. You need that class to be defined as a model:

<cfg:group name="DAM.Model" merger="Tridion.Web.UI.Core.Configuration.Resources.DomainModelProcessor" merge="always">
    <cfg:domainmodel name="DAM.Model">
      <cfg:fileset>
        <cfg:file type="script">/Scripts/Model/DAMAssetFolder.js</cfg:file>
      </cfg:fileset>
      <cfg:services>                                    
      </cfg:services>
    </cfg:domainmodel>
  </cfg:group>
</cfg:groups>

Last thing, write the javascript for your class:

Type.registerNamespace("Tridion.Extensions.DAM.Model");
Tridion.Extensions.DAM.Model.DAMAssetFolder = function Folder(id) {
Tridion.OO.enableInterface(this, "Tridion.Extensions.DAM.Model.DAMAssetFolder");
this.addInterface("Tridion.ContentManager.Folder", [id]);

var p = this.properties;
p.isDamFolder = undefined;
};

Tridion.Extensions.DAM.Model.DAMAssetFolder.prototype.setDataFromList = function DAMAssetFolder$setDataFromList(node, parentId, timeStamp) {
this.callBase("Tridion.ContentManager.Folder", "setDataFromList", [
    node,
    parentId,
    timeStamp
]);



if (tmp = $xml.getInnerText(node, "@MyAtt")) {
    this.properties.isDamFolder = true;
}
};

/* get item icon */
Tridion.Extensions.DAM.Model.DAMAssetFolder.prototype.getItemIcon = function DAMAssetFolder$getItemIcon() {

var icon = this.callBase("Tridion.ContentManager.Folder", "getItemIcon");
if (this.isDamFolder() && this.isDamFolder() == true) {
    icon = "dam-folder-" + icon;
}    
return icon;
};

Tridion.Extensions.DAM.Model.DAMAssetFolder.prototype.isDamFolder = function DAMAssetFolder$isDamFolder() {
var p = this.properties;
var xmlDoc;
if (p.isDamFolder === undefined && (xmlDoc = this.getXmlDocument())) {
    p.isDamFolder = $xml.selectSingleNode(xmlDoc, "/tcm:Folder[@MyAtt='MyValue']") ? true : false;
}
return p.isDamFolder;
};

As you can see I am baically overwriting a few methods, but the most important for you is probably the last one isDamFolder, which I use to determine whether the folder is a "regular" tridion folder or "my type" folder. In there I do an XPath "/tcm:Folder[@MyAtt='MyValue']" which will look into the attribute I previously added with my data extender, and that information, believe me is available in the isEnabled/isAvailable methods:

Tridion.Extensions.PE4.DAM.Commands.DAMDeleteVariants.prototype._isEnabled = function DAMDeleteVariants$_isEnabled(selection) {
if (selection.getItems().length == 1) {
    var itemUri = selection.getItem(0);
    var item = $models.getItem(itemUri);

    var isAvailable = (item.isMultimedia && item.isOriginalDamAsset && item.isOriginalDamAsset() == true);
    isAvailable = isAvailable || (item.isDamFolder && item.isDamFolder() == true);
    if (isAvailable) {
        return true;
    }
}
return false;

};

Shall you need more details, please let me know. I will blog about it if need it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy