PDA

View Full Version : Example: TreePanel with HTTPProxy, JSONReader, async, ASP.NET [ExtGWT 2.0-m3]



HerrB
21 Jun 2009, 4:32 AM
It has cost me some time to understand, how a ModelData class, JSONReader, HTTPProxy, ASP.NET 3.5 webservices and folder/leaf declaration have to be combined to work. The examples are great, but I needed a little bit more explanations. I will describe an example, which may answer some questions I've stumbled upon...

Note: I'm still working on a complete solution. You shouldn't follow the remote part, as using "GET" to get the data needed from an ASP.NET webservice is a BAD approach. I will post a complete solution, when I'm finished.

Let's start:
TreePanel uses a TreeStore to store the items shown in the tree. An item is called a "model" in ExtJS GWT. You have to specify which properties are available for each model (e.g. a name, an id).

Example:
The employees within a department could be described as a "Person" item. Each employee has a lastname, a firstname and an id. The PersonModel will get the properties "lastname", "firstname" and "itemid" (at least).

PersonModel.java:

import com.extjs.gxt.ui.client.data.BaseModelData;
import com.extjs.gxt.ui.client.data.DataField;
import com.extjs.gxt.ui.client.data.ModelType;

public class PersonModel extends BaseModelData {

//...

public PersonModel() {
// Initialize all properties -> avoid "null" exceptions
setItemID("0");
setLastName("");
setFirstName("");
setHasChildren(false);
}

public PersonModel(String strItemID, String strLastName, String strFirstName) {
setItemID(strItemID);
setLastName(strLastName);
setFirstName(strFirstName);
}

public void setItemID(String strItemID) {
set("itemid", strItemID);
}

public void setLastName(String strLastName) {
set("lastname", strLastName);

updateFullName();
}

public void setFirstName(String strFirstName) {
set("firstname", strFirstName);

updateFullName();
}

public void updateFullName() {
if (get("lastname") != "" && get("firstname") != "") {
set("fullname", get("lastname") + ", " + get("firstname"));
} else if (get("lastname") != "") {
set("fullname", get("lastname"));
} else {
set("fullname", "");
}
}

public void setHasChildren(boolean blnHasChildren) {
set("hasChildren", blnHasChildren);
}

public String getItemID() {
return get("itemid");
}

public String getLastName() {
return get("lastname");
}

public String getFirstName() {
return get("firstname");
}

public String getFullName() {
return get("fullname");
}

public boolean getHasChildren() {
return (Boolean)get("hasChildren");
}

static public ModelType getModelType() {
ModelType objType = new ModelType();

// Use "d" as root for ASP.NET 3.5 webservice
objType.setRoot("d");

DataField objField;

objField = new DataField("itemid");
objField.setMap("strId");
objType.addField(objField);

objField = new DataField("lastname");
objField.setMap("strLastName");
objType.addField(objField);

objField = new DataField("firstname");
objField.setMap("strFirstName");
objType.addField(objField);

objField = new DataField("hasChildren");
objField.setMap("blnHasChildren");
objField.setType(Boolean.class);
objType.addField(objField);

return objType;
}

// Complete items are accepted as equal, if they have the same item id
// Override the equals method of the BaseModelData implentation to compare
// just the item id
@Override
public boolean equals(Object objObject) {
if (objObject != null && objObject instanceof PersonModel) {
PersonModel objPerson = (PersonModel) objObject;

return getItemID().equals(objPerson.getItemID());
}
return super.equals(objObject);
}
}


If you use a remote data reader (e.g. XML, JSON), you have to specify a "ModelType" which primarely describes, how a property is mapped (assigned) to the data items you receive from the server. This mapping has been specified in the getModelType method.

Example:
Here, the value for "itemid" of the PersonModel item will be received as "strId" from the server. The value for "hasChildren" will be received as "blnHasChildren".

Best regards,

HerrB

HerrB
21 Jun 2009, 5:11 AM
// Import other needed class here, e.g. your PersonModel class
import com.extjs.gxt.ui.client.data.BaseTreeLoader;
import com.extjs.gxt.ui.client.data.JsonReader;
import com.extjs.gxt.ui.client.data.HttpProxy;
//import com.extjs.gxt.ui.client.data.MemoryProxy;
import com.extjs.gxt.ui.client.data.ModelData;
import com.extjs.gxt.ui.client.data.TreeLoader;
import com.extjs.gxt.ui.client.store.TreeStore;
import com.extjs.gxt.ui.client.widget.ContentPanel;
import com.extjs.gxt.ui.client.widget.treepanel.TreePanel;
import com.google.gwt.http.client.RequestBuilder;

