"Controller" Classes Have Been Helping Me Greatly
Wed Dec 26 16:54:16 EST 2012
- "Controller" Classes Have Been Helping Me Greatly
- More On "Controller" Classes
I mentioned a while ago that I've been using "controller"-type classes paired with specific XPages to make my code cleaner. They're not really controllers since they don't actually handle any server direction or page loading, but they do still hook into page events in a way somewhat similar to Rails controller classes. The basic idea is that each XPage gets an object to "back" it - I tie page events like beforePageLoad
and afterRenderResponse
to methods on the class that implements a standard interface:
package frostillicus.controller; import java.io.Serializable; import javax.faces.event.PhaseEvent; public interface XPageController extends Serializable { public void beforePageLoad() throws Exception; public void afterPageLoad() throws Exception; public void afterRestoreView(PhaseEvent event) throws Exception; public void beforeRenderResponse(PhaseEvent event) throws Exception; public void afterRenderResponse(PhaseEvent event) throws Exception; }
I have a basic stub class to implement that as well as an abstract class for "document-based" pages:
package frostillicus.controller; import iksg.JSFUtil; import javax.faces.context.FacesContext; import com.ibm.xsp.extlib.util.ExtLibUtil; import com.ibm.xsp.model.domino.wrapped.DominoDocument; public class BasicDocumentController extends BasicXPageController implements DocumentController { private static final long serialVersionUID = 1L; public void queryNewDocument() throws Exception { } public void postNewDocument() throws Exception { } public void queryOpenDocument() throws Exception { } public void postOpenDocument() throws Exception { } public void querySaveDocument() throws Exception { } public void postSaveDocument() throws Exception { } public String save() throws Exception { DominoDocument doc = this.getDoc(); boolean isNewNote = doc.isNewNote(); if(doc.save()) { JSFUtil.addMessage("confirmation", doc.getValue("Form") + " " + (isNewNote ? "created" : "updated") + " successfully."); return "xsp-success"; } else { JSFUtil.addMessage("error", "Save failed"); return "xsp-failure"; } } public String cancel() throws Exception { return "xsp-cancel"; } public String delete() throws Exception { DominoDocument doc = this.getDoc(); String formName = (String)doc.getValue("Form"); doc.getDocument(true).remove(true); JSFUtil.addMessage("confirmation", formName + " deleted."); return "xsp-success"; } public String getDocumentId() { try { return this.getDoc().getDocument().getUniversalID(); } catch(Exception e) { return ""; } } public boolean isEditable() { return this.getDoc().isEditable(); } protected DominoDocument getDoc() { return (DominoDocument)ExtLibUtil.resolveVariable(FacesContext.getCurrentInstance(), "doc"); } }
I took it all one step further in the direction "convention over configuration" as well: I created a ViewHandler
that looks for a class in the "controller" package with the same name as the current page's Java class (e.g. "/Some_Page.xsp" → "controller.Some_Page") - if it finds one, it instantiates it; otherwise, it uses the basic stub implementation. Once it has the class created, it plunks it into the viewScope
and creates some MethodBinding
s to tie beforeRenderResponse
, afterRenderResponse
, and afterRestoreView
to the object without having to have that code in the XPage (it proved necessary to still include code in the XPage for the before/after page-load and document-related events).
So on its own, the setup I have above doesn't necessarily buy you much. You save a bit of repetitive code for standard CRUD pages when using the document-controller class, but that's about it. The real value for me so far has been a clarification of what goes where. Previously, if I wanted to run some code on page load, or attached to a button, or to set a viewScope
value, or so forth, it could go anywhere: in a page event, in a button action, in a dataContext
, in a this.value
property for a xp:repeat
, or any number of other places. Now, if I want to evaluate something, it's going to happen in one place: the controller class. So if I have a bit of information that needs recalculating (say, the total cost of a shopping cart), I make a getTotalCost()
method on the controller and set the value in the XPage to pageController.totalCost
. Similarly, if I need to set some special values on a document on load or save, I have a clear, standard way to do it:
package controller; import com.ibm.xsp.model.domino.wrapped.DominoDocument; import frostillicus.controller.BasicDocumentController; public class Projects_Contact extends BasicDocumentController { private static final long serialVersionUID = 1L; @Override public String save() throws Exception { DominoDocument doc = this.getDoc(); doc.setValue("FullName", ("CN=" + doc.getValue("FirstName") + " " + doc.getValue("LastName")).trim() + "/O=IKSGClients"); return super.save(); } @Override public void postNewDocument() throws Exception { super.postNewDocument(); DominoDocument doc = this.getDoc(); doc.setValue("Type", "Person"); doc.setValue("MailSystem", "5"); } }
It sounds like a small thing - after all, who cares if you have some SSJS in the postNewDocument
event on an XPage? And, besides, isn't that where it's supposed to go? Well, sure, when you only have a small amount of code, doing it inline works fine. However, as I've been running into for a while, the flexibility of XPages makes them particularly vulnerable to becoming tangled blobs of repeated and messy code. By creating a clean, strict system from the start, I've made it so that the separation is never muddied: the XPage is about how things appear and the controller class is about how those things get to the XPage in the first place.
We'll see how it holds up as I use it more, but so far this "controller"-class method strikes a good balance between code cleanliness without getting too crazy on the backing framework (as opposed so some of the "model" systems I've tried making).