Thursday, June 20, 2013

Deep dive into UI Templates

One of the best new features in 3.0  is UI Templates. The main user interface for browsing a gallery is built from jsRender templates you can edit using only HTML and JavaScript skills. In this post we’ll take a deep look at how it works and how you can harness its power.


Let’s start by identifying the five sections of the gallery that are built from UI templates.



These images show the five templates: header, left pane, right pane, album, and media object. The center pane shows either the album or media object template, depending on whether the user is looking at an album or an individual media object.

I should point out that the Actions menu and the album breadcrumb links are the only portion of the page that is not part of any template. Instead, its HTML is generated on the server and inserted into the page under the header. A future version of GSP may merge this portion into the header template so that the entire page is 100% template driven.

Each template consists of a chunk of HTML containing jsRender syntax and some JavaScript that tells the browser what to do with the HTML. Typically the script invokes the jsRender template engine and appends the generated HTML to the page. A robust set of gallery data is available on the client that gives you access to important information such as the album and media object data, user permissions, and gallery settings. We’ll get into the structure of this data later in this post.

You can view the UI template definitions on the UI Templates page in the site admin area. In these two images you can see the HTML and JavaScript values:



You can assign which albums this particular template applies to on the Target Albums tab. If multiple templates target the same album, the most specific one wins. For example, if the default template is assigned to ‘All albums’ and a second template is assigned to the Samples album, the second template will be used for the Samples album and any of its children because that template definition is ‘closer’ to the album.


The preview tab lets you see the result of any edits you make before saving the changes.


Anatomy of the left pane

Let’s take a close look at how one of the templates works. We’ll choose the left pane template first. The HTML is simple:

<div id='{{:Settings.ClientId}}_lptv'></div>
It defines a single, empty div tag and gives it a unique ID. The text between the double brackets is jsRender syntax that refers to the ClientId property of the Settings object. This particular property provides a string that is unique to the current instance of the Gallery user control on the page. When it is rendered on the page you end up with HTML similar to this:

<div id='gsp_g_lptv'></div>

Note: Defining a unique ID is not required for most galleries, but it is there for admins who want to include two instances of the gallery control on a page. For example, you might have a slideshow running in one part of a web page and a video playing in another.

Astute observers will notice that a single div tag doesn’t look anything like a complex treeview. So how does that div tag eventually become the treeview? Let’s look at the JavaScript that is part of the template:

// Render the left pane, but not for touchscreens UNLESS the left pane is the only visible pane
var isTouch = window.Gsp.isTouchScreen();
var renderLeftPane = !isTouch  || (isTouch && ($('.gsp_tb_s_CenterPane:visible, .gsp_tb_s_RightPane:visible').length == 0));

