background image

MVC for Appcelerator

I was recently tasked to create a mobile app at work. Since I had never worked on a mobile app (not counting web-based apps), I started searching for a cross-platform framework. To make a long story short, I settled with Appcelerator. I have run into some issues while working on my app but for the most part there are simple workarounds. I love working with javascript and I can definitely see the platform is going somewhere. What I didn’t like is how easy it is to make bad javascript and end up with very large files. This was a problem that I could solve myself.

I searched around the internet and found a few solutions. Some people were using PureMvc or similar frameworks. I read through the documentation and the frameworks were either too complex for my needs or may have felt out of place when used with Appcelerator. I did find something close to what I wanted by Scott Montgomerie. My issues with his implementation was that you constantly have to deal with the current window context and you are coupling your views with your controllers. I also like to stay away from the ‘new’ keyword when working with javascript. In addition I wanted routing as I am used to the MVC style of Asp.Net MVC (may be overkill but it was nice to have). It’s what I know and I love it :).

Before I get started here is the working example. Keep in mind this has only been tested on Android in my Windows environment and probably needs a bit more testing so this release is just an Alpha.

So with my implementation I wanted to keep things very simple and try to stay away from coupling the framework to Appcelerator. I also wanted routing so I could simply write code that evaluates to, “Go to the default view for the current controller.” This led me to a framework with the following setup:


For setting up the framework I have:

  • Mvc.init – This is called to setup the framework. It initializes and extends all controllers
    and views.
  • Mvc.mapRoute – This sets up your default routes and is used internally by the routing engine.
  • Mvc.render – This is a callback function which is invoked after a view has been rendered.
    Using this approach we can decouple the Mvc framework from Appcelerator and possibly use in
    some other context. In the case of Appcelerator, I used this function for window management.
  • Mvc.find – You shouldn’t ever need to use this but I exposed it just in case. This will find
    your route based on a path.
  • Mvc.start – After everything is set up, this must be called to render the default action. This
    will make much more sense after my example.

For each controller I simply have:

  • name – The name of the view you want to render.
  • view – A function that takes the parameters of “action” and “model”. The action being the
    view you wish to render and the model used to render the view.

For each view I have the following functions:

  • action – When you pass a path to this function and possibly a value or model, this will invoke
    an action on a controller and return a rendered view.
  • partial – This simply takes whatever path you pass in and returns a view. This does not invoke
    the action for the route so in some cases you may have a view without an action.

That right there is the entire framework. It is very simple and easy to use. It also abstracts a ton of the work you have to go through in order to manage your project. Next I’ll go through how to use the framework with sample code.

