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

               

Friday, October 4, 2013

Improving Tridion .Net Controls - Component Presentation

In several .Net implementations I've noticed certain deficiencies related to the Component Presentation .Net Web Control. Basically this controls allow us to retrieve content from either the Tridion Broker database or file system. Additionally it will execute any server side code in the form of ASP .NET inline code / code blocks or REL tags. In the next sections in this post we will explore some of the deficiencies.

.NET server side code must be located in the File System.

A restriction is that if the developer wants to publish component presentations that includes .NET server side code to the database the standard Component Presentation web control won't execute this code. This is issue is related to the design behind the Component Presentation Assembler class, this class is intended to be technology agnostic so that it could be reason why it is not specialized to execute neither .NET or JAVA code from the database.

In a previous post I have written about how to solve this issue using a Virtual Path provider. This approach will instruct ASP .NET to consider Component Presentations stored in a database as ASCX files.


Child Controls are not initialized properly.

The standard Component Presentation Web Control renders its content by overriding the Render method in the following way.

protected override void Render(HtmlTextWriter writer)
{
    if(HttpContext.Current != null && HttpContext.Current.Application != null)
    {
        ComponentPresentationAssembler assembler = new ComponentPresentationAssembler(pageUri, this.Page);
        writer.Write(assembler.GetContent(componentUri, templateUri));

        this.RenderChildren(writer);
    }
}

This approach will work fine for common things like having <tridion:ComponentLink> or <tridion:ComponentPresentation> as child controls. However let's consider an scenario where we need more complex controls initialization (let's consider having fully functional ASP .NET Form in the form of a component presentation) like <asp:RegulerExpressionValidator> or <asp:RequiredFieldValidator> in those case the standard Component Presentation Web Control won't be able to initialize them.

The main problem resides on the fact that Render is called too late in the ASP .Net page life cycle, it will lead to child controls not to be initialized.

I order to solve it, I create a new version of the Component Presentation, this version will use the CreateChildControls method instead of Render.

The following code sample is a refactored and improved version of code I presented before in the  Virtual Path provider post.

protected override void CreateChildControls() {
    if (HttpContext.Current != null && HttpContext.Current.Application != null) {
        ComponentPresentationMeta meta = new ComponentPresentationMetaFactory(ComponentUri).GetMeta(ComponentUri, TemplateUri);
        if (meta != null) {
            string contentType = meta.ContentType;
            if (contentType.StartsWith("ASCX")) {
                using (ComponentPresentationFactory factory = new ComponentPresentationFactory(ComponentUri)) {
                    Tridion.ContentDelivery.DynamicContent.ComponentPresentation componentPresentation =
                        factory.GetComponentPresentation(ComponentUri, TemplateUri);

                    TcmUri componentId = new TcmUri(ComponentUri);
                    TcmUri templateId = new TcmUri(TemplateUri);

                    ComponentMetaFactory metaFactory = new ComponentMetaFactory(componentId.PublicationId);
                    IComponentMeta componentMeta = metaFactory.GetMeta(componentId.ItemId);

                    string virtualPath = string.Format("dcp_{0}_{1}.ascx", componentId.ItemId, templateId.ItemId);
                    CacheInvalidation(virtualPath, componentMeta);

                    Control control = Page.LoadControl(virtualPath);
                    this.Controls.Add(control);
                }
            }
            else {
                Tridion.ContentDelivery.Web.UI.ComponentPresentation componentPresentation = new Tridion.ContentDelivery.Web.UI.ComponentPresentation();
                componentPresentation.Page = this.Page;
                componentPresentation.PageUri = PageUri;
                componentPresentation.ComponentUri = ComponentUri;
                componentPresentation.TemplateUri = TemplateUri;
                this.Controls.Add(componentPresentation);
            }
        }
    }
}

The source code above will execute server side code from the database and it will also initialize child controls properly. This code can be improved to recognize if there is a Virtual Path provided present if not, it can retrieve the component presentation from the file system as before.

