Portal and Drag-and-Drop Features of Ext GWT

Exclusive offer: get 50% off this eBook here
Ext GWT 2.0: Beginner's Guide

Ext GWT 2.0: Beginner's Guide — Save 50%

Take the user experience of your website to a new level with Ext GWT using this book and eBook

$23.99    $12.00
by Daniel Vaughan | November 2010 | Open Source

The Google Web Toolkit is a great way for Java developers to create AJAX-based rich Internet applications without requiring in-depth knowledge of JavaScript or having to deal with the quirks of different browsers.

This article by Daniel Vaughan, author of the book Ext GWT 2.0: Beginner's Guide, covers the portal and drag-and-drop features of GXT. We will start by learning how to use the Portal layout and Portlet and then move on to making use of GXT's drag-and-drop features in a practical way. Specifically, we will cover the following topics:

  • Portal
  • Portlet
  • Draggable
  • DragSource
    • GridDragSource
    • ListViewDragSource
    • TreeGridDragSource
    • TreePanelDragSource
  • DropTarget
    • GridDropTarget
    • ListViewDropTarget
    • TreeGridDropTarget
    • TreePanelDropTarget
  • ColumnLayout
  • RowLayout

 

Ext GWT 2.0: Beginner's Guide

Ext GWT 2.0: Beginner's Guide

Take the user experience of your website to a new level with Ext GWT

  • Explore the full range of features of the Ext GWT library through practical, step-by-step examples
  • Discover how to combine simple building blocks into powerful components
  • Create powerful Rich Internet Applications with features normally only found in desktop applications
  • Learn how to structure applications using MVC for maximum reliability and maintainability
        Read more about this book      

(For more resources on GWT, see here.)

Portlet class

The Portlet class extends ContentPanel to provide a special type of panel that can be repositioned in the Viewport by the user with a Portal container. It may appear similar to a window in a desktop application. Creating a Portlet is similar to creating other containers. This code:

Portlet portlet = new Portlet();
portlet.setHeight(150);
portlet.setHeading("Example Portlet");

creates a Portlet like this:

A Portlet can be excluded from being repositioned by pinning it using:

portal.setPinned(true);

Apart from that, a Portlet inherits all the features of a standard ContentPanel.

The Portal class

A Portal is a special container for Portlet components. In fact, it is a Container containing a collection of LayoutContainer components arranged using ColumnLayout. Each of those LayoutContainer components in turn is able to contain Portlet components, arranged using a RowLayout.

Portal also supports dragging and dropping of Portlet components, both in terms of changing the row it is in within a column and the column within the Portal.

When creating a Portal, we need to set the number of columns the Portal should create in the constructor. We also need to set the widths of each column before using the setColumnWidth method of the Portal.

So to create a Portal with two columns, (one using 30 percent of the width and the second 70 percent) we would define it as follows:

Portal portal = new Portal(2);
portal.setColumnWidth(0, 0.3);
portal.setColumnWidth(1, 0.7);

We can then add a Portlet to each column like this:

Portlet portlet1 = new Portlet();
portlet1.setHeight(150);
portlet1.setHeading("Example Portlet 1");
portal.add(portlet1, 0);

Portlet portlet2 = new Portlet();
portlet2.setHeight(150);
portlet2.setHeading("Example Portlet 2");
portal.add(portlet2, 1);

This will produce the following output:

Both Portlet components can be dragged and dropped into different positions. The Portlet turns into a blue box while being dragged as shown in the following screenshot:

A Portlet will automatically resize and ft into the column in which it is dropped, as seen in the next screenshot:

ToolButton

Like ContentPanel that Portlet extends, we can add ToolButton components to the header. These can be very useful for making a Portlet look and behave even more like windows in a desktop application.

portlet.getHeader().addTool(new ToolButton("x-tool-minimize"));
portlet.getHeader().addTool(new ToolButton("x-tool-maximize"));
portlet.getHeader().addTool(new ToolButton("x-tool-close"));

The output can be seen as shown in the following screenshot:

At the moment, we are using ContentPanel components in our example application and laying them out using a BorderLayout. We shall now see that it does not take much to change the ContentPanel components into Portlet components and manage them using a Portal.

Portlet components are ideally suited to being independent, self-contained user interface elements that respond to the data passed to them. Rather than tying them into a Portal directly, we can use the MVC components to cause the Portal to respond to the creation of a new Portlet to preserve that independence.

