Saturday, July 09, 2011

Building a dynamic image based carousel with PrimeFaces 2.2.1

Time for another demonstration. This time, I needed to build a carousel with data including text and images. And, I chose to use the PrimeFaces carousel component for this purpose. The code presented here is available for download at my BitBucket repository.

Building a carousel with the PrimeFaces component is quite easy if you can not embedding rich and dynamic content within the carousel. The PrimeFaces User Guide downloadable off the PrimeFaces documentation page, does contain an example to get you started. I will extend that example to demonstrate how to use the StreamedContent class to enable display of images that could possibly be stored in a database or in a content management system.

The user interface layout of the desired final outcome is shown below.


The intent is to demonstrate how to display non-static content in the carousel. This is easy for text content as the carousel component supports the embedding of h:outputText tags and iteration through a collection using the var attribute. The example in the PrimeFaces user guide already covers this. We'll be looking at the use of the f:param tag to iterate through a collection of StreamedContent objects. To make things simple, we will be reading images off files instead of BLOB objects in a database, or similar binary input streams.

The Facelet


We will start by designing the facelet page that will use the carousel component. The code for the facelet is listed below:



The carousel definition contains:

  • a collection of items that used to render each individual item in the carousel. This is specified using the value="#{imageBean.images}" expression. For all practical purposes, the carousel is similar to a h:dataTable component as far as this attribute is concerned. Without specifying a value attribute that is referenced by a collection, one cannot render a collection of items in the carousel, whose size is unknown at design time. We'll see the definition of the managed bean class ImageBean later.
  • a name to reference an element in the above defined item collection. This is similar to the var attribute of the h:dataTable component. In the above facelet, var="image" is used to create a temporary reference to every item in the images collection of the ImageBean class.
  • a panel grid that is used to act as a container for the image, the title and the description of the image. It contains a style attribute whose usage is described later.
  • a PrimeFaces p:graphicImage component that is used to stream the image back to the browser. Note the use of the f:param tag. The p:graphicImage component cannot use the temporary reference created by the var attribute of the carousel component. The value attribute must evaluate to a StreamedContent object, but more importantly the p:graphicImage component will be rendered as a img tag with a src attribute containing a URL that will be used by the browser to fetch the image (present in a StreamedContent object). This URL does not change across the items in the carousel, and hence a parameter must be passed to identify the image being requested. This is done using the value="#{image.id}" attribute of the parameter that is used to pass a parameter of name photo_id. We shall see how this parameter is parsed by another Managed Bean, in a later section of the article.
  • a couple of h:outputText components that are used to display the title and description items. These use the reference created by the var attribute, and unlike the p:graphicImage component there are no issues involving the use of the temporary reference in this case.

The Managed Bean for the carousel component


The managed bean used to manage the collection of images is listed below:



The class is quite simple in design, in that it is used to store a reference to a list of Photo objects that contains merely the text content (title and description) of every image in the carousel. The Photo class is listed below. Again, it is quite simple in design, and is merely a bean class. Note the declaration of the id attribute of the class; this is used by the carousel component to provide the parameter to the p:graphicImage component.



The Managed Bean for the graphicImage component


The managed bean class that is responsible for serving image requests is listed below.



This is obviously the class that allows the carousel to obtain images dynamically, so a more descriptive explanation of it's behavior is provided.

Note that this class is a request scoped bean. Every image added in the carousel using the p:graphicImage tag will result in a separate GET request for the image. Once the image contents has been dispatched to the browser, the managed bean is no longer needed, unless the carousel needs to be rendered once again. It also not good design to store references to StreamedContent objects beyond a single request. Therefore, this class is not designed to be a session scoped class.

The following snippet is used to parse the parameter passed to the bean via the f:param tag.



It is important to know that the value of the photoId variable can be null, even if the browser has not issued a request with a null photoId. This needs to be handled by creating a StreamedContent object that points to a "default" image, as show in the following snippet.



The defaultFileContent variable is a static variable that is initialized on loading of the class. The initialization process isn't exactly production quality, and one might consider adding exception handling to address this in a better manner. But for now, it would suffice to know that the getFileContent method should not return null. Apparently, the method is invoked during the Render Response phase and it is quite possible that the value of photoId is null, thereby necessitating the need to return a non-null StreamedContent object. The "default" image is also returned in the event of an image Id being incorrect (out of bounds in this case).

Finally, if the image Id is valid, the context class loader is used to obtain a reference to a input stream that can be used to construct the StreamedContent object as demonstrated below:



In a production application, you would obviously be fetching the images from a database, a content management system, or from disk. In either case, you will need to obtain an InputStream object and use it to construct the StreamedContent object.

Styling the carousel


The carousel component uses the YUI carousel component to render the actual carousel in the browser. It relies on some JavaScript goodness to set the sizes of the individual items in the carousel on loading the document. If the height and width of the items is not specified, then it is quite possible that the items will not be displayed correctly; it is quite possible to have the items overflow each other. The YUI carousel requires that the height and width values be specified on each item (li0 in the list (ol) that is generated. Hence, it is necessary to set the style in the h:panelgrid component as follows:



Other interesting stuff


  • The carousel described above displays correctly in all modern browsers except IE 9 where it is quite horribly broken (even in compatibility mode, where the YUI carousel appears to work). This might require a revision to the posted code.
  • The PrimeFaces carousel automatically offers a drop down if the number of items exceeds 5 pages. At least that was my observation. Quite obviously, the carousel is not meant to be used in scenarios where a lot of items are present in a collection.
  • The images in the carousel are fetched upfront on initial document load, and not when pagination occurs. Other components would therefore be suitable if the number and size of the images exceeds a limit where performance becomes a problem; this would vary from application to application.
  • The icons used in the project are from the Polaroid icon set.

5 comments:

Anonymous said...

only run to request scope why?

Anonymous said...

This is not working - photo_id is null in bean. Any idea why?

Unknown said...

You might want to check the scope of the beans. Session scoped beans usually do not work here.

Unknown said...

Thanks, it works great!!

Unknown said...

Thank you a lot for your article. However, you have two bugs in here.. First, if defaultStream is taken more than once, first one will close the stream (because you cannot read twice from a stream), so you'll get IOException. Secondly, after you are checking the parsed ID, you should return the fileContent. Bye!