| Version 4 (modified by scott, 7 years ago) |
|---|
Client side MVC pattern
One of the more popular trends of the last decade has been object oriented model view controller frameworks (herein MVC). First, let's talk for a moment about MVC frameworks.
The MVC paradigm is a way of breaking an application, or even just a piece of an application's interface, into three parts: the model, the view, and the controller. MVC was originally developed to map the traditional input, processing, output roles into the GUI realm.
If you're already fairly experienced with web development methodologies, you probably are familiar with this paradigm for separation of roles. There are literally a myriad of MVC frameworks available (Apache Struts, Zope, Catalyst). The MVC concept was designed to accommodate adaptation of existing systems to graphical user interfaces, where the representation of data is complimented by modeling the representation environment (or external world). The abstraction of the model allowed an environment for transforming data into a graphical user interface. Web applications, although they don't seem to be, to everyone, are quite simply graphical user interface applications in which the interface is rendered (typically by markup) and shipped to a remote display over a transfer protocol (typically !Hyper-Text Transfer Protocol, or HTTP).
AJAX and SOAs
Today, a variation of an older web application development technique for interactive interface updates, called AJAX has sprung into popularity. The acronym stands for Asynchronous JavaScript And XML, which is slightly misleading as making your requests asynchronous is quite optional, and not all applications rely on XML as a transfer format
Another popular trend, mostly arising in enterprise systems, called service oriented architectures is quickly taking the enterprise application development world by storm. While (as with AJAX) in no way really a new concept, it's recent popularity has arisen with Web Services. Web Services are a modern twist in RPC (remote procedure call) services, which are implemented with HTTP and XML. Fortunately, the popularity with Web Services has provided a simple, standard compliant mechanism for applications to interact with each other.
The interesting thing about these two, quite separate trends is how well they actually compliment each other. The popularity of Ajax has arisen from the introduction of XML HTTP transport APIs by most modern web browsers. Interestingly enough, along with the introduction of this technology most modern web browsers also include a scriptable interface to a native XSLT processor. The combination of the two of these can quickly and easily be used, along with a standard web service, to build a graphical user interface application utilizing the MVC design pattern and standard Web based technologies.
Client MVC
In an MVC framework, the controller typically interacts with the model based on user input. In modern web browser implementations, the rendering engine displays a document based on a Document Object Model (DOM) structure. Fortunately, the DOM API includes event handling. The DOM event API virtually stands alone as a controller. It needs very little help.
One of the more flexible features of most MVC frameworks is the ability to describe the routing between events in the controller and their handlers, without source code. Fortunately, this can be done with very little work, since everything else we need for the controller already exists. Consider the XML event handling description in figure 1.1.
<?xml version="1.0" encoding="utf-8"?>
<controller xmlns="http://www.blisted.org/MVD">
<view id="upload">
<event type="click" handler="controller.upload"/>
</view>
</controller>
Now, first things first, in order to provide reasonable exception handling, I tend write wrappers around the XMLHttpRequest object. In this case, we're just going to throw exceptions (which will typically go uncaught, so as to show up in the javascript console),
function CBClientError (message, ua) {
this.message = message
this.status = ua.status
this.headers = ua.getResponseHeaders()
}
CBClientError.prototype = new Error("CBClient Error: Unknown")
function CBClient (callback) {
if (typeof callback != "function")
throw new Error(
"CBClient requires a callback function for construction")
var ua = new XMLHttpRequest()
var self = this
var readystatehandler = function () {
if (ua.readyState == 4) {
switch (ua.status) {
case 302:
case 304:
var url = ua.getResponseHeader("Location")
self.get(url)
break;
case 404:
throw new CBClientException("File not found", ua)
default:
throw new CBClientException("Unknown", ua)
case 200:
callback({
document: ua.responseXML,
data: ua.responseText
})
}
}
}
this.get = function (url) {
ua.open("GET", url, true)
ua.send("")
ua.onreadystatechange = readystatehandler
}
}
Now that we have that out of the way, let's use it to fetch some a document of the form described above, and then register the appropriate events. We're using a real simple model in this example, one where the node->event map is handled simply by unique id's for each node that is expected to fire an event.
function UnableToRegisterError (node, type, handler, reason) {
this.message = "(view id: \f) Unable to register \f as a \f handler: \f"
.format(node, type, handler, reason)
}
UnableToRegisterError.prototype = new Error("Event Registration Exception")
window.onload = function () {
var registerEvents = function (response) {
var controller = response.document.documentRootElement
var view = controller.firstChild
while (view) {
var id = view.getAttribute("id")
var node = document.getElementById(id)
var events = view.getElementsByTagName("event")
for (var i = 0; i < events.length; i++) {
var type = events[i].getAttribute("type")
var handler = events[i].getAttribute("handler")
var ns = handler.split(/\./)
/* To map the namespace, we start at the window object */
var callback = window
/* Iterate over the namespace levels and map callback to the
* appropriate event, which should be a function typically.
* Incase there was a user error, this exception should how up
* in the javascript console.
*/
for (var i = 0; i < ns; i++) {
if (callback[ns[i]] == undefined)
throw new UnableToRegisterException(id, type, handler,
"\f is undefined".format(ns.slice(0, i).join(".")))
callback = callback[ns[i]]
}
if (typeof callback != "function") {
throw new UnableToRegisterException(id, type, handler,
"value type is not function")
}
/* Finally, we safely register the event */
node.addEventListener( type, handler, false )
}
view = view.nextSibling
}
}
var client = new CBClient(registerEvents)
client.get("events.xml")
}
Because these event handlers are directly added to the node in question, they are invoked in the context of that node. Meaning "this", is the node in which the event was fired. This can be quite handy. Not only does it allow easy access to the view at the point of the event, but it also helps promote the development of more reusable events by letting events rely only to items that are relevant to their position (meaning another element that has a similar structure surrounding it to what a given element depends on could re-use an event, although this isn't always possible).
var controller {
list: {
/* We're going to assume that the filename we want, and this element,
* are in the same box.
*/
upload: function (event) {
var inputs = this.parentNode.getElementsByTagName("textbox")
for (var i = 0; i < inputs.length; i++) {
if (inputs[i].class.match(/filename/)) {
var result = model.list.upload(inputs[i].value)
updater.list.upload.handler(result)
}
}
}
}
}
At this point, we need a model that is useful. Since the MVC design pattern does not specifically mention data access, and most web applications are rather data access driven, our application places data access underneath the model. In this instance, the model is accessed from the controller in order to perform actions on the data, through a web service.
At this point, the model reaches into the web service, responding to the controller. This help us create a very strict separation between the model and the view. The controller then can update the model properly, creating the necessary separation between model and controller.
function SOAPFault (fault) {
for (int i = 0; i < fault.childNodes.length; i++) {
var node = fault.childNodes[i]
this[node.tagName] = node.value
}
}
SOAPFault.prototype = new Error("SOAP Fault")
var LISTNS = "http://www.blisted.org/example/list"
var CREDENTIALSNS = "http://www.blisted.org/example/credentials"
var model = {
list: {
service: "http://listservice.san/",
/* As our model more or less simply wraps web service calls in this
* particular case, we just return a DOM structure from the model that
* contains the response from the service call.
*
* Sometimes this might not be appropriate.
*/
upload: function (filename) {
var ua = new XMLHttpRequest()
ua.open("GET", model.list.service, false)
ua.setRequestHeader("SOAPAction", "upload")
var envelope = document.createElement("Envelope", SOAPNS)
var body = envleope.creaetElement("Body", SOAPNS)
var operation = document.createElement("upload", LISTNS)
var credentials = document.createElement("credentials",
CREDENTIALSNS)
var file = document.createElement("file", LISTNS)
credentials.setAttribute("username", session.get("username"))
credentials.setAttribute("password", session.get("password"))
file.setAttribute("name", filename)
envelope.appendChild(body)
body.appendChild(operation)
operation.appendChild(credentials)
operation.appendChild(file)
ua.send(envelope)
var envelope = ua.responseXML.documentRootElement
var response = envelope.firstChild.firstChild
if (response.tagName == "Fault") {
throw new SOAPFault(response)
}
return response
}
}
}
So if you've paid attention to this bit of example code, you'll see that there are two things that are unaccounted for. One of them is an object called updater, and the other is an object called session. Well the former, we will not go into here. But assume it is a hash table for managing simple data about the correspondence with the web services, and that it was populated by the controller at some point. And now for the more important (to this discussion) of the two, the updater.
The updater is the connector between the controller and the view. As you could see previously, the view's interaction with the controller is being dictated by a simple XML configuration file mapping the handling of DOM events to certain callbacks on runtime objects. Now the updater will take the response from the model (and it is key the model knows thing about it), and update the user interface with very little code by simply applying an XSLT transformation rule to the data.
Here we'll reuse the callback client (CBClient) code above to fetch and parse the templates before we use them, so we have them ready by the time a request is actually made.
var client = new CBClient(loadTemplate)
var updater = {
list: {
upload: {
xsl: null,
template: "upload.xsl",
handler: function (dom) {
var xsl = new XLSTProcessor()
xsl.importStylesheet(updater.list.upload.xsl)
var result = xsl.transformToFragment(dom)
var status = document.getElementById("status")
status.parentNode.replaceChild(result, status)
}
}
}
}
for (var component in updater) {
for (var handler in updater[component]) {
var loadXSL = function (response) {
updater[component][handler].xsl = response.document
}
var client = new CBClient(loadXSL)
client.get(updater[component][handler].template)
}
}
Things to think about
- The interaction with the updater could easily be externally described with a a more generic mechanism similar to the view->controller map.
- You could easily pre-instantiate one XSLTProcessor object per update handler, and populate it with the appropriate style sheet during initialization.
- The Observer Pattern could be very easily implemented between the controller and the model, allowing you to actually make these request asynchronous.
- A small piece of code (roughly 150 total lines) could wrap these features into an even easier to use framework.
- There are a number of Mozilla specific tools that would simplify many of these steps (such as the SOAPClient object)
