Thursday, December 10, 2009

GXT grids - Binding with beans

I had to use many grids in my first GXT project. I got to know that I have to use GXT right after I sent my first release of the front end to the client. I had never used GXT, the deadline was tight. I was puzzled, should I spend time in understanding the library or just start coding with the help of google. I started coding the simpler screens, I was able to show some progress to the client in next couple of days. I turned to the grids after writing all the screens in which there were no grids. I opened their demo and tried to understand the code – I was failed. I started experimenting with the examples and tried to copy it with my classes – I got no success. I searched the internet and I found one description copied to many places by different authors. The description was not enough for me. I read it again and again and tried to implement it in my application. After many trial and errors I was successful. Following is a detailed description of problem and solution.

I had prewritten data access objects which I had to use for grid data binding. I found a good tutorial for this at gxt site at following link

http://www.extjs.com/helpcenter/index.jsp?topic=/com.extjs.gxt.help/html/tutorials/beanmodel.html

After I read the above I did following steps

1) Implement BeanModelTag in all beans. Following is an example of TimeZone bean which contains Timezone record from database.

public class TimeZone extends LightEntity implements Serializable, BeanModelTag
{
private int id;

private String name;
private String defaultValue;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getDefaultValue() {
return defaultValue;
}

public void setDefaultValue(String defaultValue) {
this.defaultValue = defaultValue;
}
}

Note that it is serializable and it is marked as BeanModel by implementing BeanModelTag interface.

Next step is to fetch data from the database. My service implementation returns an ArrayList of above bean and it is as follows.


public class TimeZoneServiceImpl extends PersistentRemoteService implements
TimeZoneService {

public TimeZoneServiceImpl() {
super(HibernateUtil.getBeanManager());
}

@Override
public List getAllTimeZones() throws Exception {
Session currentSession = HibernateUtil.getSessionFactory(false)
.getCurrentSession();

List timeZones = DataCubeUtil.getTimeZoneList(currentSession);

return timeZones;
}

}

You can call this service as a simple RPC call or you can use gxt RPCProxy which handles callback on its own. Using RPCProxy is easy and straight forward it will bind the grid columns with your beans properties using ColumnModel.

final TimeZoneServiceAsync timeZoneService = GWT
.create(DeviceService.class);
RpcProxy proxy = new RpcProxy() {
@Override
public void load(Object loadConfig, AsyncCallback callback) {

timeZoneService.getAllTimeZones(callback);

}
};

Abovel lines make an RPC call and fetch data from TimeZoneService. But we have not loaded this data yet and we have not provide any grid-data binding configuration. To load data into a store we need a loader, we can use ListLoader or PagingLoader etc. I used a ListLoader in this manner

BeanModelReader reader = new BeanModelReader();
ListLoader loader = new BaseListLoader(proxy, reader);

Here reader is used to handle creating new model instances from the beans being returned from the data proxy. So at the stage when you introduce reader into your code, your actual bean is gone and at runtime the gxt libaray creates its own instance of TimeZone class.

Now I have to create a store which can store the loaded data.

ListStore store = new ListStore(loader);

So far what we have done is that we created a bean, created a proxy which brings a list of beans (filled with data), we created a reader which can read the returned beans, then we created a store and a loader, loader loads data into the store. But how it will bind the loaded data into the grid. This is done by ColumnModel.

ColumnConfig columnConfig=getColumnConfig();
ColumnModel cm = new ColumnModel(configs);

Following function creates binding by setting column id same as bean's properties. Here I would like to mention that while binding the grid column to bean's property it follows java's coding convention. Like the id of column name is "name" (column.setId("name"). While creating the binding at runtime that library find the getter method by concatenating get infront of id, in this example it would be getName(). If this getter is not found gxt will not raise any exception. It simply ignores this binding. For example if the name of getter was get_name() then your binding will not be done as expected. So be careful while creating these bindings.

