Blogs

About this blog

This community site is for software developers interested in topics related to Web 2.0 and mobile device user interface development. This includes both general trends and technology discussions, as well as specific discussions and other resources involv

Tags

Recent tweets

Introduction to Gridx: Intentions of the Design

I've written an article introducing basics of Gridx here. If you've never heard of Gridx, you might need that one instead of this one. You can also visit Gridx's website or the GitHub site for more details. In this article, I'd like to share some intentions of the design in Gridx, explaining why and how it grew up in the current way. If you have experience using Dojo'sDataGrid orEnhancedGrid, you might find it more interesting.

Some History

Before Gridx, let's talk about some history first.

dojox/grid/DataGrid has been in Dojo for a very long time. It has been widely adopted and customized. But it is very hard to add new features to it. So EnhancedGrid came to rescue. It tried to add a "plugin system" to DataGrid so that new features can be implemented as plugins. But it was still a nightmare since almost every plugin needs to do a lot of "hacking" to get to work. And sometimes the "hack" of one plugin would conflict with that of another.

With the increasing number of requirements, the performance of EnhancedGrid got poorer and poorer, while the code size quickly grew. So we decided to write a new grid, which can completely solve these problems. Thus we have Gridx.

The Module System

The "x" in Gridx means "extensible". From our experience of DataGrid, we learned we have to be ready to add a lot of new features to Gridx, while not all of these feature should run at the same time. So the "plugin system" is crucial. This system must minimize the conflicts among "plugins", and also allow multiple implementations of one feature (like different UIs for sorting). After careful investigation, we find it more suitable to use smaller "modules" instead of big "plugins" as those in EnhancedGrid. For instance, when dealing with a PaginationBar plugin, it actually involves 2 parts: one is the paging mechanism logic, the other is the UI. The paging mechanism is much more likely to be reused than the UI part, and different scenarios might need different UIs, so it makes perfect sense if we split the PaginationBar plugin into two separate but cooperating "modules".

define([

"gridx/Grid" ,

"gridx/modules/pagination/Pagination", //Require only the modules that are necessary

"gridx/modules/pagination/PaginationBar",

......

], function(Grid, Pagination, PaginationBar, ...){

var grid = new Grid({

......

modules: [

Pagination, //The logic pagination module, no UI in this module

PaginationBar //Providing a bar with link buttons to do paging

]

});

});

Two different paging bar UI in Gridx:

Things looked good at first, but other problems occurred when we wanted to make some more basic grid components extensible too, like the vertical scrolling bar. The logic for vertical scrolling is the core of DataGrid, because it implements "virtual scrolling", a mechanism that only renders a few rows at a time while still makes the user believe that all rows are there. But we find the "virtual scrolling" logic is too complicated and unnecessary in many cases, such as a small store. So why not make this feature optional? But to accomplish this we have to make the vertical scroll a "module" too, then why don't we go even further? So now in Gridx, every UI component is a "module", including header and body, and vertical scroller and horizontal scroller. The core of Gridx has no UI at all, it's just a logic grid, allowing any possible UI to be implemented upon it. Every "module" is nothing but a simple Dojo class plus an implicit life cycle. When you need a feature, load the "module" file of it and add it to your grid. If you don't need the feature, you won't have any code about it. If you have a better implementation of this feature, use that new "module", and the old module won't bother you anymore.

The starting up process of Gridx is just to initialize all these modules, one by one. So here comes two questions.

First, which modules go first? Virtual scrolling can not start working if the grid body and header have not been created. PaginationBar must come after the logic paging module Pagination. And the width of the grid body can not be decided when the width of RowHeader or VScroller are not calculated out yet. So there must be some mechanism to manage module initialization order.

Dijit has already been using a simple system for "plugins": dojo.declare."Plugins" are just "Mixin" classes for dijits. This works perfect if the "plugins" do not affect each other. But it will cause problems if initialization order is restricted. We can not require Gridx users to remember which mixins must be put before other mixins. And it'll also be hard to write new mixins, because we'll never know where it'll be placed in the base class list.

The second question is, what if a module depends on another module, or, how to deal with module dependencies? Some basic/nonUI modules are likely to be reused, such as the logic Pagination module. Different UI implementations can be based on this same module. Similarly, RowDragAndDrop module drags selected rows, so it can take advantage of RowSelect module, but it really doesn't care how the row is selected, single selection or multiple selection, swipe selection or keyboard selection, anything can do the job. So here comes the dependency solving problem.

Dependency solving is already well done by the Dojo loader system. But it can't be used in Gridx because the Dojo loader solves dependency of "implementation" instead of "interface". That means, once my RowDragAndDrop module depends on my RowSelect module using Dojo loader, it'll be very hard to have a new RowSelect implementation. Yes, I know the "alias" in dojoConfig can do the job, but that'll be for the whole page/app. If you have two grids on one page in need of two different RowSelect implementations, that will not do the trick.

So it looks like Gridx needs its own "module/plugin" system, solving dependency and managing module life cycle. This is exactly what the core of Gridx does.

The Data Model

Besides extensibility, another major problem of DataGrid is its data model (dojo store). It is not powerful enough to support common grid operations, especially in the case of virtual scrolling. In DataGrid, virtual scrolling and lazy loading are the same thing. There's no API for you to get a row that is not currently loaded. This is okay when grid is only displaying store data. But it'll be very inconvenient when the data shown in grid is not the same as those in store. This is really a common case since we have "get" and "formatter" widely used in DataGrid column definition. If a page developer wants to go through all the rows in the grid, or just get the data of 100th row (probably not loaded) in current view, he/she has to fetch raw data from the store and then call some private function in grid to transform it into grid data. Note if the grid is sorted or filtered, things get much more complicated because you have to pass the exact same fetch arguments to the store to get the current row index order. All the grid data loading facilities can not be used because they tightly bind to the rendering logic. This feels really wasteful.

It looks like we definitely need a new data model that best suits grid operations, if we want to do anything interesting programmatically. So we designed "Model" in Gridx.

The "Model" in Gridx is row-oriented, just like store. But it provides not only raw store data, but also formatted grid data. This "Model" can be accessed via grid.model. It provides a small set of API that can conveniently retrieve data from grid. For example, grid.model.byIndex(99).data, gets the grid data of the 100th row, and grid.model.byIndex(99).rawData, get the store data instead. And this "Model" manages row index in a way that sorting/filtering/row moving are all taken into consideration. So the following code can easily determine where the current first row will be after sorting:

var id = grid.model.indexToId(0);

grid.model.sort([{colId: 'col1'}]);

grid.model.when({id: id}, function(){

var index = grid.model.idToIndex(id);

});

The "when" function here is another story of Gridx Model. It handles all asynchronous operations, thus makes all other functions in grid.model synchronous. For more details on this function, please refer to this article.

Gridx users usually don't need to use these model functions directly. Gridx modules will wrap them into more user-friendly APIs or UI behaviors. For example, when you want to get a row in Gridx, you can just do the following:

var row = grid.row(0);

This row object contains almost everything you need about a row. Same for columns and cells:

grid.column(3).moveTo(0);

grid.cell(0, 1).select();

Conclusion

Two major design topics, module system and data model, are covered here to explain why Gridx is designed in this way. There're always better ideas on grid design, so I'm just starting a topic here and really welcome anyone to join this discussion.