This article is going to wrap up what you need to know to get started with the JSP Wrappers for Kendo UI. Let's do just a quick review.
In this final installment, I'm going to be adding in all of the necessary actions to allow users to edit, create, and delete items from my grid. This article builds heavily on the first three, so if you find yourself late to the conversation, might I suggest giving the prior entries a quick read.
The popup editor that we have now is really nice. It's got plenty of real-estate, and all of the forms controls are laid out cleanly so we don't have to fight with CSS or tables.
You might also enjoy the kitty in the upper right-hand side of that screenshot. I know I do. That's because I have Chrome profiles setup. You can set them up yourself and get your very own kitty.
However if you click the Update button, nothing happens. This is due to the fact that the grid currently has no idea what we want it to do when this button is clicked. What it's going to be looking for is a defined update transport on the data source.
... <kendo:dataSource serverPaging="true" pageSize="10"> <kendo:dataSource-transport> <kendo:dataSource-transport-read url="api/products">
</kendo:dataSource-transport-read> <kendo:dataSource-transport-update url="api/products?update" type="POST">
</kendo:dataSource-transport-update> ...
We need to stop a minute and talk about the update transport settings, because there is actually some pretty important stuff buried in there.
The transports are broken out into their own elements so that I can provide extra attributes. In the case of the update, it's the type attribute. I am going to be calling the same URL that I did for the read to do the update. In fact, I'm going to be calling that same url for create and delete eventually. How does this work?
The type attribute here specifies that when the servlet responds to this request, it should be using the doPost method as we have specified that this is a POST and not a GET (GET is the default). Additionally, any parameters that we send will go in the body of the request, not the query string. Don’t worry, Kendo UI is going to handle all of this for you. The only thing we need to do is tell the doPost what sort of action we want to do. This is why I am passing update on end of the query string. I can still read the query string in the servlet, we just can't parse parameters off if it as easily as I did in the doGet.
I realize that might be a bit confusing if you are new to HTTP requests and the way that they are made. Don't worry thought, its actually very simple. You can essentially make 4 types of requests from a browser. You can do a GET, POST, PUT, or DELETE. If you are using a framework like Spring MVC, you can use the PUT for a create and the DELETE for a delete. However, plain servlets do not provide a way to do this very easily. It's going to be better for me to stick with a POST and just determine my action from the query string.
Now clicking the button will execute a request to the server.
I just need to tell the server what to do when it receives this request.
Open the Products servlet. If you don't already have a doPost method, add one.
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // handle CRUD operations here }
Inside of this doPost method, we are going to get the variables from the body of the request and map them into a Products model object. Doing this helps us validate that the data we are getting from the client actually maps to a product object. It's highly advisable that you do server-side data validation in this method as well. We're going to implement it on the client in a just a bit, but always validate your data on both the client and the server.
I created a private method in the servlet which just maps the request parameters into a Product model object.
private models.Product parseRequest(HttpServletRequest request) { models.Product product = new models.Product(); // VALIDATION SHOULD BE DONE HERE AS WELL // parse the rest of the information off of the request product.setSupplier(new models.Supplier(Integer.parseInt(request.getParameter("Supplier[SupplierID]")), request.getParameter("Supplier[SupplierName]"))); product.setCategory(new models.Category(Integer.parseInt(request.getParameter("Category[CategoryID]")), request.getParameter("Category[Categoryname]"))); product.setUnitPrice(Float.parseFloat(request.getParameter("UnitPrice"))); product.setUnitsInStock(Integer.parseInt(request.getParameter("UnitsInStock"))); product.setDiscontinued(Boolean.parseBoolean(request.getParameter("Discontinued"))); product.setProductName(request.getParameter("ProductName")); return product; }
It's pretty easy to just read parameters from the request that Kendo UI sends using the servlet getParameter method. How do I know what these parameters are? Simply look at the request in your browser tools. The screenshot above clearly shows me what parameters I am dealing with.
You need to be very comfortable using your browser development tools when doing HTML5 development, or you will be flying blind. If you are on Windows, you can use Fiddler, which will make working with AJAX requests an absolutely sublime experience.
Of course, I also need methods in the data access layer (something that I've called "repositories"). I'm going to implement the update method. While I'm at it, I'm also going to implement create and delete methods, because I'll be needing those very soon.
// update products method public models.Product doUpdateProduct(models.Product product) throws Exception { // create a prepared statement object PreparedStatement stmt = null; try { // build the sql needed for the update String sql = "UPDATE Products SET SupplierID = ?, CategoryID = ?, ProductName = ?, " + "UnitPrice = ?, UnitsInStock = ?, Discontinued = ? " + "WHERE ProductID = ?"; // prepare the statement for safe execution stmt = super.connection.prepareStatement(sql); // map the parameters into the query stmt.setInt(1, product.getSupplier().getSupplierID()); stmt.setInt(2, product.getCategory().getCategoryID()); stmt.setString(3, product.getProductName()); stmt.setFloat(4, product.getUnitPrice()); stmt.setInt(5, product.getUnitsInStock()); stmt.setBoolean(6, product.getDiscontinued()); stmt.setInt(7, product.getProductID()); // execute the update stmt.executeUpdate(); } catch (Exception e) { e.printStackTrace(); throw new Exception(e.getMessage()); } finally { // close all necessary connection related instances stmt.close(); } return product; }
The create method has a bit of a twist to it in that what I need back is the id of the item that was just generated. In order to get that, I have to first tell the prepared statement that I'm going to be wanting it, then I have to ask for it after the statement is executed.
// product create method public int doCreateProduct(models.Product product) throws Exception { // set a default id value int id = 0; //create prepared statement and result set objects PreparedStatement stmt = null; ResultSet rs = null; try { // build the sql string String sql = "INSERT INTO Products (SupplierID, CategoryID, " + "ProductName, UnitPrice, UnitsInStock, Discontinued ) " + "VALUES (?, ?, ?, ?, ?, ?)"; // prepare the statement for safe execution, specifying that we // want the auto-generated id from the database returned. stmt = super.connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); // map the product object to the sql stmt.setInt(1, product.getSupplier().getSupplierID()); stmt.setInt(2, product.getCategory().getCategoryID()); stmt.setString(3, product.getProductName()); stmt.setFloat(4, product.getUnitPrice()); stmt.setInt(5, product.getUnitsInStock()); stmt.setBoolean(6, product.getDiscontinued()); // execute the create statement int rows = stmt.executeUpdate(); // if no rows were returned, it failed if (rows == 0) { throw new SQLException("Unable to create product"); } // get the generated key for this item rs = stmt.getGeneratedKeys(); // pull the key off the result set if (rs.next()) { id = rs.getInt(1); } else { throw new SQLException("Unable to create product. No auto-genereated key obtained"); } } finally { // close all connection related instances stmt.close(); rs.close(); } // return the id that was or wasn't created return id; }
The delete is nice and simple. There is nothing to return from this method. It either worked or it didn't.
// the delete method public void doDeleteProduct(int productId) throws SQLException { // create a prepared statement object PreparedStatement stmt = null; try { // build the simple sql statement String sql = "DELETE FROM Products WHERE ProductID = ?"; // prepare the statement for safe execution stmt = super.connection.prepareStatement(sql); // set the id of the product to delete stmt.setInt(1, productId); // execute the delete stmt.executeUpdate(); } finally { // close all connection related instances stmt.close(); } }
These methods are verbose and there is no doubt about that. Building an entire application this way might result is some carpel tunnel. That's why developers usually choose a framework which handles so much of this manual labor for them. To avoid getting bogged down in the specifics of some specific framework that might not be applicable to everyone, I'm doing things the hard way.
I've got all the CRUD statements I need in the data access layer, and that's great because calling them from the servlet is nice and easy. At this time I just have to handle the edit.
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { // set the content type we are sending back as JSON response.setContentType("application/json"); // get the product id off of the request. only a delete or // update will contain the product id int productId = request.getParameter("ProductID") == "" ? 0 : Integer.parseInt(request.getParameter("ProductID")); // get the type of operation off the query string String type = request.getQueryString().trim(); // parse the rest of the request into a product object models.Product product = parseRequest(request); // if the query string specified an update if (type.equals("update")) { // add the product id onto the model and update the product product.setProductID(productId); product = _repository.doUpdateProduct(product); response.getWriter().print(_gson.toJson(product)); } } catch (Exception e) { e.printStackTrace(); // if something went wrong, send back a server error response.sendError(500); } }
Edits in the grid will now be persisted in the database. The editor sends the product and it's updated properties over to the database. It only does this if a value is actually changed. If you bring up the edit window and don't change anything, it's smart enough to know that and not issue an unnecessary update to the server.
There are some fields that I want to validate. I don't want people to be able to completely null out the product name. Right now, they can do that. Kendo UI will do this for me, but I first need to define my constraints in the model of the grid datasource. Once I specify the validation rules in the model, Kendo UI will not let the CRUD actions happen unless these validation rules are met.
.... <kendo:dataSource-schema-model id="ProductID"> <kendo:dataSource-schema-model-fields> <kendo:dataSource-schema-model-field name="ProductName"> <kendo:dataSource-schema-model-field-validation required="true"/> </kendo:dataSource-schema-model-field> <kendo:dataSource-schema-model-field name="Supplier"></kendo:dataSource-schema-model-field> <kendo:dataSource-schema-model-field name="Category"></kendo:dataSource-schema-model-field> <kendo:dataSource-schema-model-field name="UnitPrice" type="number"></kendo:dataSource-schema-model-field> <kendo:dataSource-schema-model-field name="UnitsInStock" type="number"></kendo:dataSource-schema-model-field> <kendo:dataSource-schema-model-field name="Discontinued" type="boolean"></kendo:dataSource-schema-model-field> </kendo:dataSource-schema-model-fields> </kendo:dataSource-schema-model> ....
A Kendo UI model is very similar to a server-side model class. It wants to know which field is the id, or primary key. Then I specify each of the fields in the model, and which type they are if they are something other than a standard string. For the ProductName, I specify that this field is required.
Now If I try and submit a row in edit mode with a blanked out product name, it will give me a message which kindly informs me that this is not allowed.
Kendo UI provides very granular control over validation. I encourage you to read through the documentation on the validation for models. It's a tutorial in and of itself, but they are rather easy to implement and customize once you know how a basic validation rule works in a model. Now you do!
I need to add in definitions for the transport on the datasource on how to handle a create and a delete. Kendo UI refers to a delete as a destroy because delete is a reserved word in JavaScript.
.... <kendo:dataSource-transport> <kendo:dataSource-transport-read url="api/products"></kendo:dataSource-transport-read> <kendo:dataSource-transport-create url="api/products?create" type="POST"></kendo:dataSource-transport-create> <kendo:dataSource-transport-update url="api/products?update" type="POST"></kendo:dataSource-transport-update> <kendo:dataSource-transport-destroy url="api/products?delete" type="POST"></kendo:dataSource-transport-destroy> </kendo:dataSource-transport> ....
Let's round this out by adding in the necessary UI for the delete and create actions.
The delete button is already there next to the edit button. Nothing else is required of me at this point for a delete.
For creating a product, I want to add a toolbar at the top of the grid with a convenient "create" button which opens the popup window.
..... <kendo:grid-editable mode="popup"/> <kendo:grid-toolbar> <kendo:grid-toolbarItem name="create" /> </kendo:grid-toolbar> <kendo:dataSource serverPaging="true" pageSize="10"> ......
That small configuration change to the markup will do the trick. Kendo UI will take care of the rest.
I do need to tell the model what values it should use for the Category and Supplier, because as you will recall, these are not simply fields but actually objects consisting of both a name and an id. I can use Java for that by opening a tag and creating a new supplier and category model object for the default value.
<kendo:dataSource-schema-model id="ProductID"> <kendo:dataSource-schema-model-fields> <kendo:dataSource-schema-model-field name="ProductName"> <kendo:dataSource-schema-model-field-validation required="true"/> </kendo:dataSource-schema-model-field> <kendo:dataSource-schema-model-field name="Supplier" defaultValue="<%= new Supplier() %>"></kendo:dataSource-schema-model-field> <kendo:dataSource-schema-model-field name="Category" defaultValue="<%= new Category() %>"></kendo:dataSource-schema-model-field> <kendo:dataSource-schema-model-field name="UnitPrice" type="number"></kendo:dataSource-schema-model-field> <kendo:dataSource-schema-model-field name="UnitsInStock" type="number"></kendo:dataSource-schema-model-field> <kendo:dataSource-schema-model-field name="Discontinued" type="boolean"></kendo:dataSource-schema-model-field> </kendo:dataSource-schema-model-fields> </kendo:dataSource-schema-model>
I've already implemented the create and delete methods in the data access layer, so all that's left to do is call them in the event that I request one of those actions in the query string. The completed doPost method looks like this:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { // set the content type we are sending back as JSON response.setContentType("application/json"); int productId = request.getParameter("ProductID") == "" ? 0 : Integer.parseInt(request.getParameter("ProductID")); // get the type of operation String type = request.getQueryString().trim(); // parse the rest of the request into an employee object models.Product product = parseRequest(request); if (type.equals("update")) { // add the product id onto the model and update the product product.setProductID(productId); product = _repository.doUpdateProduct(product); response.getWriter().print(_gson.toJson(product)); } if (type.equals("create")) { //create the product productId = _repository.doCreateProduct(product); response.getWriter().print(_gson.toJson(productId)); } if (type.equals("delete")) { // delete the product System.out.println(type); _repository.doDeleteProduct(productId); } } catch (Exception e) { e.printStackTrace(); response.sendError(500); } }
Grab the completed project for this final tutorial from the GitHub repo. It contains everything that has been covered so far.
That's all there is to it. You have a complete editing interface that you can now begin to customize to your hearts content. If you made it through all 4 of these tutorials, you have already examined some of the most complex scenarios you will likely encounter, and you are ready to go forth and unleash your inner HTML5 creativity.
Kendo UI doesn't build your application for you. Instead, it simply helps you to realize your full potential as a web and mobile developer by taking care of the details that can take the fun out of programming. This is what allows you to concentrate on bringing your ideas and requirements to life as only you can.
Be sure to sign up for the launch webinar for the JSP Wrappers and MANY other new things in Kendo UI. It's just around the corner and you don't want to miss it. Besides, you stand a chance at winning some pretty awesome prizes and we love to give them out!
Burke Holland is a web developer living in Nashville, TN and was the Director of Developer Relations at Progress. He enjoys working with and meeting developers who are building mobile apps with jQuery / HTML5 and loves to hack on social API's. Burke worked for Progress as a Developer Advocate focusing on Kendo UI.