Child Controls events are not properly attached and initialized.

This is another issue related to complex scenarios where we have complex child controls like the ones present in an ASP .NET web form. Basically even if we use CreateChildControls instead of Render event handlers are still not attached properly. In order to solve it our Component Presentation Web Control should inherit from CompositeControls instead from Web Control.

Here a sample of how the class declaration should look like.

[DefaultProperty("ComponentUri"),
ToolboxData("<{0}:ComponentPresentation runat=server></{0}:ComponentPresentation>"),
ParseChildrenAttribute(ChildrenAsProperties = false)]
public class ComponentPresentation : CompositeControl {
...
}

Here how the whole class should look like.

[DefaultProperty("ComponentUri"),
ToolboxData("<{0}:ComponentPresentation runat=server></{0}:ComponentPresentation>"),
ParseChildrenAttribute(ChildrenAsProperties = false)]
public class ComponentPresentation : CompositeControl {

    [Bindable(true), Category("Appearance"), DefaultValue("")]
    public string ComponentUri { get; set; }
    [Bindable(true), Category("Appearance"), DefaultValue("")]
    public string TemplateUri { get; set; }
    [Bindable(true), Category("Appearance"), DefaultValue("")]
    public string PageUri { get; set; }

    protected override void CreateChildControls() {
        if (HttpContext.Current != null && HttpContext.Current.Application != null) {
            ComponentPresentationMeta meta = new ComponentPresentationMetaFactory(ComponentUri).GetMeta(ComponentUri, TemplateUri);
            if (meta != null) {
                string contentType = meta.ContentType;
                if (contentType.StartsWith("ASCX")) {
                    using (ComponentPresentationFactory factory = new ComponentPresentationFactory(ComponentUri)) {
                        Tridion.ContentDelivery.DynamicContent.ComponentPresentation componentPresentation =
                            factory.GetComponentPresentation(ComponentUri, TemplateUri);

                        TcmUri componentId = new TcmUri(ComponentUri);
                        TcmUri templateId = new TcmUri(TemplateUri);

                        ComponentMetaFactory metaFactory = new ComponentMetaFactory(componentId.PublicationId);
                        IComponentMeta componentMeta = metaFactory.GetMeta(componentId.ItemId);

                        string virtualPath = string.Format("dcp_{0}_{1}.ascx", componentId.ItemId, templateId.ItemId);
                        CacheInvalidation(virtualPath, componentMeta);

                        Control control = Page.LoadControl(virtualPath);
                        this.Controls.Add(control);
                    }
                }
                else {
                    Tridion.ContentDelivery.Web.UI.ComponentPresentation componentPresentation = new Tridion.ContentDelivery.Web.UI.ComponentPresentation();
                    componentPresentation.Page = this.Page;
                    componentPresentation.PageUri = PageUri;
                    componentPresentation.ComponentUri = ComponentUri;
                    componentPresentation.TemplateUri = TemplateUri;
                    this.Controls.Add(componentPresentation);
                    componentPresentation.Dispose();
                }
            }
        }

    }

    private void CacheInvalidation(string virtualPath, IComponentMeta componentMeta) {
        if (HttpContext.Current.Cache[virtualPath] == null) {
            HttpContext.Current.Cache[virtualPath] = componentMeta.LastPublicationDate;
        }
        else {
            DateTime lastPublishedDate = (DateTime)HttpContext.Current.Cache[virtualPath];
            if (lastPublishedDate < componentMeta.LastPublicationDate) {
                HttpContext.Current.Cache.Remove(virtualPath);
                HttpContext.Current.Cache[virtualPath] = componentMeta.LastPublicationDate;
            }
        }
    }
}

Thursday, September 26, 2013

Working with Tridion and ASP .Net 4 Mobile

In this post I would like to introduce some concepts about how to use the new mobile capabilities of ASP .Net 4 and Tridion. ASP .Net 4 brings a nice feature called display modes where we can define specific views for display modes without changing the controllers. Additionally Tridion brings a new feature called Context Engine Cartridge (CEC) this new feature uses Ambient Data Framework to put context specific information in the form of claims.