The first thing we need to do is grab the mvc code. This is all home-grown except for some regular expressions I borrowed and modified from jsRouter. Save this as ‘mvc.js’. Here it is:

		var Mvc = (function ($) {

			if($.mvc) {
				return $.mvc; // do not reinitialize
			}

			var getRouteCollection;

			function extend() {
				var destination = arguments[0], source;
				for(var i = 1; i < arguments.length; i += 1) { 					source = arguments[i]; 					for (var property in source) { 						if (typeof source[property] === "object" && 							source[property] !== null ) { 							destination[property] = destination[property] || {}; 							arguments.callee(destination[property], source[property]); 						} else if(destination && source[property]) { 							destination[property] = source[property]; 						} 					} 				} 				return destination; 			} 			 			function isEmptyObject(obj) { 				for(var prop in obj) { 					if (Object.prototype.hasOwnProperty.call(obj, prop)) { 						return false; 					} 				} 				return true; 			} 			 			// via jsRouter with some modifications -- http://jsrouter.codeplex.com/ 			function buildRegExp(route) { 				// converts the route format into a regular expression that would match matching paths 				var pathSegments = route.path.replace(/[/.]+$/, '').split(//|./), 					defaults = route.defaults; 				var regexp = ['^']; 				for (var i = 0, segment, match; (segment = pathSegments[i]) !== undefined; i++) { 					if ((match = /{(w+)}/.exec(segment)) != null) { 						var argName = match[1]; 						// add a backslash, except for the first segment 						regexp.push((i > 0 ? '(/' : '') + '([^/]+)' + (i > 0 ? ')' : ''));

						if (defaults && defaults[argName] !== undefined) {
							// make the group optional if the parameter has a default value
							regexp.push('?');
						}
					}
					else {
						regexp.push((i > 0 ? '/' : '') + segment);
					}
				}
				regexp.push('$');

				return new RegExp(regexp.join(''), 'i');
			}

			function parsePath(path, route) {
				// parses the values in the hash into an object with the keys the values specified in the given route
				var values = extend({}, route.defaults),
					pathSegments = path.split(//|./);

				for (var i = 0, segment, match; i < pathSegments.length && (segment = route.segments[i]); i++) {
					if ((match = /{(w+)}/.exec(segment)) != null) {
						if (pathSegments[i] == '' && values[match[1]]) {
							continue; // skip empty values when the value is already set to a default value
						}
						values[match[1]] = pathSegments[i];
					}
				}
				return values;
			}

			// The base controller -- all controllers under Mvc.Controllers will get these properties
			function Controller(name) {
				return {
					name : name,
					view : function(action, model) {
						return {
							action : action, // either foo/bar/1, foo.bar.1 or {controller : 'foo', action : 'bar', id : '1'}
							model : model
						};
					}
				};
			}

			$.mvc = {
				Controllers: {}, // object to hold all controllers -- used in initialization
				Views : {}, // object to hold all views
				init : function(options) {
					var ctrlrs = this.Controllers, controller, views = this.Views;

					// set up controllers
					for(var key in ctrlrs) {
						if(!ctrlrs.hasOwnProperty(key)) {
							continue;
						}

						// make sure this is actually a Controller
						if(key.indexOf("Controller") === -1) {
							ctrlrs[key] = null;
							continue;
						}

						// extend the controller to include the properties from baseController
						ctrlrs[key] = extend(ctrlrs[key], Controller(key));
					}

					var self = this;
					self.process = function(route, data) {
						var r = self.find(route), createdObject = false;
						if(!r) {
							throw "Could not find any routes. Register routes via Mvc.mapRoute";
						}

						// set the routeData in sharedData
						rc.context.routeData = extend({}, r.routeData);

						if(!data && data !== false) {
							data = {};
							createdObject = true;
						}

						// only extend the data object if it is already a complex object
						if(!isEmptyObject(data)) {
							data = extend(data, r.routeData);
						}

						var rd = r.routeData;
						delete data["controller"];
						delete data["action"];				

						if(createdObject && isEmptyObject(data)) {
							data = null;
						}

						var cname = rd.controller + "Controller", controller = self.Controllers[cname];
						if(!controller) {
							throw "Controller " + cname + " has not been defined";
						}
						if(!controller[rd.action]) {
							throw "Action " + rd.action + " in controller " + cname + " does not exist";
						}
						var view = controller[rd.action].call(controller, data);

						if(!view) {
							// we didn't need to show anything
							return;
						}

						return self._render(view.action, view.model);
					};

					self._render = function(route, data, partial) {
						var r = self.find(route);
						if(!r) {
							throw "Could not find any routes. Register routes via Mvc.mapRoute";
						}
						var cname = r.routeData.controller, vname = r.routeData.action;
						if(!self.Views[cname]) {
							throw "View Controller " + cname + " does not exist in Views collection";
						}
						if(!self.Views[cname][vname]) {
							throw "View " + vname + " does not exist in View Controller " + cname;
						}
						var renderOutput = self.Views[cname][vname](data);
						if(!partial && self.render) {
							renderOutput = self.render(renderOutput, r.routeData);
						}
						return renderOutput;
					};
					self._partial = function(route, data) {
						// tell the render function that this is a partial render and we should not call
						// the user defined render function
						return self._render(route, data, true);
					}

					// setup the views so they have an 'action' method
					for(var key in views) {
						if(!views.hasOwnProperty(key)) {
							continue;
						}
						views[key].action = self.process;
						views[key].partial = self._partial;
					}

					var rc = this.sharedData = {
						routes : [],
						context : {
							routeData : {
								controller : null,
								action : null
							}
						}
					};
				},
				mapRoute : function (name, path, defaults) {
					var scrubbedPath = path.substr(path.indexOf('{'));

					// get rid of slashes
					if(scrubbedPath[0] === '/') {
						scrubbedPath = scrubbedPath.substring(1, scrubbedPath.length - 1);
					}
					if(scrubbedPath[scrubbedPath.length - 1] === '/') {
						scrubbedPath = scrubbedPath.substr(scrubbedPath.length - 2);
					}

					this.sharedData.routes.push({
						name : name,
						path : path,
						defaults: defaults
					})
				},
				find: function (path) {
					var routes = this.sharedData.routes,
						ctrlrs = this.Controllers,
						context = this.sharedData.context;

					if(!path) {
						path = "";
					}

					// check for match
					for (var i = 0, route; i < routes.length; i++) { 						route = routes[i]; 						if (!route.regexp) { // generate regexps on the fly and cache them for the future 							route.regexp = buildRegExp(route); 							route.segments = route.path.split(//|./); 						} 						var isMatch = route.regexp.test(path); 						if (isMatch) { 							// route found, parse the route values and return an extended object 							 							// populate last controller if it doesn't exist with default 							if(!context.routeData.controller) { 								context.routeData.controller = route.defaults.controller; 							} 							 							// check current controller for match -- takes care of simply passing an action 							var ctrlr = context.routeData.controller + "Controller"; 							var useLastController = ctrlrs[ctrlr] && 													ctrlrs[ctrlr][path];  							 							var routeData = extend({}, context.routeData); 							if(useLastController) { 								var pathSegments = path.split(//|./), iter = 1; 								routeData.action = pathSegments[0]; 								for(var key in routeData) { 									if(!routeData.hasOwnProperty(key)) { 										continue; 									} 									if(key === "controller" || key === "action") { 										continue; 									} 									 									if(iter >= pathSegments.length) {
										break;
									}

									routeData[key] = pathSegments[iter];
									iter += 1;
								}
							} else {
								routeData = parsePath(path, route);
							}

							return extend({}, route, { routeData : routeData });
						}
					}
					return null;
				},
				start : function (route, data) { // parameters in case we want to start the app with something other than the default page
					this.process(route, data);
				}
			};

			return $.mvc;
		}(Titanium.App));

