In the first three post of this series, I've introduced the concepts of the "Dojo Class", the ability to load them from an NSF repository, and the use of a namespaced class as the foundation of your coding practices for client-side javascript in your XPages applications. In this post, I am going to further flesh out how to implement an MVC design pattern in your XPages, both server and client side, and then show you how to practically apply the use of the dojo class as your controller in an XPage application as part of the Model-View-Controller pattern.
First lets define how we can implement the MVC design pattern in XPages. We have three basic elements with which to work with in XPages
- The XSP Markup document (The MODEL)
- CSS & Themes (The VIEW)
- Java/Javascript (both Client & Server Side) (The CONTROLLER)
First, let's talk briefly about the XSP document, and what you should, and more importantly, what you shouldn't be putting into the XSP.
What you should: Data, semantic markup (for example, div's, ul's, p's), "block UI devices" CSS classes, and hooks into your Controller. What you shouldn't: styles, inline client-side javascript code, and as little inline server-side javascript code as possible. Why? Following these guidlines will help you create re-usable components that can be assembled or displayed differently based on different a VIEW or different CONTROLLER. Your XSP document is your model, if it is filled with inline styles that describe how it is supposed to look, or scripts that define how its supposed to act, then your VIEW and CONTROLLER will have a hard time displaying the model the way its supposed to.
So for example, here is an example of bad markup for an MVC XPages Application:
var x = database.getView('myView');
var y = x.getDocumentByKey(viewScope.someScopedVar);
return ((y.getItemValueString('VendorActive') == 'YES') ? true : false);
]]>
Vendor Address
var x = database.getView('myView');
var y = x.getDocumentByKey(viewScope.someScopedVar);
var z = y.getItemValueString('Address1') + '
' + y.getItemValueString('Address2') + '
' + y.getItemValueString('City') + ', ' + y.getItemValueString('State') + ' ' + y.getItemValueString('ZipPlus4')
]]>
confirm('Are you sure you want to inactive this customer?');
// ... more client side javascript here ...
]]>
While syntactically correct, and it "gets the job done," this code can be used once, where it's at, and how it's defined. Also, on the face of it, its very hard code hard to read. Even all nicely laid out like i have it. If you were to let that same code be generated by designer, it is much worse. Next, any and all chance to re-use the above code in other aspects of the page is nil, null, nothing, 0, {}, and [].
As a start, let's take the same block, and follow our rules from above on what should go into our Model, just replacing inline styles with CSS classes, and replacing our javascript with hooks into a controller.
Vendor Address
Very tight, and easy to read isn't it? Not to mention, further down the page we can reuse the same CSS classes for another address block, and the same javascript functions can be used for their functions as well.
Ok, so now you can see the difference between an XPage with and without an MVC pattern applied. Now lets look at how we can apply the controllers to an XPage. We really have two controllers when it comes to an XPage, one for the server side, and one for the client side. The "controlling" controller is the server side controller script, as it can change out or decide which client side controller to apply to your XPage. Of the two controllers, the easiest to implement is the client side. With the server side controller, we have to take into consideration things like what phase of the JSF lifecycle the page is in, we have to worry about data caching, we have to worry about object restoration, etc. Because of that, I'll introduce the use of server-side controllers at a later time. Let's focus in on the client side for now.
Ok, so in previous posts, I've introduced the concepts of the dojo namespace and the "class" as defined by dojo. Quite simply, our client-side controller is going to be a class that represents our client side page. If you have developed in another "class" based development environment such as Delphi, Visual Basic, or the like, you are probably very familiar with the concept of your UI being defined by a class, but its something new to Notes development, and in general not really practiced in web development. For the same reasons you get simplicity and power from class-based languages like Delphi and Visual Basic, having a class represent your page can be very powerful as well.
Let's start off with defining how we would implement a class on our page. First let's create a client-side javascript library in our database, following the namespace rules we've laid out before. Let's assume our xsp page (our model) is called "vendorRecord.xsp", in a "vendor" application ... so we could namespace our class like this:
com.ZetaOne.app.vendor.vendorRecord
They way I prefer to implement the controller is to have one script that is the "controller" for the page, and that controller implements other scripts that are "class" scripts. For example, I will create a script called:
com/ZetaOne/app/vendor/vendorRecord/controller.js
and one script called
com/ZetaOne/app/vendor/vendorRecord/class.js
Sometimes, I may actually implement 2 or 3 or more classes within a given page, depending on the model, and the elements I have included in the model. For example. If I have a custom control called vendorPerformanceReview that I have included on the page I would include that class script as well. Each of these scripts are loaded and initialized by the controller.js script.
For example, lets take a look at a sample controller.js script:
dojo.require("dojo._base._loader.loader_nsf");
dojo.requireFromNsf('com.ZetaOne.app.vendor.vendorRecord._class', window.location.href.split('.nsf')[0] + '.nsf/');
dojo.requireFromNsf('com.ZetaOne.app.vendor.vendorPerformanceReview._class', window.location.href.split('.nsf')[0] + '.nsf/');
XSP.addOnLoad(function() {
window.vendorRecord = new com.ZetaOne.app.vendor.vendorRecord._class();
window.vendorRecord.performanceReview = new com.ZetaOne.app.vendor.vendorPerformanceReview._class()
// Here you can do additional non-class related functions after the loading of your page
});
The main reasons I use the controller.js to get things started is because it provides a single initialization point for the page where I can coordinate the load/configration process, and I can use the server-side controller to load different client side controllers that load different classes that do different functions at run time based upon different conditions, such as user security level, or the data's position in a process, etc. This allows my server-side controller to be fairly agnostic to the the requirements of each condition, it just needs to associate my desired cireteria with the proper controller script.
For simplicity's sake at the moment, let's just assume we have a single controller. In this case, to apply it to the page, I just add the controller.js as a script library resource to the XPage. In a future post, when we review server side controllers, we can review how to include different client side controllers, etc.
Ok, now that we've looked at how the main client-side controller script could operate by loading individual classes that represent our page, let's look at how a class actually operates.
Let's start out with the basic class definition for our client side controller:
dojo.provide("com.ZetaOne.app.vendor.vendorRecord._class");
(function(){
dojo.declare("com.ZetaOne.app.vendor.vendorRecord._class", null, {
constructor: function() {
return this;
},
destroy: function() {
}
})
})();
This is the very basic start of our class. When our controller does a " = new com.ZetaOne.app.vendor.vendorRecord._class", it basically creates a copy of this object, and then executes the constructor function. The constructor function needs to return the actual constructed object, hence the line "return this;". The constructor is where we do all our initialization work to hook our class into the HTML page as needed, initialize variables, or work over the HTML page as it is at the time the class is initialized.
The destroy function is a basic destructor. It preps the class to be garbage collected. Garbage collection can be a new concept as well for many of us who aren't used to languages like java, C, etc. Javascript is an automatically garbage collecting language, however, there are things you can do to prevent memory from being collected, and you can cause a memory leak in the browser. As a practice, there are certain things you should do to prevent this from happening, and you should do them in your class's destructor. For example, each and every time you do a dojo.connect() to hook into an event in the DOM or another widget, you need to later dojo.disconnect(), or you'll eat memory. I've seen applications with deep use of dojo classes and dojo.connects() that didn't properly manage their resources quickly gobble up GB's of memory in a browser. If you are careful, and you follow a few basic rules, you'll be good.
So as an example, here's an instance of connecting the onclick event on a set of DIV tags to a function in our class, and then later we'll take care to dispose of them when the page gets unloaded.
dojo.provide("com.ZetaOne.app.vendor.vendorRecord._class");
(function(){
dojo.declare("com.ZetaOne.app.vendor.vendorRecord._class", null, {
constructor: function() {
this._connects = [];
var myDivs = dojo.query('div.myDivClass');
for (i=0; i this._connects.push(dojo.connect(myDivs[i], 'onclick', this, this.clickAlert));
}
// make sure we destroy ourselves if they unload us...
this._connects.push(dojo.connect(document, 'onbeforeunload', this, this.destroy());
return this;
},
destroy: function() {
for (i=0;i dojo.disconnect(this._connects.pop());
}
},
clickAlert: function() {
var e = (arguments[0]) ? arguments[0] : window.event;
alert('You just clicked the DIV tag with an ID of ' + e.target.id);
}
})
})();
There we have a basic class that hooks into the HTML, adds some onclick events, and then gracefully cleans up after itself when the page is unloaded.
Now, in our XSP markup we can reference our class as well. For example, we know the controller script is going to place our controller in the window.vendorRecord object, so in our XSP code, we can hook into the class as well.
For example, we could add a button that calls the clickAlert function from our class above:
This is just the start of the capability of what you can do, and the portability you can create in your code. I'll have some downloadable samples soon that you can download and look through an actual implementation.
In the mean time, Happy Coding, and stayed tuned to the XPages Blog for more tips and tricks!