Time for action – creating a Portal Controller and a Portlet View

  1. The first thing we need to do is add a new EventType to the existing AppEvents class named NewPortletCreated. We will fire this when we create a new Portlet.

    public static final EventType NewPortletCreated = new EventType();

  2. Create a new class named PortalController that extends Controller.

    public class PortalController extends Controller {

  3. Create a new class named PortalView that extends View.

    public class PortalView extends View {

  4. Create a constructor that sets the Controller of the PortalView.

    public PortalView(PortalController portalController) {
    super(portalController);
    }

  5. Returning to PortalController, create a variable to hold the PortalView and override the initialize method to set the view.

    private PortalView portalView;
    @Override
    public void initialize() {
    super.initialize();
    portalView = new PortalView(this);
    }

  6. Create a constructor that registers each EventType the PortalController should observe, specifically NewPortletCreated creation and Error.

    public PortalController() {
    registerEventTypes(AppEvents.NewPortletCreated );
    registerEventTypes(AppEvents.Error);
    }

  7. Override the handleEvent method to forward any events to the View apart from errors which for the time being we will just log to the GWT log.

    @Override
    public void handleEvent(AppEvent event) {
    EventType eventType = event.getType();
    if (eventType.equals(AppEvents.error)) {
    GWT.log("Error", (Throwable) event.getData());
    } else {
    forwardToView(portalView, event);
    }
    }

  8. Returning to PortalView, create a new portal field consisting of a Portal component with two columns.

    private final Portal portal = new Portal(2);

  9. Override the initialize method to set the width of the two columns, the first to 30 percent of the width of the Portal and the second to 70 percent.

    @Override
    protected void initialize() {
    portal.setColumnWidth(0, 0.3);
    portal.setColumnWidth(1, 0.7);
    }

  10. Now create a Viewport, set the layout to FitLayout, add the Portal, and then add the Viewport to GWT's RootPanel.

    @Override
    protected void initialize() {
    portal.setColumnWidth(0, 0.3);
    portal.setColumnWidth(1, 0.7);

    final Viewport viewport = new Viewport();
    viewport.setLayout(new FitLayout());
    viewport.add(portal);
    RootPanel.get().add(viewport);
    }

  11. We also need to implement the handleEvent method of the View. For now, we will catch the NewPortletCreated event, but we will not do anything with it yet.

    @Override
    protected void handleEvent(AppEvent event) {
    EventType eventType = event.getType();
    if (eventType.equals(AppEvents.NewPortletCreated )) {

    }
    }

  12. Finally, go to the onModuleLoad method of the EntryPoint RSSReader class and instead of creating an AppController, create a PortalController, and remove the line that forwards an Init AppEvent, as we will not be using it. The onModuleLoad method will now look like this:

    public void onModuleLoad() {
    final FeedServiceAsync feedService =
    GWT.create(FeedService.class);
    Registry.register(RSSReaderConstants.FEED_SERVICE, feedService);
    Dispatcher dispatcher = Dispatcher.get();
    dispatcher.addController(new PortalController());
    }

What just happened?

We created the basic framework for a Portal layout of our application. However, if we started it now, we would just get a blank screen. What we need to do is add Portlet components.

The actual Portlet components are not too complicated. They will just act as wrappers.

Ext GWT 2.0: Beginner's Guide Take the user experience of your website to a new level with Ext GWT using this book and eBook
Published: December 2010
eBook Price: $23.99
Book Price: $39.99
See more
Select your format and quantity:
        Read more about this book      

(For more resources on GWT, see here.)

Time for action – creating the Navigation Portlet

  1. Create a new class named NavPortlet that extends Portlet.

    public class NavPortlet extends Portlet {

  2. Create a constructor and set the heading, layout, and height of the Portlet.

    public NavPortlet()
    {
    setHeading("Navigation");
    setLayout(new FitLayout());
    setHeight(610);
    }

  3. In the RSSReaderConstants class, add a new constant to act as the ID for this Portlet.

    public static final String NAV_PORTLET = "navPortlet";

  4. Back in the constructor of NavPortlet, set the ID of the Portlet to be the NAV_PORTLET constant.

    public NavPortlet()
    {
    setHeading("Navigation");
    setLayout(new FitLayout());
    setHeight(610);
    setId(RSSReaderConstants.NAV_PORTLET);
    }

  5. Now create a new instance of the NavPanel class to provide the content of the Portlet. As the Portlet already has a title, hide the header of the NavPanel and add it to the Portlet.

    public NavPortlet() {
    setHeading("Navigation");
    setLayout(new FitLayout());
    setHeight(610);
    setId(RSSReaderConstants.NAV_PORTLET);
    NavPanel navPanel = new NavPanel();
    navPanel.setHeaderVisible(false);
    add(navPanel);

  6. We now need to tell the Portal that this new Portlet has been created. We will do that by forwarding an AppEvent of the NewPortletCreated EventType with this Portlet as the data payload using the Dispatcher.

    public NavPortlet()
    {
    setHeading("Navigation");
    setLayout(new FitLayout());
    setHeight(610);
    setId(RSSReaderConstants.NAV_PORTLET);
    NavPanel navPanel = new NavPanel();
    navPanel.setHeaderVisible(false);
    add(navPanel);
    Dispatcher.forwardEvent(AppEvents.NewPortletCreated , this);
    }

  7. Now we have to respond to the NewPortletCreated event in the PortalView. So in PortalView, create a method called onNewPortletCreated and implement it so that if the NavPortlet is contained in the data of the AppEvent, it will be added to the first column of the Portal. All the other Portlet components will be added to the second column.

    private void onNewPortletCreated (AppEvent event) {
    final Portlet portlet = (Portlet) event.getData();
    if (portlet.getId() == RSSReaderConstants.NAV_PORTLET) {
    portal.add(portlet, 0);
    } else {
    portal.add(portlet, 1);
    }
    }

  8. In the handleEvent method, call the onNewPortletCreated method when an AppEvent with the NewPortletCreated EventType is handled.

    @Override
    protected void handleEvent(AppEvent event) {
    EventType eventType = event.getType();
    if (eventType.equals(AppEvents.NewPortletCreated )) {
    onNewPortletCreated (event);
    }
    }

  9. All we need to do now is go back to the onModuleLoad method of the RSSReader EntryPoint class and create a new instance of NavPortlet and the MVC events will take care of the rest.

    public void onModuleLoad() {
    final FeedServiceAsync feedService =
    GWT.create(FeedService.class);
    Registry.register(RSSReaderConstants.FEED_SERVICE, feedService);
    Dispatcher dispatcher = Dispatcher.get();
    dispatcher.addController(new PortalController());
    new NavPortlet();
    }

  10. Finally, start the application and you will see a Portlet complete with a list of feeds.

What just happened?

We have created a new navigation Portlet and constructed the framework to automatically add it to the Portal. With this in place, it is now straighforward to create two more portlets, one for displaying feeds and one for displaying items.

Time for action – creating more portlets

  1. Create two new constants in RSSReaderConstants for the two new Portlet components we are going to create, namely, FEED_PORTLET and ITEM_PORTLET.

    public static final String FEED_PORTLET = "feedPortlet";
    public static final String ITEM_PORTLET = "itemPortlet";

  2. Create a new class named FeedPortlet, extending Portlet, and build a constructor in the same way as we did with NavPortlet, this time setting the ID of the Portlet to the FEED_PORTLET constant.

    public FeedPortlet() {
    setHeading("Feed");
    setLayout(new FitLayout());
    setHeight(350);
    setId(RSSReaderConstants.FEED_PORTLET);
    }

  3. Create a new FeedPanel field, set its header to invisible in the constructor of the FeedPortlet, and add it to the underlying Portlet.

    private final FeedPanel feedPanel = new FeedPanel();

    public FeedPortlet()
    {
    setHeading("Feed");
    setLayout(new FitLayout());
    setHeight(350);
    setId(RSSReaderConstants.FEED_PORTLET);
    feedPanel.setHeaderVisible(false);
    add(feedPanel);
    }

  4. As before, tell the Portal about this new Portlet by forwarding an AppEvent of the NewPortletCreated EventType with the Portlet as the data payload, using the Dispatcher.

    public FeedPortlet()
    {
    setHeading("Feed");
    setLayout(new FitLayout());
    setHeight(350);
    setId(RSSReaderConstants.FEED_PORTLET);
    feedPanel.setHeaderVisible(false);
    add(feedPanel);
    Dispatcher.forwardEvent(AppEvents.NewPortletCreated, this);
    }

  5. Create a new class called ItemPortlet again extending Portlet and with a similar constructor to the other Portlet components, but this time using an ItemPanel as the content.

    public ItemPortlet()
    {
    setHeading("Item");
    setLayout(new FitLayout());
    setHeight(250);
    setId(RSSReaderConstants.ITEM_PORTLET);
    final ItemPanel itemPanel = new ItemPanel();
    itemPanel.setHeaderVisible(false);
    add(itemPanel);
    Dispatcher.forwardEvent(AppEvents.NewPortletCreated, this);
    }

  6. With the new portlets defined, we can now create new instances of each in the onModuleLoad method of the RSSReader EntryPoint class.

    public void onModuleLoad() {
    Registry.register(RSSReaderConstants.FEED_SERVICE, GWT
    create(FeedService.class));
    Dispatcher dispatcher = Dispatcher.get();
    dispatcher.addController(new PortalController());
    new NavPortlet();
    new FeedPortlet();
    new ItemPortlet();
    }

  7. Now start the application, and you will see that there are three Portlet components in the Portal.

What just happened?

We now have three Portlet components in our Portal. However, two are blank, and selecting a feed from the list will not do anything because there is nothing to pass the data to the other portlets. We are going to solve this in a different way by using drag-and-drop.

Drag-and-drop

Drag-and-drop is another built-in feature of GXT that is useful and flexible. Like other GXT features, drag-and-drop is a feature common in desktop applications but unusual in web applications.

Many GXT components already have specific drag support, but you can extend this to any component you like by implementing the Draggable class.

The Draggable class

The Draggable class is used to add drag behavior to any component by providing a wrapper around it. For example, if we wanted to make a Button draggable, we would do the following:

Button dragButton = new Button("Draggable Button");
Draggable draggaable = new Draggable(dragButton);

Now the user will be able to drag the Button. The new location for the Button will be shown by a blue "ghost" rectangle of the Button like this:

By default, a draggable component can be dragged in any direction. However, this can be constrained to not allow horizontal or vertical dragging using setConstrainVertical and setConstrainHorizontal respectively.

Button dragButton = new Button("Draggable Button");
Draggable draggable = new Draggable(dragButton);
draggable.setConstrainVertical(true);

The DragSource class

The DragSource class identifies a component that drag and drops can be initiated from.

A DragSource is used to define the data that will be dragged during the drag-and-drop operation. Data can either be moved or copied from the source component. As this setting is only set when the data reaches the target, a DragSource also needs to be able to remove data from the source component.

The data can be set using the setData method of the DragSource. When the drag starts, a new DNDEvent is created. Alternatively, data can be set at this point by using the setData method on DNDEvent itself by overriding the onDragStart method.

DragSource source = new DragSource(component) {
@Override
protected void onDragStart(DNDEvent event) {
event.setData(component);
}
};

DragSource implementations

All the data-backed controls—Grid, ListView, TreeGrid, and TreePanel—have ready- made DragSource implementations. These support both single and multi-selection.

The implementations for each component are as follows:

Component

DragSource

Grid

GridDragSource

ListView

ListViewDragSource

TreeGrid

TreeGridDragSource

TreePanel

TreePanelDragSource

The DropTarget class

DropTarget is the other end of the drag-and-drop operation. DropTarget identifies a component that can receive the data from a drag-and-drop operation.

A DropTarget is responsible for a number of things. The first is determining if the object that is dragged over it is valid for a drop and showing a visual indication.

Data is obtained from the DropTarget by overriding the onDragDrop method and calling the getData method of the DNDEvent.

DropTarget target = new DropTarget(component) {
@Override
protected void onDragDrop(DNDEvent event) {
super.onDragDrop(event);
Object data = event.getData();
}

It can also specify the DND.Operation, which is either COPY or MOVE. If it is moved (the default), the corresponding DragSource needs to remove the data from its component.

target.setOperation(DND.Operation.MOVE);

DropTarget implementations

As with DragSource, there are specific ready-made implementations for Grid, ListView, TreeGrid, and TreePanel.

Component

DropTarget

Grid

GridDropTarget

ListView

ListViewDropTarget

TreeGrid

TreeGridDropTarget

TreePanel

TreePanelDropTarget

Grouping sources and targets

Both DragSource and DropTarget classes can be put into groups to constrain where data can be dragged and dropped to. This is useful for avoiding the user dropping data into a component that is unable to handle data of that type.

Simply use the setGroup method with a String parameter to identify the group of both the DragSource and DropTarget classes to put them in the same group. Once a DropTarget is in a group, it will only accept data from a DragSource in the same group.

Using drag-and-drop

We can use drag-and-drop with the Portal layout of our example application. This will give an example of how to use built-in and custom DragSource and DropTarget components.

The first thing we are going to do is allow users to drag a feed from the FeedList in the NavPortlet. When dropped on the FeedPortlet, this will cause the items in the feed to be displayed in an ItemGrid.

Time for action – dragging and dropping of feeds

  1. In the RSSReaderConstants class, create a new constant named FEED_DD_GROUP to act as an ID for the drag-and-drop group for feeds.

    public static final String FEED_DD_GROUP = "feedDDGroup";

  2. At the end of the onRender method of the FeedList class, create a new DragSource object that wraps the FeedList.

    DragSource source = new DragSource(feedList);

  3. Override the onDragStart method of the DragSource so that the BeanModel object is selected from the FeedList and is attached to the DNDEvent as data.

    DragSource source = new DragSource(feedList) {
    @Override
    protected void onDragStart(DNDEvent event) {
    event.setData(feedList.getSelection());
    }
    };

  4. Set the group of the DragSource to FEED_DD_GROUP.

    DragSource source = new DragSource(feedList) {
    @Override
    protected void onDragStart(DNDEvent event) {
    event.setData(feedList.getSelection());
    }
    };
    source.setGroup(RSSReaderConstants.FEED_DD_GROUP);

  5. Now in FeedPortlet, create a new method named onFeedsDropped. This should extract the BeanModel objects contained in the data of the DNDEvent, and with each of them, create a new ItemGrid for each Feed in the same way as we did in the FeedView.

    private void onFeedsDropped(DNDEvent event) {
    List<BeanModel> beanModels = event.getData();
    for (BeanModel beanModel : beanModels) {
    Feed feed = beanModel.getBean();
    final ItemGrid itemGrid = new ItemGrid(feed);
    TabItem tabItem = new TabItem(feed.getTitle());
    tabItem.setId(feed.getUuid());
    tabItem.setData("feed", feed);
    tabItem.add(itemGrid);
    tabItem.addListener(Events.Select, new
    Listener<TabPanelEvent>() {
    @Override
    public void handleEvent(TabPanelEvent be) {
    itemGrid.resetSelection();
    }
    });
    tabItem.setClosable(true);
    feedPanel.addTab(tabItem);
    }
    }

  6. Override the onRender method of FeedPortlet, and in it, create a new DropTarget using the actual FeedPortlet itself as the target component.

    protected void onRender(Element parent, int index) {
    super.onRender(parent, index);
    DropTarget target = new DropTarget(this);
    }

  7. Override the onDragDrop method of the DropTarget so that it passes the DNDEvent to the onFeedsDropped method.

    protected void onRender(Element parent, int index) {
    super.onRender(parent, index);
    DropTarget target = new DropTarget(this) {
    @Override
    protected void onDragDrop(DNDEvent event) {
    super.onDragDrop(event);
    onFeedsDropped(event);
    }
    };
    }

  8. Set the operation of the DropTarget to be DND.Operation.COPY so that the selected feeds are not removed from the FeedList when the data is dropped.

    protected void onRender(Element parent, int index) {
    super.onRender(parent, index);
    DropTarget target = new DropTarget(this) {
    @Override
    protected void onDragDrop(DNDEvent event) {
    super.onDragDrop(event);
    onFeedsDropped(event);
    }
    };
    target.setOperation(DND.Operation.COPY);
    }

  9. Set the group of the DropTarget to FEED_DD_GROUP, so that it is in the same group as the DragSource we defined earlier.

    protected void onRender(Element parent, int index) {
    super.onRender(parent, index);
    DropTarget target = new DropTarget(this) {
    @Override
    protected void onDragDrop(DNDEvent event) {
    super.onDragDrop(event);
    onFeedsDropped(event);
    }
    };
    target.setOperation(DND.Operation.COPY);
    target.setGroup(RSSReaderConstants.FEED_DD_GROUP);
    }

  10. Start the application and drag a feed from the FeedList to the FeedPortlet to display the content of the feed in an ItemGrid.

What just happened?

We used a DragSource together with a custom DropTarget to allow the drag-and-drop of the feeds to view as a list of items.

We can now implement drag-and-drop in a similar way to allow items to be dragged from the FeedPortlet ItemGrid to the ItemPortlet to display them.

Time for action – dragging and dropping items

  1. In the RSSReaderConstants class, create a new constant named ITEM_DD_GROUP to act as an ID for the drag-and-drop group for items.

    public static final String ITEM_DD_GROUP = "itemDDGroup";

  2. At the end of the onRender method of the ItemGrid, but before the Grid is added, create a new GridDragSource using the Grid as the source component.

    GridDragSource source = new GridDragSource(grid);

  3. Set the group of the GridDragSource to ITEM_DD_GROUP.

    GridDragSource source = new GridDragSource(grid);
    source.setGroup(RSSReaderConstants.ITEM_DD_GROUP);

  4. In the constructor of the ItemPortlet class, create a DropTarget where the ItemPortlet is the target.

    DropTarget target = new DropTarget(this);

  5. Override the onDragDrop method of the DropTarget to retrieve the list of Item objects and then call the displayItem method of the ItemPanel to display the first Item object in the list.

    DropTarget target = new DropTarget(this) {
    @Override
    protected void onDragDrop(DNDEvent event) {
    super.onDragDrop(event);
    List<Item> items = event.getData();
    itemPanel.displayItem(items.get(0));
    }
    };

  6. Again we need to set the operation of the target to COPY to avoid removing the Item objects from the Grid and use setGroup to put the DropTarget in the same group as its DragSource. This will prevent the user from being able to drop a feed into the ItemPortlet.

    DropTarget target = new DropTarget(this) {
    @Override
    protected void onDragDrop(DNDEvent event) {
    super.onDragDrop(event);
    List<Item> items = event.getData();
    itemPanel.displayItem(items.get(0));
    }
    };
    target.setOperation(DND.Operation.COPY);
    target.setGroup(RSSReaderConstants.ITEM_DD_GROUP);

  7. Now start the application, drop a Feed from the NavPortlet to the FeedPortlet, and then an Item from the FeedPortlet to the ItemPortlet.

What just happened?

We implemented drag-and-drop between the FeedPortlet and the ItemPortlet. We now have three portlets that are completely independent and just respond to the data that is dragged into them.

Summary

We have looked at GXT's drag-and-drop features and used Portlet components to create components that independently respond to the data that is dragged and dropped into them.


Further resources on this subject:


Ext GWT 2.0: Beginner's Guide Take the user experience of your website to a new level with Ext GWT using this book and eBook
Published: December 2010
eBook Price: $23.99
Book Price: $39.99
See more
Select your format and quantity:

About the Author :


Daniel Vaughan

Daniel Vaughan has worked with enterprise web applications for over 12 years. He is currently a software architect for a UK financial institution. An experienced Java developer, Daniel first started working with Google Web Toolkit soon after it was released in 2006 and loved the power and simplicity it bought to web application development. When Ext GWT came along he was an early adopter and has used it as part of several large projects.

Daniel currently splits his time between the beautiful tranquility of the Cotswolds, England and the fast-moving city state of Singapore. He enjoys travel, scuba diving, and learning new ideas.

Books From Packt


Google Web Toolkit 2 Application Development Cookbook
Google Web Toolkit 2 Application Development Cookbook

OGRE 3D 1.7 Beginner's Guide
OGRE 3D 1.7 Beginner's Guide

Blender 2.5 Lighting and Rendering
Blender 2.5 Lighting and Rendering

Inkscape 0.48 Essentials for Web Designers
Inkscape 0.48 Essentials for Web Designers

PostgreSQL 9.0 High Performance
PostgreSQL 9.0 High Performance

OpenCart 1.4 Beginner's Guide
OpenCart 1.4 Beginner's Guide

jQuery Plugin Development Beginner's Guide
jQuery Plugin Development Beginner's Guide

NHibernate 3.0 Cookbook
NHibernate 3.0 Cookbook


No votes yet

Post new comment

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
H
n
F
r
F
7
Enter the code without spaces and pay attention to upper/lower case.
Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software