The next thing you need to do is erase everything in your app.js file. This file will be the setup/initialization code for the project. We will add the following code.

		// include a reference to the mvc file
		Ti.include('mvc.js'); 

		// This doesn't exist yet but we'll get to that next
		Ti.include('/controllers/HomeController.js');

		// Initializes the framework
		Mvc.init(); 

		// map a default route
		Mvc.mapRoute(
			"Default_Route",
			"{controller}.{action}.{id}", // could also be {controller}/{action}/{id}
			{
				controller : "Home",
				action : "Default",
				id : null
			});

		// we'll get into this in a bit.
		Mvc.render = function(ui, routeData) { } 

		// This will go off and attempt to render the default route but will
		// fail since we have not created the 'Home' controller yet
		Mvc.start();

Now I will actually implement the Mvc.render function in app.js. This is super specific to how I want my app to manage windows. It should work for you as well but you may want to customize/improve this code.

	// manage the windows -- window stack
	var openWindows = [];

	Mvc.render = function(ui, routeData) {
		var win, index, routeName = routeData.controller + "." + routeData.action;

		// find the index in the stack of windows
		for(var i = openWindows.length - 1; i >= 0; i -= 1) {
			if(openWindows[i]._title === routeName) {
				win = openWindows[i];
				index = i + 1;
				break;
			}
		}
		function cleanup(index) {
			for(var i = index; i < openWindows.length; i += 1) {
				openWindows[i].close();
				openWindows[i] = null;
			}
			openWindows = openWindows.splice(0, index);
		}
		// if the window already exists in the stack
		if(win) {
			// clean up array -- remove all windows
			cleanup(index);

			// remove children from current view
			var children = win.children;
			for(var i = 0; i < children.length; i += 1) { 				win.remove(children[i]) 			} 		} else { 			// create a new window 			win = Ti.UI.createWindow({  				_title : routeName, 				fullscreen : true 			}); 			 			win.addEventListener('close', function() { 				// find the index of this window so we can remove it when the back button is pressed 				for(var i = openWindows.length - 1; i >= 0; i -= 1) {
					if(openWindows[i]._title === routeName) {
						index = i;
						break;
					}
				}
				cleanup(index);
			});

			// This handles the creation of the first window so that
			// Appcelerator will exit your app when this window is closed.
			if(openWindows.length === 0) {
				win.exitOnClose = true;
			}

			openWindows.push(win);
			win.open();
		}

		// This adds the controls you created in your view
		win.add(ui);

		// return the window in case you need it for something in your view
		return win;
	};