public void onModuleLoad() {

// See getModelType, which string has been specified as "root" element.
// Here it is - due to an ASP.NET 3.5 webservice - the term "d"
// Note, that "strId", "blnHasChildren" and so on are not directly properties of
// our PersonModel, but they will become properties using the mappings specified in
// getModelType (see above).

// MemoryProxy not used, as HTTPProxy is now available
// MemoryProxy objProxyTest = new MemoryProxy("{\"d\":[{\"strId\":\"1\",\"strLastName\":\"Test1\",\"strFirstName\":\"Test2\",\"blnHasChildren\":true},{\"strId\":\"2\",\"strLastName\":\"Test3\",\"strFirstName\":\"Test4\"}]}");

RequestBuilder objBuilder = new RequestBuilder(RequestBuilder.GET,
"<your url to the server>/ajax_persons.asmx/handlePersons");
// Specify request content type as application/json to get JSON back from ASP.NET webservice
objBuilder.setHeader("content-type", "application/json");

HttpProxy<List<PersonModel>> objProxy = new HttpProxy<List<PersonModel>>(objBuilder);

// The JSONReader class in Ext GWT 2.0 is almost the same as
// the "JSONTreeReader" class found in the forum. But this version is
// working in the new version and provides an easy way specify you own
// model class
JsonReader<PersonModel> objReader = new JsonReader<PersonModel>(PersonModel.getModelType()) {
// You have to override the newModelInstance method
// to generate "your" objects (e.g. PersonModel objects).
// Otherwise, only "BaseModelData" objects will be generated -
// which (obviously ... ;-)) doesn't contain your methods,
// e.g. here my hasChildren method...
@Override
protected ModelData newModelInstance() {
return new PersonModel();
}

// See below for an explanation, why the createReturnData
// has been overridden, also.
// Note, that it has to be "List<ModelData>", not "List<PersonModel>"
// as parameter.
@Override
protected Object createReturnData(Object objLoadConfig, List<ModelData> lstRecords, int intTotalCount) {
if (lstRecords.size() > 0) {
for (ModelData objItem: lstRecords) {
// We do get a "ModelData" object, but
// we know, it is a "PersonModel" object.
// Cast it to the real object and call
// the special method
((PersonModel)objItem).updateFullName();
}
}

return lstRecords;
}
};

// Override the hasChildren method of the BaseTreeLoader class to
// distingiush between "folder" and "leaf". This means, that we delivered
// the information, that there are children, as we have generated the parent
// object - in this example it is part of the JSON data received from the server
TreeLoader<PersonModel> objLoader = new BaseTreeLoader<PersonModel>(objProxyTest, objReader) {
@Override
public boolean hasChildren(PersonModel objParent) {
return objParent.getHasChildren();
}
};

// Attach loader to store
TreeStore<PersonModel> objStore = new TreeStore<PersonModel>(objLoader);

// Generate a tree using the data from the store
TreePanel<PersonModel> objTree = new TreePanel<PersonModel>(objStore);

// Specify, which of the available properties (here: itemid, lastname,
// firstname or fullname) is displayed on the tree
objTree.setDisplayProperty("fullname");

// Generate a viewport ...
Viewport objViewport = new Viewport();
objViewport.setLayout(new BorderLayout());

// ... and a nice content panel
ContentPanel objPanel = new ContentPanel();

// ... add the tree to the content panel
objPanel.add(objTree);

// ... add the content panel to the viewport
objViewPort.add(objPanel);

// ... and add the viewport as your body
RootPanel.get().add(objViewport);
}
(See your main java file for the method onModuleLoad.)

Explanation for the overridden createReturnData method and the method "updateFullName" in the PersonModel class:
Showing either lastname or firstname in the tree doesn't seem to be a good idea, but the fields may be needed for later update on the store. As we have them, we should be able to combine them to get a "fullname" used for the tree.

As the JSONReader stores the values received by using the "set" method ("object.set(<fieldname>, value);") the "setLastName", "setFirstName" and "updateFullName" methods of the PersonModel class are not used. So, after the JSONReader has finished generating the objects, the "updateFullName" method is called for each object, before the list of objects is returned to the store -> in createReturnData.

Note, that the first item is specified as "folder" ("children:true"), the other is just a leaf. If you click on the first item, the next level is loaded and generated.

Regards,

HerrB