private ColumnConfig getColumnConfig(){
ArrayList config=new ArrayList();

ColumnConfig column = new ColumnConfig();
column.setId("name");
column.setHeader("Name");
column.setAlignment(HorizontalAlignment.CENTER);
column.setWidth(84);
config.add(column);

ColumnConfig column = new ColumnConfig();
column.setId("defaultValue");
column.setHeader("Default Value");
column.setAlignment(HorizontalAlignment.CENTER);
column.setWidth(84);
config.add(column);


}

After you make the binding your grid is ready to go.

To me gxt grids with their store API is robust solution. You can create grids with checkboxes, with multiple headers, with custome styles for each column data etc. They saved me a lot of time and efforts.

14 comments:

  1. Shamaila! You rock, this helped me out perfectly. Lack of docs/seeming code complexity had me stumped but after reading your post I was up and running in just a few hours!

    Thanks-
    John-

    ReplyDelete
  2. Thanks John for nice comments. Yeah this is really bad that we have such a great API but lack of proper documentation makes using it harder. I think if users write such blogs or write their experiences in relative forums it will help others. Also I have seen that most of the people nearly same problems while playing such APIs. If we post our problems and their solutions it will help creating good community and it will help others.

    ReplyDelete
  3. Thanks for the great info. I am new to Gxt and have a doubt.
    If I have all my beans in an array and I dont need to use the RPC call, then how do I get the beans in my store such that it will work with grid? i.e. These 3 lines below will get replaced with what?
    BeanModelReader reader = new BeanModelReader();
    ListLoader loader = new BaseListLoafer(proxy, reader);
    ListStore store = new ListStore(loader);

    Thanks

    ReplyDelete
  4. Good Work Shamaila!

    I was wondering if it would be possible for you to upload your project for others to use it as a skeletal architecture pattern?

    thank you in advance

    ReplyDelete
  5. Hi arkitect,

    I think I have already pasted here most of the code. You can copy them and create classes to start with. I will also try to upload a fully functional application to download for others.

    ReplyDelete
  6. Hi Shamaila. I need explicit control to the click events on the headers and extend the default behavior of column sorting to include secondary and tertiary level sorting. Did you happen to deal with this situation ?

    ReplyDelete
  7. I have not dealt with header events and sorting but I believe that you can handle Grid.HeaderClick event.

    HeaderClick : GridEvent(grid, rowIndex, colIndex, event)
    Fires a header is clicked.
    grid : this
    rowIndex : row index
    colIndex : column index
    event : the dom event

    About custom sort I don't believe that it is possible in the current stage of GXT grid to implement a custom sort. I have two suggestions to implement the custom sort.

    1) Handle header click event and do a server side sort meaning bring the sorted data from your database.

    2) Extend Store and implement client side multi-level sorting. I have seen a possible solution at gxt forum I am pasting the solution here.


    It doesn't look like Ext supports this. But you can work around it. This code adds a "customSort" config setting to Ext.data.Store. This is a function that takes two arguments: the Ext.data.Records to compare. It should return 1 if the first record is greater, -1 if the second is greater, or 0 if they're equal.

    Ext.override(Ext.data.Store, {
    sortData : function(f, direction){
    direction = direction || 'ASC';
    var fn = this.fields.get(f).customSort;
    if (!fn) {
    var st = this.fields.get(f).sortType;
    var fn = function(r1, r2){
    var v1 = st(r1.data[f]), v2 = st(r2.data[f]);
    return v1 > v2 ? 1 : (v1 < v2 ? -1 : 0);
    };
    }
    this.data.sort(direction, fn);
    if(this.snapshot && this.snapshot != this.data){
    this.snapshot.sort(direction, fn);
    }
    }
    });
    To use it, add the customSort function to your field definitions. This example sorts a "street" field by state first, then city, then street itself.

    new Ext.data.JsonReader(
    {...},[
    {name:'address', mapping:'street',
    customSort:function(r1, r2) {
    var v1 = r1.data["state"], v2 = r2.data["state"];
    if (v1 !== v2) return v1 > v2 ? 1 : -1;
    v1 = r1.data["city"], v2 = r2.data["city"];
    if (v1 !== v2) return v1 > v2 ? 1 : -1;
    v1 = r1.data["street"], v2 = r2.data["street"];
    if (v1 !== v2) return v1 > v2 ? 1 : -1;
    return 0;
    }
    }]
    );

    ReplyDelete
  8. Hi Shamaila, can i have the complete source code of your implementation? I'm confused with my code because it won't show up the data to the grid.

    ReplyDelete
  9. Hi Lynard,

    All the code is present in the example. If you are not getting data in your grid please use firbug to examin what data you are getting after your a fetch call is made. If you are getting right data then look into the bindings. If you want you can send me your code.

    Thanks

    ReplyDelete
  10. Hi Guys, I have tried following the Shamaila's example, but I get no data on the grid. I have not used BeanFactory anywhere whatsoever. What could be wrong with my code?

    Here is my code
    --------------------------------------
    RpcProxy proxy = new RpcProxy() {

    @Override
    protected void load(Object loadConfig, AsyncCallback callback) {
    // TODO Auto-generated method stub
    greetingService.getCars(callback);
    }


    };
    BeanModelReader reader = new BeanModelReader();

    // loader and store
    ListLoader loader = new BaseListLoader(proxy, reader);
    ListStore store = new ListStore(loader);

    loader.load();

    // column model
    List columns = new ArrayList();
    columns.add(new ColumnConfig("car_make", "Make", 200));
    columns.add(new ColumnConfig("car_model", "Model", 100));
    columns.add(new ColumnConfig("car_year", "Year", 50));
    ColumnModel cm = new ColumnModel(columns);

    Grid grid = new Grid(store, cm);
    grid.setAutoExpandColumn("car_make");
    grid.setWidth(400);
    grid.setAutoHeight(true);
    grid.setBorders(true);

    contentPanel.setHeading("BeanModel Grid Example");
    contentPanel.setSize(400, 200);

    contentPanel.setLayout(new FitLayout());

    contentPanel.add(grid);
    ----------------------------------------
    Thanking you,
    Simon.

    ReplyDelete
  11. Hi Simon,

    Its been a while when I had used this grid. I cannot remember exactly but here are two suggestions

    1) Please see if you have to call something like grid.setAutoLoad(true)?

    2) Use firebug to see if your loaddata request is going to the server and bringing correct JSON data.

    If you are still unable to fix the issue then send me the JSON response.

    ReplyDelete
  12. Hi Shamaila, thanks for the quick response!

    Unfortunately, there is no such method as .setAutoLoad().
    I have installed firebug, and this is the response.
    ------------------------------
    //OK[2004,7,6,2,2004,5,3,2,2007,4,3,2,3,1,["java.util.ArrayList/3821976829","org.example.moi.client.Car/2809884344","Benz","GL","ML","Toyata","Carib"],0,5]
    -----------
    I see all my records from the server are shown.
    Am not yet able to display the data on the grid.
    Anything am missing?
    Thanking you,
    Simon.

    ReplyDelete
  13. Hi Simon

    Looks like your JSON is not valid. For example see my JSON that I had got from the server

    [
    {
    "id": 16,
    "name": "sites package",
    "description": "",
    "isPrivate": false,
    "rating": 3,
    "dateCreated": {
    "@class": "sql-date",
    "$": "2011-06-05"
    }
    },
    {
    "id": 17,
    "name": "russian package1",
    "description": "",
    "isPrivate": false,
    "rating": 0,
    "dateCreated": {
    "@class": "sql-date",
    "$": "2011-06-05"
    }
    }
    }
    ]

    The JSON starts and end with square brackets. GXT grid matches the grid column names with JSON element names and assign values accordingly. For example here I have id, name, description, rating etc as JSON object properties. In my grid there were columns with the same name, thus the grid find related values.

    ReplyDelete
  14. Hi Shamaila,

    I was able to get my way around it. I followed the example at https://code.google.com/p/gaedemoiuqrul/source/browse/trunk/gwtSamples/gxtShowcase/src/com/extjs/gxt/samples/client/examples/grid/BeanModelGridExample.java?r=62 . I took off time, and wrote line by line, tested their code, and until it worked.
    Thank you all for your time.
    Simon.

    ReplyDelete