That right there is all the code you need to get going. Next we need to actually implement the ‘HomeController’. So create a file called ‘HomeController.js’ inside a ‘controllers’ folder and add the following code:

		// This will be the location for the 'Default' view
		Ti.include('/views/Home/Default.js');
		// another view
		Ti.include('/views/Home/DoStuff.js');

		(function(c) {
			// This is just a basic model used to render
			// the 'Default' view.
			function DefaultModel() {
				return {
					text : null,
					value : 0
				};
			}
			// Initializes the 'HomeController' in Mvc.Controllers
			c.HomeController = {
				Default : function() {
					// Create the model and populate it with some values
					var model = DefaultModel();
					model.text = "Sample Text";
					model.value = "Sample value";

					// This returns a view and tells the framework to render the
					// 'Default' view using the model we created.
					return this.view('Default', model);
				},
				// we could add another action here
				DoStuff : function (value) {
					// manipulate the value parameter and return the appropriate view
					return this.view('DoStuff', value + "!!!");
				}
			};
		}(Mvc.Controllers));

Next we need to implement our view. This is a very simple and similar process to creating Controllers. Create a ‘views’ folder and then a ‘Home’ folder.

		(function(v) {
			// Create the 'Home' object if it doesn't already exist
			// This is used by the routing engine to assosciate the
			// view with the correct controller
			if(!v.Home) {
				v.Home = {};
			}

			/*
			 * Render the default page
			 */
			v.Home.Default = function(model) {
				// get a reference to the view
				var self = this;

				var view = Ti.UI.createView({layout : "vertical"});
				var label = Ti.UI.createLabel({
					text : model.text
				});
				var textField = Ti.UI.createTextField({
					value : model.value
				});
				var button = Ti.UI.createButton({
					title : "Submit"
				});

				// This will call DoStuff
				button.addEventListener("click", function(e) {
					self.action("DoStuff", textField.value);
				});
				view.add(label);
				view.add(textField);
				view.add(button);

				return view;
			};
		}(Mvc.Views));

Lastly we create the ‘DoStuff’ view which is nearly the same as the ‘Default’ view.

		(function(v) {
	// Create the 'Home' object if it doesn't already exist
	if(!v.Home) {
		v.Home = {};
	}
	
	/*
	 * Render the DoStuff page
	 */
	v.Home.DoStuff = function(model) {
		var view = Ti.UI.createView({
			layout : "vertical",
			backgroundColor : "#fff"
		});
		var label = Ti.UI.createLabel({
			text : model,
			color : "#000"
		});
				
		view.add(label);
		
		return view;
	};
}(Mvc.Views));