if (renderLeftPane ) {
 $('#{{:Settings.LeftPaneClientId}}').html( $.render [ '{{:Settings.LeftPaneTmplName}}' ]( window.{{:Settings.ClientId}}.gspData ));

 var options = {
  albumIdsToSelect: [{{:Album.Id}}],
  navigateUrl: '{{:App.CurrentPageUrl}}'

 // Call the gspTreeView plug-in, which adds an album treeview
 $('#{{:Settings.ClientId}}_lptv').gspTreeView(window.{{:Settings.ClientId}}.gspAlbumTreeData, options);

Technically this text is not pure JavaScript. Do you see the jsRender syntax in there? That’s right, it is ALSO a jsRender template that will be run through the jsRender engine to produce pure JavaScript just before execution. This is an extraordinarily powerful feature and can be harnessed to produce a wide variety of UI possibilities. Imagine writing script that loops through the images in an album to calculate some value or invoke a callback to the server to request data about a specific album.

The first thing the script does is decide whether the left pane template should even be appended to the page. We decide not to show it on touchscreens for two reasons: (1) touchscreens typically have small screens and cannot afford the real estate required by the left pane (2) the splitter control that separates the left, center, and right panes does not work well on touchscreens (that’s something I need to work on).

The script refers to the function window.Gsp.isTouchScreen(). You’ll find this function in the minified file gallery.min.js. If you are editing the gallery you will probably prefer to have un-minified script files loaded into the browser. You can easily accomplish this by switching the debug setting to ‘true’ in web.config.

If the script decides the left pane is to be rendered, it executes this line:

$('#{{:Settings.LeftPaneClientId}}').html( $.render [ '{{:Settings.LeftPaneTmplName}}' ]( window.{{:Settings.ClientId}}.gspData ));
This will look familiar to anyone with jsRender experience or other types of template engines. In plain English, it says to take the template having the name LeftPaneTmplName and the data in the variable gspData, run in through the jsRender engine, and assign the resulting HTML to the HTML element named LeftPaneClientId. In other words, it takes the string <div id='{{:Settings.ClientId}}_lptv'></div>, converts it to <div id='gsp_g_lptv'></div>, and adds it to the page’s HTML DOM.

So far all the script has done is add a div tag to the page, but we still need to convert it into the album tree. That’s what the last section does:

var options = {
 albumIdsToSelect: [{{:Album.Id}}],
 navigateUrl: '{{:App.CurrentPageUrl}}'

// Call the gspTreeView plug-in, which adds an album treeview
$('#{{:Settings.ClientId}}_lptv').gspTreeView(window.{{:Settings.ClientId}}.gspAlbumTreeData, options);

The JavaScript file I mentioned above contains several jQuery plug-ins to help reduce the complexity of the templates and to maximize the amount of script that is cached by the browser (inline script is never cached). Here we use jQuery to grab a reference to our generated div tag and we invoke the gspTreeView plug-in on it, passing along the album treeview data and a few options. The tree data is a JSON object containing the album structure and is included in every page request. In turn, the gspTreeView plug-in is a wrapper around the third party jQuery tree control jsTree, the code for which is in the lib.min.js file (or lib.js if you are running with debug=true). There are several third party script libraries in that file.

The treeview plug-in builds an HTML tree from the data and appends it to the div tag, resulting in the tree view you see in the left pane. You can play with the HTML and JavaScript and then use the preview tab to see how those edits affect the output.

Adding a logo to the header

A common requirement is to add your logo to the top of the page. This was a little tricky to do in previous versions of GSP because you had to first figure out how the header was constructed, which code file to edit, and then make your change. Some changes required editing C# code and recompiling due to the use of server controls. Now all you have to do is edit the header UI template. Let’s say we want to replace the title with a logo, like this:


Go to the UI Templates page and choose Header from the gallery item dropdown. We want to preserve the original header template in case we want to revert to it, so select Copy as new, enter a name, then click Save:


Now go back to the default header template by selecting Default from the Name dropdown. Click the Target Albums tab and uncheck the albums. Save. This forces the new template to take over for all albums.


Return to the new template and activate the HTML tab. Scroll down until you find the place where the title is rendered:

{{if Settings.Title}}
 <p class='gsp_bannertext'>
 {{if Settings.TitleUrl}}<a title='{{:Settings.TitleUrlTt}}' href='{{:Settings.TitleUrl}}'>{{:Settings.Title}}</a>{{else}}{{:Settings.Title}}{{/if}}</p>

Replace this section with an img tag that points to your logo and save:

<img src='' />

That’s it! The logo now appears in the top left corner as shown in the image shown earlier.

The client data model

Each page contains a rich set of data that is included with the browser request. A global JavaScript variable named gspData exists that is scoped to the current gallery user control instance. In a default installation, you can find it at window.gsp_g.gspData, as seen in this image from Chrome:


Let’s take a brief look at each top-level property:

ActiveGalleryItems – An array of GalleryItem instances that are currently selected. A gallery item can represent an album or media object (photo, video, audio, document, YouTube snippet, etc.)

ActiveMetaItems – An array of MetaItem instances describing the currently selected item(s).

Album – Information about the current album. It has two important properties worth explaining: GalleryItems and MediaItems. Both represent albums and media objects, but a GalleryItem instance contains only basic information about each item while a MediaItem instance contains all the metadata and other details about an item. Because a GalleryItem instance is lightweight, it is well suited for album thumbnail views where you only need basic information. Therefore, to optimize the data passed to the browser, the MediaItems property is null when viewing an album and the GalleryItems property is null when viewing a single media object.

App – Information about application-wide settings, such as the skin path and current URL.

MediaItem – Detailed information about the current media object. Has a value only when viewing a single media object.

Resource – Contains language resources.

Settings – Contains gallery-specific settings.

User – Information about the current user.

Here is a complete list of client objects and their properties (click the first image for a larger version):



Most properties are self-explanatory, but a few can use explanation:

Album.SortById – An integer that indicates which metadata field the album is sorted by. It maps to the enumeration values of MetadataItemName. The ID values are listed in the table on the Metadata page in the site admin area.

Album.VirtualType – An integer that indicates the type of the current album. It maps to the enumeration values of VirtualAlbumType (NotSpecified=0, NotVirtual=1, Root=2, Tag=3, People=4, Search=5)

MediaItem.Index - The one-based index of this media object among the others in the containing album.

MediaItem.Views and GalleryItem.Views – The Views property is an array of DisplayObject instances. Each DisplayObject represents a thumbnail, optimized, or original view of a media object or album.

MediaItem.ViewIndex and GalleryItem.ViewIndex – The zero-based index of the view currently being rendered in the browser. This value can be used to access the matching view in the Views property. The thumbnail is always be at index 0.

DisplayObject.HtmlOutput – The HTML for the display object. For thumbnails this is a <img> tag; the optimized/original HTML is generated from the media template defined for this MIME type on the Media Templates page in the site admin area.

DisplayObject.ScriptOutput – The JavaScript to execute to help render the display object. For example, it may contain the script necessary to initialize and run FlowPlayer for a Flash video. This property is populated from the script defined for this MIME type on the Media Templates page in the site admin area.

DisplayObject.Url – An URL that links directly to the media object file. For example, this value can be assigned to the src attribute of an img, video, or audio tag.

DisplayObject.ViewSize – This is an integer that maps to the enumeration values of DisplayObjectType (Unknown=0, Thumbnail=1, Optimized=2, Original=3, External=4).

DisplayObject.ViewType – An integer that maps to the enumeration values of MimeTypeCategory (NotSet=0, Other=1, Image=2, Video=3, Audio=4). For example, imagine a video MediaItem or GalleryItem. The Views property will have two or three DisplayObject instances (three if a web-optimized version has been created; otherwise two).The DisplayObject instance having ViewSize=1 represents the thumbnail image, so you can expect the ViewType to be 2 (image). But the remaining DisplayObject instances will have ViewType=3 (video).

MetaItem.GTypeId – Indicates the kind of gallery object the meta item describes. It is an integer that maps to the GalleryObjectType enumeration (None=0, All=1, MediaObject=2, Album=3, Image=4, Audio=5, Video=6, Generic=7, External=8, Unknown=9). Typically the client will never have the values of None (0), All (1), or Unknown (9).

The default UI templates use this data model, so they are an excellent place to look for examples of how to do things. It can be a little tricky, though, because much of the data access is done within the jQuery plug-ins.

UI Template examples

To get you up and running quickly, let’s look at a few examples. Paste these samples into a template to see it in action.

Show username or login link

<p>{{if User.IsAuthenticated}}
 Welcome, {{:User.UserName}}
 <a href='{{:App.CurrentPageUrl}}?g=login'>Log In</a>

Determine if user has permission to delete media objects in the current album

<p>You {{if !Album.Permissions.DeleteMediaObject}}do not{{/if}} have permission to delete media objects in the album '{{:Album.Title}}'.</p>

Show the number of albums and media objects in the current album

<p>This album contains {{:Album.NumAlbums}} child albums and {{:Album.NumMediaItems}} media objects ({{:Album.NumGalleryItems}} total).</p>

Show a bulleted list of hyperlinked album and media object titles

 {{for Album.GalleryItems}}
   <a href='{{: ~getGalleryItemUrl(#data) }}'>{{:Title}} ({{getItemTypeDesc:ItemType}})</a>

This example shows a few interesting things:

  • for loop – The template loops through each gallery item of the album. Inside the loop the data context changes to that of the GalleryItem instance. For example, the {{:Title}} refers to the title of the gallery item being iterated on.
  • getGalleryItemUrl  – The call to getGalleryItemUrl is a jsRender helper function defined in gallery.js. Helper functions are useful when you need some JavaScript to figure out how to render something, like here to help calculate the URL to the album or media object. You can define your own helper function in the window.Gsp.Init function of gallery.js.
  • #data – A reference to the currently scoped data instance is passed to getGalleryItemUrl. In this example it is an instance of GalleryItem. You can refer to parent data items with the parent keyword. For example, using {{}} from inside the GalleryItems loop gets the current album title by navigating up to the root data structure and then back down to the current Album title.
  • jsRender converter – The string {{getItemTypeDesc:ItemType}} says to run the ItemType property through a jsRender converter named getItemTypeDesc. A converter takes a property as input and massages it in some way. For example, you might want to do this to format a date/time value. The converter in this example converts an integer to a description such as Image, Album, etc. It is defined in gallery.js and, as with helper functions, you can define you own.

Note: This example requires that the GalleryItems property of the Album is populated. We discussed earlier that this property will have a value when viewing an album but is null when viewing an individual media object. To get this to work when a single media object is visible, change GalleryItems to MediaItems in the template.

Note: In 3.0.0 getGalleryItemUrl and getItemTypeDesc are defined in the gspThumbnails plug-in, so they are only available when that plug-in is invoked (as it is when viewing album thumbnails). In 3.0.1, I intend to refactor these to the generic page load function window.Gsp.Init in gallery.js, making them available to the entire page, regardless of which templates are visible.


Add image preview on thumbnail hover

When you hover over a thumbnail image, you see a larger version of the image appear in a popup that disappears automatically when you move the cursor away:


This example requires adding to both the HTML and JavaScript album UI template. Open the album UI template and insert this HTML as the first line in the HTML tab:

<div id='pvw' style='display:none;'><img id='imgpvw' style='width:400px;'></div>

Then add this JavaScript to the end of the script in the JavaScript tab:

 appendTo: $('.gsp_floatcontainer'),
 autoOpen: false,
 position: { my: "left top", at: "left center" } 

$('.thmb[data-it=' + window.Gsp.Constants.ItemType_Image + '] .gsp_thmb_img').hover(
 function () {
  $('#imgpvw').prop('src', this.src.replace('dt=1', 'dt=2'));
  $('#pvw').dialog("option", "width", 425)
   .dialog( "option", "position", { my: "left+30 top+20", at: "right bottom", of: $(this)} )

$('.gsp_floatcontainer', $('#{{:Settings.ClientId}}')).mouseleave(function() {

It works by opening a jQuery UI dialog window on the hover event of the thumbnail image.


Add virtual album links

Enhance the left pane by adding some links to popular tags or people in your gallery:


This is easily accomplished by adding the following to the end of the HTML text for the left pane UI template:

<p style='color:#B2D6A2;' class='gsp_addtopmargin5 gsp_addleftmargin4'>TAGS</p>
<div class='gsp_addleftmargin10'>
 <p><a href='{{:App.CurrentPageUrl}}?tag=Party'>Party</a></p> 
 <p><a href='{{:App.CurrentPageUrl}}?tag=Vacation'>Vacation</a></p>
 <p><a href='{{:App.CurrentPageUrl}}?tag=Family'>Family</a></p>

<p style='color:#B2D6A2;' class='gsp_addtopmargin5 gsp_addleftmargin4'>PEOPLE</p>
<div class='gsp_addleftmargin10'>
 <p><a href='{{:App.CurrentPageUrl}}?people=Roger%20Martin%2bMargaret'>Roger & Margaret</a></p>
 <p><a href='{{:App.CurrentPageUrl}}?people=Margaret'>Margaret</a></p>
 <p><a href='{{:App.CurrentPageUrl}}?people=Skyler'>Skyler</a></p>

When creating the hyperlink, notice spaces are encoded with %20 (e.g. Roger%20Martin). To create a link for multiple tags, separate them with %2b (an encoded + sign). The gallery does not support searching for both tags and people at the same time.


Show last 5 objects added in album

Show the five most recently added items in an album at the top of the center pane:


Before editing the template, go to the Metadata page and make sure the DateAdded property is visible for albums and media objects. If not, make it visible and click the rebuild action.

Open the album UI template and look for the following text in the HTML tab:

<div class='gsp_floatcontainer'>
Paste this text just before it:

<div id='divRecent' class='gsp_floatcontainer'></div>
Now paste the following at the end of the script on the JavaScript tab:

var getUrl = function(gItem) {
 var qs = { aid: gItem.IsAlbum ? gItem.Id : null, moid: gItem.IsAlbum ? null : gItem.Id };

 if (gItem.IsAlbum) {
 // Strip off the tag and people qs parms for albums if present, since we want them to link directly to the album.
 // We don't do this for media objects so they can be browsed within the context of their tag/people.
 qs.tag = null;
 qs.people = null; = null;

  return Gsp.GetUrl(document.location.href, qs);

  type: "GET",
  url: window.Gsp.AppRoot + '/api/albums/' + window.{{:Settings.ClientId}}.gspData.Album.Id + '/galleryitems/?sortByMetaNameId=111&sortAscending=false',
  dataType: 'json',
  success: function (galleryItems) {
   galleryItems.splice(5); // Keep the first 5 elements; get rid of the rest
   var html = "<p style='color:#B2D6A2;' class='gsp_addleftmargin2'>LAST 5 ITEMS</p><div class='gsp_addleftmargin5'";
   $.each(galleryItems, function(idx, galleryItem) {
    html += "<p><a href='" + getUrl(galleryItem) + "'>" + galleryItem.Title + "</a></p>";
  html += "</div>";
  error: function (response) {
    $.gspShowMsg("Action Aborted", response.responseText, { msgType: 'error', autoCloseDelay: 0 });

What we’re doing here is first adding a div tag to receive the HTML we generate. In the JavaScript we make a call to the server to retrieve the items in this album sorted in descending order on the DateAdded field (sortByMetaNameId=111). You can see the ID values of other fields on the Metadata page.

When the server returns the data (which is an array of GalleryItem instances), we get rid of everything except the first 5 items, then we iterate through them and build up an HTML string. Finally we append the HTML to the div tag, resulting in it being shown on the screen.

NOTE: When you request a sorted list as shown above, the gallery tries to be smart and saves this sort preference in your profile, causing the album to be sorted on this field whenever you view it. This is probably not what you want. I intend to change this behavior in a future version. Meanwhile, if you don’t mind editing the source code you can stop this behavior by commenting out the call to the PersistUserSortPreference function in GalleryObjectController.GetGalleryItemsInAlbum().

What if you wanted to include the date added property in the HTML? For example, instead of the title ‘Road to nowhere’, you have ‘Road to nowhere (Added 2013-08-27)’. Unfortunately, the AJAX method we are using returns an array of GalleryItem instances, which is a lightweight data structure that doesn’t have any metadata in it. Refer to the screenshot earlier in this post to see its properties. What we really need is an array of MediaItem properties, which includes the metadata. And there is an API call we can make to get that data. It looks like this:


The 22 is the album ID. But there’s a problem. Version 3.0.0 does not allow us to request a custom sort through this method, so we just get the items in the same order as they are shown in the album view. I intend to fix this in a future version.

AJAX API calls

The previous example made a call to the web server to retrieve data. There are a number of API calls available for reading and writing data in the gallery. Here’s a brief overview:

HTTP Method URL Description
GET /api/albums/{AlbumId}/inflated Returns a GalleryData instance. Album.GalleryItems will be populated; Album.MediaItems will be null. Optional parms: top (int), skip (int) Can be used for paged results. Example: api/albums/22/inflated/?top=5&skip=5 returns the 6th – 10th gallery items in album 22.
GET /api/albums/{AlbumId}/galleryitems Returns an array of GalleryItem instances representing the albums and media objects in the album. Optional parms: sortByMetaNameId (int), sortAscending (bool) Can be used to return items in the album in a custom sort. Note that 3.0.0 will cause the user’s profile to be updated with this sort preference for this album. Example: /api/albums/22/galleryitems/?sortByMetaNameId=111&sortAscending=false returns the items in album 22 sorted in descending order on the DateAdded metadata property.
GET /api/albums/{AlbumId}/mediaitems Returns an array of MediaItem instances representing the albums and media objects in the album. Optional parms: none
GET /api/albums/{AlbumId}/meta Returns an array of MetaItem instances representing the metadata for the album. Optional parms: none
POST /api/albums Updates a limited set of properties for the album: DateStart, DateEnd, SortByMetaName, SortAscending, IsPrivate, Owner
POST /api/albums/{AlbumId}/sortalbum?sortbyMetaNameId={{MetaNameId}}&sortAscending={{true|false}} Resort the items in the album and persist to the database. No value is returned. Perform a GET to retrieve the sorted items.
POST /api/albums/{AlbumId}/getsortedalbum Resort the items in the album but DO NOT persist to the database. Requires an instance of AlbumAction to be POSTed. See example in gallery.js.
DELETE /api/albums/{AlbumId} Deletes the specified album, including the media files and directory.
GET api/mediaitems/{MediaObjectId}/inflated Returns a GalleryData instance. The MediaItem property contains data for the specified media item. The Album.MediaItems property contains data for the remaining items in the album. Album.GalleryItems is null.
GET api/mediaitems/{MediaObjectId}/meta Returns an array of MetaItem instances belonging to the specified media object.
POST /api/mediaitems/createfromfile Adds a media file to an album. Prior to calling this method, the file should exist in App_Data\_Temp. Requires an instance of AddMediaObjectSettings to be POSTed. See example usage in gs\pages\task\addobjects.ascx.
PUT /api/mediaitems/ Persists changes to the database about the MediaItem instance PUT to the method. Current implementation saves the title only and requires that the media item exist.
DELETE /api/mediaitems/{MediaObjectId} Permanently deletes the media object from the file system and data store.
GET /api/meta/tags/?galleryId={GalleryId} Gets an array of Tag instances containing all tags used in the specified gallery. Optional parms: q (string) Specifies a search string to filter the tags by. Example: /api/meta/tags/?galleryId=1&q=bob returns all tags with the text bob in them.
GET /api/meta/people/?galleryId={GalleryId} Gets an array of Tag instances containing all people tagged in the specified gallery. Optional parms: q (string) Specifies a search string to filter the tags by. Example: /api/meta/tags/?galleryId=1&q=bob returns all tags with the text bob in them
POST /api/meta/rebuildmetaitem?metaNameId={MetaNameId}&galleryId={GalleryId} Rebuild the data entries for the specified meta property for all media items in the gallery.
PUT /api/meta/ Persists the POSTed MetaItem instance to the database.
GET /api/task/startsync/?albumId={AlbumId}&isRecursive={true|false}&rebuildThumbnails={true|false}&rebuildOptimized={true|false}&password={Password} Begins a synchronization. Requires enabling the remote sync option on the Admin page in the site admin area.
GET /api/task/statussync/?id={GalleryId} Gets the status of the synchronization. Requires an HTTP header variable named X-ServerTask-TaskId to be set to the sync task ID. See example on gs/pages/task/synchronize.ascx
GET /api/task/abortsync/?id={GalleryId} Cancels a synchronization. Requires an HTTP header variable named X-ServerTask-TaskId to be set to the sync task ID. See example on gs/pages/task/synchronize.ascx
POST /api/task/startsync/ Begins a synchronization. Requires an instance of SyncOptions to be POSTed and an HTTP header variable named X-ServerTask-TaskId to be assigned a value. The sync page uses this method to start a sync.
GET /api/task/purgecache/ Purges the cache on the web server. The next HTTP request will retrieve data from the database.
POST /api/task/logoff/ Logs off the current user


Rob Diaz-Marino said...

In the article, you make it sound like disabling the left frame actually makes it go away altogether (for mobile devices etc, to conserve screen real estate).

When I tried hard-coding renderLeftPane to be false (on IE10/Firefox/Chrome on a standard desktop machine), sure it gets rid of the tree view but there is still a blank left pane taking up space. I even tried deleting all HTML and JavaScript from the template and there is still a blank left pane showing up beside the album/media object. Sure, I can collapse it, but I don't want it there at all.

So is there a way I can make the left pane truly go away?

Roger Martin said...

Go to the Gallery Control Settings page, select the option 'Override the following default settings', then uncheck the two options that start with 'Show left pane'. That will prevent the left pane template from even being sent to the browser, saving bandwidth and improving client performance.

Rob Diaz-Marino said...

Hmmm...when I looked at those settings I discovered that I had already tried that. Neither of the "Show Left Pane" options are checked, and still the left pane is showing.

Oddly, I tried turning off the right pane and that doesn't work either, the right pane doesn't go away when I uncheck it and save the settings.

Roger Martin said...

Rob - This should be moved to the forum. If you create a forum thread and give me credentials for logging into your gallery, I can take a look.