PDA

View Full Version : DWRDataModel ?



sjivan
3 Dec 2006, 4:16 PM
I was wondering if anyone was loading data in grids using DWR? A DWRDataModel would probably be required. Is this on the cards?

I'd also like to hear how others are using DWR with YUI-ext..

Thanks,
Sanjiv

jack.slocum
3 Dec 2006, 5:30 PM
"Animal" is using it with DWR. I'm sure he will chime in soon.

Animal
4 Dec 2006, 12:46 AM
Yes, I'm using DWR.

The way I do it, is, I have an interface which provides all the data that ColumnModels and DataModels need.



public interface ListProvider
{
public String getId();
public void close();
public ListColumn[] getColumnModel();
public long getRowCount();
public boolean setPosition(int row);
public boolean hasMore();
public Object[] getNext();
public Object[] get(int rowIndex);
public Object[] getBlock(int startRow, int blockSize);
public Object[] refresh(int startRow, int blockSize);
public Object[] refresh(String paramString, int startRow, int blockSize);
public Object[] orderBy(int columnIndex, String order, int startRow, int blockSize);
public void moveColumn(int from, int to);
public void hideColumn(int col);
public void setColumnWidth(int col, int width);
}


As you can see, everything needed by the Model objects is provided there.

I have a ListHelper class which is exported to DWR which provides a createListProvider call taking a class name as a parameter, and a uurlencoded string of parameters which is passed to the constructor. We're using Hibernate here, so the implementation of ListProvider is quite easy - just cobble up a bunch of HQL.

The ListHelper class exports all the methods of ListProvider taking the created ListProvider's ID as the 1st parameter.

ListHelper:



public class ListHelper
{
public static String create(String className, String id, String uurlEncodedParams) throws Exception
{
WebContext w = WebContextFactory.get();
HttpServletRequest request = w.getHttpServletRequest();
HttpSession session = w.getSession();
Locale locale = request.getLocale();
Principal principal = request.getUserPrincipal();
Class c = Class.forName(className);
Constructor con = c.getConstructor(String.class, String.class, Locale.class, Principal.class, HttpSession.class);
String result = null;
try
{
// Create an instance of the requested ListProvider implementation. This stores itself in the HttpSession
// under the passed ID (Same as the ID of the yui-ext Grid). All calls to it are routed through the methods below.
result = ((ListProvider)con.newInstance(id, uurlEncodedParams, locale, principal, session)).getId();
}
catch (InvocationTargetException e)
{
throw (Exception)e.getCause();
}
// Return the ID
return result;
}


// All the methods of ListProvider, taking the ID of the Provider as the 1st param:

public static ListColumn[] getColumnModel(String ListProviderId) throws Exception....

public static long getRowCount(String ListProviderId) throws Exception....


The Column Model is returned as an array of ListColumn objects which exactly match the object array which initializes DefaultColumnModel, so that gets passed right in.

The ListProvider implementation is stashed in the session under the passed ID (which is this id of the grid, so that everything matches up and debugging is easier), and DWR calls are routed through the ListHelper class which exports all the ListProvider methods pefixed by an ID, so that you can have mulitple grid, and communicate with the ListProvider that;s appropriate.

dwr.xml:



<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dwr PUBLIC "-//GetAhead Limited//DTD Direct Web Remoting 2.0//EN" "http://www.getahead.ltd.uk/dwr/dwr20.dtd">
<dwr>
<allow>
<create creator="new" javascript="ListHelper">
<param name="class" value="com.aspicio.util.ListHelper"/>
</create>


<convert converter="bean" match="com.aspicio.util.SearchResult"/>


<convert converter="bean" match="com.aspicio.util.ListColumn">

<param name="exclude" value="type"/>
</convert>
</allow>
</dwr>


[ListColumn]
package com.aspicio.util;

import javax.persistence.Entity;

import com.aspicio.beanInfo.Property;

public class ListColumn
{
private String header;
private int width;
private boolean hidden;
private int dataIndex;
private int visibleIndex;
private Property type;

public void setHeader(String header)
{
this.header = header;
}
public String getHeader()
{
return header;
}
public void setWidth(int width)
{
this.width = width;
}
public int getWidth()
{
return width;
}
public void setHidden(boolean hidden)
{
this.hidden = hidden;
}
public boolean isHidden()
{
return hidden;
}
public void setDataIndex(int dataIndex)
{
this.dataIndex = dataIndex;
}
public int getDataIndex()
{
return dataIndex;
}
public void setVisibleIndex(int visibleIndex)
{
this.visibleIndex = visibleIndex;
}
public int getVisibleIndex()
{
return visibleIndex;
}
public void setType(Property type)
{
this.type = type;
}
public Property getType()
{
return type;
}
public String getName()
{
return type.getFieldName();
}
public String getEntityName()
{
Class<?> c = type.getType();
return c.isAnnotationPresent(Entity.class) ? c.getSimpleName() : "";
}
public int getInputType()
{
return type.getInputType();
}
public String[] getSelectValues()
{
return type.getSelectValues();
}
}[/code]


It's so easy to pull data in with DWR, in my DataModel, loadPage is just a couple of statements:



YAHOO.ext.grid.AspicioDataModel.prototype.loadPage = function(pageNum, callback, keepExisting)
{
this.fireEvent('beforeload');
ListHelper.getBlock(this.listerId, (pageNum - 1) * this.pageSize, this.pageSize,
this.loadData.createDelegate(this, [pageNum, callback, keepExisting], 1));
};


and loadData is simple (apart from my grid has to have the ability to have checkboxes on each row to allow multiple entity operations):



YAHOO.ext.grid.AspicioDataModel.prototype.loadData = function(rowData, pageNum, callback, keepExisting)
{
var doCallback = (typeof callback == "function");
try
{
if (keepExisting !== true)
{
this.removeAll();
}

// Add "Selected" column
if (this.checkboxes)
{
for (var i = 0; i < rowData.length; i++)
rowData[i].unshift(false);
}
this.addRows(rowData);
this.loadedPage = pageNum;
}
catch (e)
{
YAHOO.widget.Logger.log(e.description + " at line " + e.lineNumber + " in " + e.fileName);
this.fireLoadException(e, null);
if(doCallback)
{
callback(this, false);
}
return;
}

// Call external code outside our try/catch. IT should handle its errors, not us.
this.fireLoadEvent();
if (doCallback)
{
callback(this, true);
}
};

sjivan
4 Dec 2006, 9:42 AM
Thanks a bunch. I get what you're doing.

Just one minor question : Can you use the DWR exported ListHelper to be a factory for ListProvider implementations instead of having all the methods of ListProvider on ListHelper as well.

ie


public class ListHelper {
public static ListProvider create(String className, String id, String uurlEncodedParams) {..}
}

and have DWR converters for ListProvider implementations or have a base Javabean class that implements ListProvider and just declare a converter for that base class (all implementations will satisfy the is-a relationship with the base bean class).

I have all the backend code to generically sort / page / filter a collection of objects any class based on bean properties. For eg : sort or filter a Person collection based on 'company.name' . I'm using Hibernate too. I'll give your approach a shot and report back. Looks promising..

Thanks,
Sanjiv

Animal
5 Dec 2006, 12:47 AM
Thanks a bunch. I get what you're doing.

Just one minor question : Can you use the DWR exported ListHelper to be a factory for ListProvider implementations instead of having all the methods of ListProvider on ListHelper as well.

ie


public class ListHelper {
public static ListProvider create(String className, String id, String uurlEncodedParams) {..}
}

and have DWR converters for ListProvider implementations or have a base Javabean class that implements ListProvider and just declare a converter for that base class (all implementations will satisfy the is-a relationship with the base bean class).

I have all the backend code to generically sort / page / filter a collection of objects any class based on bean properties. For eg : sort or filter a Person collection based on 'company.name' . I'm using Hibernate too. I'll give your approach a shot and report back. Looks promising..

Thanks,
Sanjiv

The problem is that DWR works on a stateless basis. It creates a new instance of the bean you are requesting methods on very call (Unless they are static methods, in which case, it calls them statically which is why all my wrapper methods are static), so to communicate with a stored instance of a particular object, you have to use a wrapper passing in the ID which delegates to the correct instance out of the HttpSession.

That is slightly annoying about DWR, that you can't set up a stateful bean. My implementation of ListProvider starts a timer on every request which shuts itself down and cleans up all resources after a period of inactivity. The next request cancels it, and re-starts. So if you leave a page with a ListManager which has a ListProvider back on the server, it will go away soon. I trap in the wrapper the condition of not being able to find the ID in the session, and throw a meaningful Exception, like



if (e == null)
throw new Exception("Your list has expired.\nPlease re-issue the query");


Which DWR just alerts to the user.

Hibernate is great for being able to create queries isn't it? My Grid column names are Hibernate property names. Sorting is a piece of cake. Generation of an HQL query from a set of params is a piece of cake. You can get the metadata about an entity using



ClassMetadata classMetadata = sessionFactory.getClassMetadata(entityType);


So you know what properties they are, what they link to etc... We have added our own extra annotations onto our entity classes and have an API which pulls Hibernate metadata, and our own extra annotated metadata together to provide all the information about an entity at runtime that you need to operate on entities in a completely generic way.

We annotate validation types, which field is the descriptive field (so that when you have a link to an entity, you can pull the description out), which field is the default key field to use in the UI as a key, etc...

In my filter pages I have params of the form



<input name="entityType" value="Country" type="hidden">
<input name="code"/>
<input name="language.name"/> <!-- language is an @ManyToOne property linking to the Language entity, so use the HQL way of accessing it>
etc...


So I can just use Connect.setForm to wrap those params up, and generate an HQL query. I go through the HTTP parameter Map looking at non-blank parameters. If the input name matches a property name, I add an ">=" test. (I also check form HTTP params of the form "from" + propertyName and "to" + propertyName to create "between" tests)

So, in my log file, if I just enter something into the "code" field, I see:



08:22:23,921 INFO [DefaultRemoter] Exec: ListHelper.create()
08:22:23,937 INFO [aspicio] Creating FilterListManager("CountryListManager1165306916109Grid81","entityType=Country&filter=_this.code%20%3E%3D%20'CN'&orderBy=order%20by%20_this.code%20asc")
08:22:23,953 INFO [aspicio] FilterListManager CountryListManager1165306916109Grid81 stored in session B271CACD5BEED9EED386A3728A77ABBB


and the created HQL query:



08:22:23,984 INFO [aspicio] FilterListManager list query: select _this.id,
_this.code,
_this.name,
_this.language.name,
_this.market.name,
_this.economicGroup.name,
_this.currency.name,
_this.timeZone.name,
_this.internetCode,
cast(_this.unitsDistance as integer),
cast(_this.unitsMeasure as integer),
cast(_this.isoCountry as integer),
_this.dialCode
from Country _this
left outer join _this.language
left outer join _this.market
left outer join _this.economicGroup
left outer join _this.currency
left outer join _this.timeZone
where _this.code >= 'CN' order by _this.code asc


The casts are Java Enumeration types, and the ColumnModel created contains info about the string representation, eg for unitsDistance, it would create ["kilometres", "miles"].

Using this setup, creating an interactive "key" type input on a page is simple. I have a JSP tag which uses our metadata API to create all the complex markup and script just from the entity and it's property name, so in my JSP, I can have:



<aspicio:keyfield entity="<%=entity%>" property="market" descid="marketDesc"/>


And I'll get a field, which, on change, will call a DWR method, which will first check the value against whichever DB column is annotated as the UI primary key, so if you typed "EU", it would find "European Economic Union". If no match, it checks against whichever column is annotated as the descriptive field, so if you typed "Eur", it would also find "European Economic Union". If you typed a value which had multiple matches (or clicked the "Search" button), it would use your value as the start value in a ">=" match on the key column, ordering by the key column, and create a ListManager yui-ext object which uses a DWR grid to allow you to see them all. An onDblClick handler would be passed to populate the input field with the key of the row you clicked on.

sjivan
5 Dec 2006, 6:18 AM
How are you getting the Grid sort to set the appropriate sortBy/orderBy request param to be the entity property name (as opposed to column index)?

I've described the problem here http://www.yui-ext.com/forum/viewtopic.php?t=1254

Thanks,
Sanjiv

Animal
5 Dec 2006, 7:33 AM
I'm using the column index. See the orderBy method in the ListProvider interface.