That is my MVC framework in a nutshell although this only shows the basic functionality. In my next post I will expand on this example and integrate model-binding, jsAutomapper, and using joli for data access. I will also focus on the routing portion of my framework and how to do some more advanced things with it. I think this framework is stupidly simple and really allows for SOC as well as forcing you to write better code. If you have any comments/suggestings/bugs feel free to leave them here.

Anyway, you can download the working project HERE



22 views shared on this article. Join in...

  1. Rick says:

    Nice work here – love that someones working on getting a decent MVC on Titanium….I hate how my apps end up being a pain in the butt to manage due to poor code organization.

    I did download the sample code however on the iOS iPhone simulator all you’re presented with on running the base code is a black screen with half of the word ‘submit’ showing at the top of the window. Any ideas?

  2. John says:

    Hmm, I am on a windows machine and it renders correctly in Android. I tried to keep the rendering code relatively simple and just show how the framework transitions from window to window.

    Look at ~/views/Home/Default.js for the rendering code there. The only thing that I can see that could be an issue is

    layout : “vertical” on line 14

    Does that normally work for iOs?

  3. Rick says:

    Ahh that may be it. I’ve not used a layout setting for iOS before so maybe its just meant for Android. I’ll have a play around with it later today :)

  4. florian says:

    and another comment: i can launch the app, i see the manage account button click on it and get this error:

    message = “Result of expression ‘dal.accounts’ [undefined] is not an object.”;
    sourceURL = “file://localhost/Users/florhaf/Documents/Titanium Studio Workspace/mvc example 2/Resources/controllers/AccountsController.js”;
    line = 20;

  5. John Kalberer says:

    Alright… I am halfway through writing the example for using this. I’ll figure out what is going on when I get to work tomorrow. Thanks for reporting these issues. I guess I need to write some JavaScript unit tests.

  6. Daniel says:

    First of all, this looks GREAT! I am a ColdFusion dev and use CFWheels a lot so this seems to fit nicely with my line of thinking. Plus the Titanium kitchen sink was starting to piss me off.

    Secondly, just wanted to bump Florian’s “dal.accounts” problem…

  7. Wouter Van den Bosch says:

    Hi again John,

    How would you go about connecting with a webservice in the framework? Suppose the Accounts view would be filled up with accounts from a REST or SOAP service.

    Trying to work out where to best load up that part of my code, but some pointers on how you would do it, would most-certainly be welcome.

    Best regards,
    Wouter

    • Yeah so what I would do if I was working with a web service is find a modified jQuery file that works with Appcelerator. This will allow you to use jQuery selectors to navigate your xml and build objects from them.

      From here you simply use the jQuery.ajax function to access the webservice and build your object from the xml (if the web service supports json this is even easier).

      Next you create a repository object similar to my dal object. I would implement the basic ‘create, read, update, delete’ methods in this object for Accounts. So your call would be like ‘repo.Get(accountID, callback)’. I pass in a callback since the web service is asynchronous. You can use this callback to populate your ui etc.

      The difficult part that I see is synchronizing the action and the web request since you need to return a view. If the jQuery implementation is done correctly you will be able to set the ‘async’ parameter of jQuery.ajax to false. If not, you will have to use a setInterval or something to keep polling to see if your web request is finished.

      If this doesn’t explain it well enough I can write a blog post.

      • Wouter Van den Bosch says:

        Thanks for the reply John,

        The whole exercise made me go over the framework in depth, which was a good thing. I indeed figured the best place to add such functionality to was to create something similar to the joli.model which I could then add to the dal.

        I’m browing around for a good existing bit of code and I’ll get back to you with what I did.

        Best regards,
        Wouter

  8. Clayton says:

    I am relatively new to MVC, whether via web or rich client, so forgive me if this is a silly question…

    What is your recommended approach under this scheme for a controller to respond to events in the view? Should any such event cause an action on a controller and a re-rendering of the view?

    The scenario I have is, I want to monitor the text input in a particular field, and inform the controller when it has changed. That is, on every key press. This is a “lightweight” sort of event, because if the full view were to re-render every time the user pressed a key, that would inhibit the user’s ability to actually enter text.
    From what I understand the definition of MVC to be, it is legitimate for the controller to observe the view.

    This MVC framework seems to abstract the view objects from the controllers. The controllers determine the names of the views, and the actions, but do not access the view objects. So I assume this means there is no opportunity for the controller to setup handlers for events on the view.

    How would you allow a controller to observe key presses in a view using this framework?

    • Well, this is a pretty subjective and really depends on what your needs are and your actual thoughts of how the MVC pattern should be applied.

      If you are validating the text input and you are popping up an alert for some reason, you could say that this only has to do with the view so the logic could be placed in the view — on the flip side, you may think that any validation should be done in the controller so you send a request on each keystroke to an action and populate the ui based on the result of the action. I would prefer the latter.

      Anyway, I should add some modification so you could return any object from an action and only render the object when you call ‘return this.View()’

      In the meantime, in your view call ‘this.action(“Default”, {value : text.value, result : someBool })’ and do not return a value from the controller. This way you can use ‘someBool’ (or another value as your result) to modify the ui. Also, since you are not returning a value from the action it will not refresh the UI (like I said, I want to change this so the UI is only updated when you return a view).

      Another thing you may want to do is bind the textbox value to the value in the object you send to the validation action (utils.bind). This way you can modify the textbox inside the action.

  9. gautier says:

    HI,

    First,
    Tk u very much for this framework -> easy to use.

    Second,
    I would like to return my view (C -> V) in the function xhr.onreadystatechange.

    Somethink like this :
    xhr.onreadystatechange = function() {
    if (xhr.readyState == 4) {
    c.nameControler.view(‘view’, value)
    }
    };

    but i don’t succeed in

    Third,

    Sorry i just start in js and in your framework maybe my question is trivial.

    Tk u very much.

  10. Hendra says:

    Do you have a sample web sericve implementation that receives a token and extracts the claims data from it? I’m using the OAuth2 endpoint and can get the token and send it to a sericve, but I’m not sure how to use the token with the sericve to get claims.

    • Are you talking about a C# webservice or node? If it’s C# I am using WebAPI + OWIN from Nuget and it’s pretty straight forward. It will auto-generate some files where you can view the claims.

  11. florian says:

    i downloaded your version from the marketplace but i keep getting this error at runtime:

    [ERROR] Script Error = Result of expression ‘path.split’ [undefined] is not a function. at mvc.js (line 66).

    the version i get from your website right here works fine though

    any idea?

    thx!

  12. John Kalberer says:

    Yes I made a last minute change and missed that. I uploaded a 1.0.1 version today so simply try that tomorrow. If you don’t want to wait go into mvc.js and edit the “Mvc.start” method so it is “this.process.call(this, arguments);”

    I’m not at my desk right now but I believe that is the change needed.

  13. florian says:

    same error when i change it to
    this.process.call(this, arguments);

    but it works when i use route instead of arguments:
    this.process.call(this, route);

    or this works too
    this.process(route)

    since the start method is called with route as parameter not arguments



Pings to this post

  1. […] Kalberer gives his take on a Structured MVC for Appcelerator at his […]

  2. […] Hey, I just wanted to get it out there that my Appcelerator Mvc implementation is up in the Appcelerator marketplace. It has some bug fixes and improvements over the original example I gave here. […]

  3. Pacers…

    […]following are a few url links to webpages that we link to for the fact we believe they will be really worth checking out[…]…


Leave a Reply

Your email address will not be published. Required fields are marked *

Comment

You may use these tags : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>