HerrB
22 Jun 2009, 5:41 AM
Using a HTTPProxy and a ASP.NET 3.5 webservice is a story for it's own and I'm not sure, if it still was a good idea. But anyway it's working know and I'd like to share the experience. As always: If there is an (ASP.NET-)crack out there, help is appreciated.

The code above has been updated to reflect the changes needed for HTTPProxy and ASP.NET webservice. In particular:


Webservices in ASP.NET 3.5 always use a "d" as JSON-root element as a security precaution, so the root entry in getModelType has been changed from "root" to "d".
Webservices in ASP.NET 3.5 return JSON only, if the request is using the content-type "application/json". You will receive XML data, if you just open the URL within your webbrowser. Please note, that you have to add

<webServices>
<protocols>
<add name="HttpGet"/>
<add name="HttpPost"/>
</protocols>
</webServices>
to the <system.web> area of the web.config file (ASP.NET) to even be able to open the URL directly within your browser. Otherwise you will receive a "Request format is unrecognized for URL ..." error page, if error reporting is turned on (add <customErrors mode="Off"/> to <system.web> area in the web.config file).
The webservice uses the build-in-JSON serialization functionality of ASP.NET. This means, that public properties of an object are the elements within JSON. So, the mapping in getModelType has been updated to map the used property names. Note, that on a click you will get the internal TreePanel store object parameters back (see next).
If you click on an expandable tree folder (hasChildren = true), a request is sent to the webserver. Unfortunately, the ASP.NET service expects to get a JSON object, not a query string-like looking POST string and you will receive an "Invalid JSON primitive" error message. To get around this problem you may alter the HTTPProxy load method as described here: http://extjs.com/forum/showthread.php?p=297714#post297714 (haven't tried) or you have to change the request mode to GET instead of POST (has been done here).

Note, that the request will contain the parameters as used in the store, not as described in the map.

Example:
The id of an element is stored as "itemid" in the store. The initial request for the TreePanel (without further parameters/data) will return the id as "strId" in JSON and gets mapped to "itemid". If you click on the element (and it is expandable), the server receives ...?itemid=1&... as request string.

Especially, this doesn't sound great and any idea to get a better design is heartly welcomed.


Code for ajax_persons.asmx:

<%@ WebService Language="VB" CodeBehind="~/App_Code/ajax_persons.vb" Class="claAjaxPersons" %>

Code for ajax_persons.vb (stored in the App_Code folder with the ASP.NET server folder!):

Imports System.Web
Imports System.Web.Services
Imports System.Web.Services.Protocols
Imports System.Web.Script.Services
Imports System.ServiceModel.Web
Imports System.Collections.Generic

<System.Web.Script.Services.ScriptService()> _
<WebService(Namespace:="Something")> _
<WebServiceBinding(ConformsTo:=WsiProfiles.None)> _
Public Class claAjaxPersons
Inherits System.Web.Services.WebService

' Set ResponseFormat.JSON to get JSON, not XML and the UseHttpGet to true
' to accept to be called using GET request mode
' The JSON/XML serializer is only able to serialize List(of Object), e.g. an ArrayList
' won't work. Maybe HashMap would work, also.
<WebMethod()> _
<ScriptMethod(ResponseFormat:=ResponseFormat.Json, UseHttpGet:=True)> _
Public Function handlePersons() As List(Of Person)
Dim lstData As New List(Of Person)
Dim objPerson As Person

If (HttpContext.Current.Request.QueryString.Count > 0) Then
' There is parameter data available, use it
objPerson = New Person
objPerson.strId = "3"
objPerson.strLastName = "I'm the child of " & HttpContext.Current.Request.QueryString.Get("itemid")
objPerson.strFirstName = ""
objPerson.blnHasChildren = False

lstData.Add(objPerson)
Else
objPerson = New Person
objPerson.strId = "1"
objPerson.strLastName = "Test1"
objPerson.strFirstName = "Test2"
objPerson.blnHasChildren = True

lstData.Add(objPerson)

objPerson = New Person
objPerson.strId = "2"
objPerson.strLastName = "Test3"
objPerson.strFirstName = "Test4"
objPerson.blnHasChildren = False

lstData.Add(objPerson)
End If

Return lstData
End Function
End Class

' Person class has been added here for convenience
Public Class Person
Public strId As String
Public strLastName As String
Public strFirstName As String
Public blnHasChildren As Boolean
End Class


Please note, that the example creates a tree with "only" three elements, one expandable.

Regards,

HerrB