ASP .Net 4 Display Modes

ASP .Net 4 comes with two display modes by default (default and mobile), they are determined by the Razor View Engine which is responsible to determine if the request is coming from a mobile device or not. This feature is very simple and powerful, let's put some examples.

Imagine you want to define two different versions of a razor view, one for regular browsers and one for mobile browsers, if that is the case you will need to follow this rules.

Default View Name: Index.cshtml
Mobile View Name: Index.mobile.cshtml

It means that we just need to add the "mobile" word after the regular view name. The Razor View Engine will use the home.mobile.cshtml view if the requests comes from a mobile device. If we want to integrate this feature with Tridion we just need to create two pages using the same page template (cshtml extension) but using different content, metadata and logic which should be specific for mobile or regular pages.







I have this simple code in my Controller as you can see, the same code will work for both of them.

public class HomeController : Controller {
    public ActionResult Index() {          
        return View();
    }
}

Here the result in a regular desktop browser.








Here the result in a mobile browser.



Adding Custom Display Modes

Additionally to the default and mobile display modes, ASP .Net 4 allows us to define custom display modes for specific devices and browsers, in this example I will add an iPhone specialized view.



In order to define an specialized display mode so that the Razor View Engine will use the index.iphone.cshtml view when the request is coming from an iPhone we will insert a new Display Mode in the Global.asax

Here a code sample.

protected void Application_Start() {
    AreaRegistration.RegisterAllAreas();

    WebApiConfig.Register(GlobalConfiguration.Configuration);
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);

    DefaultDisplayMode iPhoneDisplayMode = new DefaultDisplayMode("iPhone");
    iPhoneDisplayMode.ContextCondition = (HttpContextBase context) => {
         return context.GetOverriddenUserAgent().IndexOf("iPhone", StringComparison.OrdinalIgnoreCase) != -1;
    };

    DisplayModeProvider.Instance.Modes.Insert(0, iPhoneDisplayMode);
}

In the code sample I am declaring a new display mode called "iPhone" this will directly map with the view name it will lead to a view called index.iphone.cshtml. Other important point in this code is the ContextCondition property which is a delegate that is executed for each view that is being requested by the Razor View Engine, this delegate should point to a boolean function. In the sample I am checking if the User Agent contains the word "iPhone". Additionally we should insert it as the first one. The rule is that the most specific display modes should be inserted first and the less specific last.

Context Engine Cartridge

There is a very nice feature that was recently added to the Tridion technology stack. This feature is similar to Wurfl or Device Atlas with the difference that is integrated with Tridion via Ambient Data Framework using a Cartridge that will make all the device context information available in the Claim Store.

Context Engine Cartridge uses "Aspects" to divide information, CEC comes with 3 aspects out of the box "device", "browser" and "os".

Claims are identified following this naming convention.

Uri claimUri = new Uri("taf:claim:context:<ASPECTNAME>:<PROPERTYNAME>");

I have updated the custom Display Mode for iPhone devices code to use CEC.

DefaultDisplayMode iPhoneDisplayMode = new DefaultDisplayMode("iPhone");
iPhoneDisplayMode.ContextCondition = (HttpContextBase context) => {
    ClaimStore claimStore = AmbientDataContext.CurrentClaimStore;
    Uri mobileUri = new Uri("taf:claim:context:device:mobile");
    Uri modelUri = new Uri("taf:claim:context:device:model");

    bool isMobile = claimStore.Get<string>(mobileUri).AsBool();
    string model = claimStore.Get<string>(modelUri);

    return isMobile && model.Equals("iPhone");
};

DisplayModeProvider.Instance.Modes.Insert(0, iPhoneDisplayMode);


In the code above I am accessing to the ClaimStore in the ContextCondition delegate and checking the mobile and model device properties.

Here how the page is looking in an iPhone screen.