Saturday, 27 February 2010

Ajax Forms with ASP.NET MVC and jQuery

This post uses jQuery, jquery.form.js, jquery.validate.js, ASP.NET MVC 2 and .NET 4.

I’ve been content loading my projects for a little while now using a graceful Ajax technique that combines jQuery with MVC PartialViews. Please see this post1 for more information.

This technique works fine with GET requests but I recently had a requirement for a form within some “content loaded” content. The whole point of content loading is that the HTML should be semantically correct and work without the need for JavaScript, so I obviously wanted to keep the form as clean as possible.

<div class="form-wrapper">
 <%= Html.ValidationSummary() %>
 <% using (Html.BeginForm("Edit", "Pages")) { %>
 <%= Html.EditorForModel() %>
 <input type="submit" name="submit" value="Submit" />
 <%= Html.Encode(TempData["Message"]) %>
 <% } %>
</div>

The example above is a very simple but incredibly scalable form thanks to EditorForModel, however notice that I’m not using any Ajax Extension Methods. I try to reduce dependence on the Microsoft JavaScript libraries whenever possible. This is not out of any particular dislike for those libraries, I just prefer jQuery and want to keep my file sizes down. The form above is placed in a PartialView, as per the fore mentioned Graceful Modals post, which is in term referenced by a parent View. We’ll call the View FormView and the PartialView _FormView.

We’ll also employ the Graceful Modals technique in the Controller actions, as seen below, by using AjaxView in place of View.

 // Edit
 public virtual ActionResult FormView(int id) {
  return AjaxView(_modelService.Load(id));
 }

 [HttpPost]
 public virtual ActionResult FormView(ModelOfChoice model) {
  if (!ModelState.IsValid)
   return AjaxView(model);

  // Save to Database
  _modelService.Save(model);
  TempData["Message"] = "Record Saved.";

  return AjaxRedirectToAction(MVC.Something.FormView(model.ID));
 }

A new method is used here called AjaxRedirectToAction, I’m still undecided on the best implementation, but a few signatures I’ve been playing around with can be seen below. You should also note that I try and create signatures to be compatible with T4MVC2 when possible.

 protected ActionResult AjaxRedirectToRoute(RouteValueDictionary routeValues, Func<ActionResult> ajaxCallback) {
  if (Request != null && Request.IsAjaxRequest())
   return ajaxCallback();

  return RedirectToRoute(routeValues);
 }

 protected ActionResult AjaxRedirectToRoute(RouteValueDictionary routeValues) {
  return AjaxRedirectToRoute(routeValues, () => { return Content("ok"); });
 }
 
 protected ActionResult AjaxRedirectToRoute(ActionResult result) {
  return AjaxRedirectToRoute(result.GetRouteValueDictionary(), () => { return AjaxView(); });
 }

The purpose of this method is to implement the Redirect-After-Post pattern for non JavaScript requests.

All that is left to do now is wire up the JavaScript. Firstly, I’m going to make slight change to jquery.form.js. The change deals with the over zealous caching by all versions of Internet Explorer, whereby IE doesn’t differentiate based on headers. By default Microsoft Ajax and jQuery use a header to signify that a request originates in JavaScript. When designing a graceful solution, you must accept that within the same session a user may access Actions with either a standard request or an Ajax request. If the user is exposed to a combination, we want to make sure the browser is displaying the correct output for the right request.

MVC makes allowances for the fact that not all environments allow custom headers to be sent with a request, by allowing the header value to be sent as a Query String parameter. IE does differentiate on Query Strings, so the change to jquery.form.js will add this parameter. The change is added at line 62.

 var ajaxified = url;
 if (ajaxified.indexOf("?") >= 0)
  ajaxified += "&";
 else ajaxified += "?";
 ajaxified += "X-Requested-With=XMLHttpRequest";

 url = ajaxified;

The last job is the wire up itself, which is an almost out-of-the-box implantation of jquery.validate.js and jquery.form.js. The code is below.

 $("form").validate({
  submitHandler: function(form) {
   $(form).ajaxSubmit({
    target: ".form-wrapper"
   });
  }
 });

At this point we should have a form that posts back to the server using Ajax or a standard page request, dependent on the existence of JavaScript. These forms should live quite happily within dynamically generated content and server rendered content.

A production implementation of this technique can be seen at http://21wappinglane.com.

  1. http://blog.dogma.co.uk/2010/01/graceful-modals-with-aspnet-mvc2-jquery.html
  2. http://aspnet.codeplex.com/wikipage?title=T4MVC

Sunday, 14 February 2010

Rolling with the Project Ball

A while ago I came across a post on Devirtuoso, on creating a 3D sphere using jQuery. There is fantastic demo on the post, but what really made it stand out was a lack of dependency on the canvas element, so it should be cross browser compatible. That is exceptional and when my colleagues found out about it, it was decided, this needs to power our new portfolio page.

“Okay”, I said. “That should be fine.” They do know how bad my maths is, don’t they?

The big idea. We are currently redeveloping the Ink Creations web site, the old site had been up for a few years and little had changed. We want the new site to be a reflection of the way we work/think with each page exploring a different aspect. Like a programmers playground, but more polished. One of these pages is our portfolio. We wanted to present our portfolio in an interesting new way while still remaining accessible. The 3D sphere gives us the interesting, but not so much the accessible. The sphere is comprised of plus signs that are generated in JavaScript, which are invisible to screen readers and search engines.

There are other issues with the sphere, in the context of our requirements:

  1. Each of the plus signs would need to be replaced with a unique portfolio image, but the rendering of the sphere allows for duplication of plus signs.
  2. The default behaviour of the sphere is to react to the position of the mouse, but to be constantly on the move. This makes tracking a particular plus sign very difficult. We would need our project images to be easily clicked.
  3. The plus signs are all the same solid colour, so there is no need to worry about the z-index of each element. With individual portfolio images, you don’t want the images at the back overlapping the ones at the front. No, no you don’t.

Lets deal with accessibility first. I still want to see a portfolio with JavaScript is switched off. The 3D sphere uses an unordered list which is good enough for me. So I start off by pre-rendering a list of each of our projects as an unordered list. Within the 3DEngine.js file I need to check for this and recreate the list if it doesn’t already exist.

//if there isn't a ul than it creates a list of +'s
if (container.children("ul").length === 0) {
	var $ul = $("<ul></ul>").appendTo(this.container);
	for (i = 0; i < this.pointsArray.length; i++) {
		var project = g_projects[idx];

		$ul.append("<li><img id=\"item" + i + "\" class=\"" + project.id + "\" src=\"/Image/Project/" + project.thumbnail.id + "?height=320&width=390\" /></li>");

		idx = nextIndex(idx, g_projects);
	}
}

I have created a global array of the projects called g_projects and have referenced it directly from 3DEngine.js. Now it is just a case of creating images instead of b elements. The pre-rendered ul list contains anchor tags as well, so the images are clickable. The fact that I haven’t done it in JavaScript is probably an oversight on my part. 

We are now accessible, with JavaScript switched off we get an unordered list of portfolio images. With JavaScript switch on the images form a spinning ball.

While we’re in 3DEngine.js, we’ll deal with the z-index.

currItem.style.height = (37.5 * currItem.scale) + "px";
currItem.style.width = (50 * currItem.scale) + "px";

$(currItem).css({ opacity: (currItem.scale - .5), zIndex: Math.ceil((currItem.scale - .5) * 100) });

Using the scale that has already been provided I’m adjusting the photo’s size, opacity and z-index. This should hopefully add to the illusion of depth.

Sphere.js is where I encountered some difficulty. As previously mentioned mathematics isn’t my strongest attribute. The Sphere objects constructor takes three parameters; radius, sides and numOfItems. These parameters make up the configuration of the resulting sphere and as such can’t be applied randomly, it would seem. For instance, increasing the numOfItems may well increase the number of portfolio images, but will not necessarily increase the complexity of the sphere. Additional images occupy the space of existing images leading to undesirable results.

I’m positive the relationship between the three parameters can be worked out mathematically, I just haven’t worked out that relationship yet. Until I do, my solution has been to control which images are created based on their coordinates. This will remove the chance of images occupying the same space, but does require a little fiddling (with radius and sides) as our portfolio increases. This isn’t a huge concern as I believe our current portfolio count is already testing the CPU usage or your average browser to it’s limits, but any help in determining the fore said relationship would be very much appreciated. The code for Sphere.js is below:

var Sphere = function (radius, sides, numOfItems) {
	var arr = new Array();
	var totalPerSide = Math.ceil(numOfItems / sides);
	var comparer = ["x", "y", "x"];

	for (var j = 1; j < (sides + 1); ++j) {
		for (var i = 0; i < totalPerSide; ++i) {
			if (this.pointsArray.length >= numOfItems)
				break;

			var angle = i * Math.PI * 2 / totalPerSide;
			var angleB = j * Math.PI * 2 / sides;

			var x = Math.sin(angle) * Math.sin(angleB) * radius;
			var y = Math.cos(angle) * Math.sin(angleB) * radius;
			var z = Math.cos(angleB) * radius;

			x = Math.round(x * 100) / 100;
			y = Math.round(y * 100) / 100;
			z = Math.round(z * 100) / 100;

			var points = { x: x, y: y, z: z };
			if (!arr.contains(points, comparer)) {
				arr.push(points);
				this.pointsArray.push(this.make3DPoint(x, y, z));
			}
		}
	}

	delete arr;
};

Sphere.prototype = new DisplayObject3D();

Array.prototype.unique = function () {
	var r = new Array();
	o: for (var i = 0, n = this.length; i < n; i++) {
		for (var x = 0, y = r.length; x < y; x++) {
			if (r[x] == this[i]) {
				continue o;
			}
		}
		r[r.length] = this[i];
	}
	return r;
};

Array.prototype.contains = function (obj, properties) {
	for (var i = 0, n = this.length; i < n; i++)
		if (_equals(this[i], obj, properties))
			return true;

	return false;
};

var _equals = function (a, b, properties) {
	if (!properties)
		return a == b;

	for (var i = 0; i < properties.length; i++)
		if (a[properties[i]] != b[properties[i]])
			return false;

	return true;
};

I believe this leaves us with the issue of actually selecting a project and clicking on the image for more information.

var g_portfolio = (function () {
	$(document).ready(function () {
		if (g_speed && g_speed.isSlow())
			return;

		var camera = new Camera3D();
		camera.init(0, 0, 0, 300);

		var container = $("#item");

		container.css({
			margin: "0 auto",
			top: "46%",
			position: "absolute",
			left: "50%"
		});

		container.find("ul").css({
			position: "static"
		});

		container.find("img").css({
			position: "absolute",
			zIndex: 100
		});

		container.find("div").remove();

		var item = new Object3D(container);
		var sphere = new Sphere(175, 13, _projectCount);
		item.addChild(sphere);

		var scene = new Scene3D();
		scene.addToScene(item);

		var mouseX = 600;
		var mouseY = 0;
		var offsetX = $("#item").offset().left;
		var offsetY = $("#item").offset().top;
		var speed = 15000;
		var moving = null;

		$("#controls").click(function (evt) {
			evt.preventDefault();

			if (moving)
				stopTracking();
			else startTracking();
		});

		$("#item a").click(function (evt) { evt.preventDefault(); });

		$("#item img")
				.hover(function () {
					stopTracking();

					var $this = $(this);
					$this.data("state.3d", $.extend({}, { height: $this.height(), width: $this.width(), opacity: $this.css("opacity") }, $this.position()));
					$this.animate({ height: "+=18px", width: "+=18px", opacity: 1, top: "-=9px", left: "-=9px" });
				}, function () {
					var $this = $(this);
					var state = $this.data("state.3d");
					$this.stop(false, true).css(state);

					startTracking();
				})
				.click(function (evt) {
					location.href = "/portfolio/carousel/" + $(this).attr("class");
				});

		var animateIt = function () {
			if (!moving)
				return;

			if (mouseX != undefined) {
				axisRotation.y += (mouseX) / speed
			}

			if (mouseY != undefined) {
				axisRotation.x -= mouseY / speed;
			}

			scene.renderCamera(camera);
		};

		var mouseMove = function (evt) {
			mouseX = evt.clientX - offsetX - (container.width() / 2);
			mouseY = evt.clientY - offsetY - (container.height() / 2);
		}

		var startTracking = function () {
			$(document).mousemove(mouseMove);
			moving = setInterval(animateIt, 20);
		};

		var stopTracking = function () {
			$(document).unbind("mousemove", mouseMove);
			clearInterval(moving);
			moving = null;
		};

		startTracking();

		var $img = container.find("img");
		var imgCount = $img.length;
		var overflow = imgCount - sphere.pointsArray.length;

		if (overflow > 0) {
			var offset = (imgCount - overflow);
			var margin = 20;
			var withMargin = $img.width() + margin;
			var left = -((withMargin * overflow) / 2) + (margin * 2);

			for (var i = 0; i < overflow; ++i) {
				$img.eq(offset + i).css({
					left: left,
					top: 0
				});
				left += withMargin;
			}
		}
	});
})();

In the code above, I have refined the initialisation of the sphere to allow for the addition of a stop/start button. The sphere pauses when the mouse moves over an image, the said image then slightly increases in size to give emphasis. The last section of the above example deals with the remaining projects that didn’t fit within the sphere. It’s not ideal, but after running a few scenarios, I’ve found that I’m not able to guarantee a sphere that will encompass all projects, sometimes there is a remainder. These projects are lined up in the centre of the sphere.

And that is the current incarnation of our portfolio page. The full site is still under development and I’m sure the portfolio section still has some cosmetic changes to come. My prediction is some sort of featured projects sphere with a link to a full list. Until that happens, all the code is available uncompressed on the portfolio, when finalised and we’ve optimised the performance reasons, I’ll endeavour post the source to a follow up post. Our portfolio page can be found http://inkdigitalagency.com/portfolio.

Wednesday, 3 February 2010

jQuery plug-in for summing attributes

I love functions like foreach() and map() as a way for quickly arrogating through jQuery objects. I had a requirement to create a similar function today. I’m sure it has been done before but I couldn’t a direct equivalent within jQuery itself.

The function is called sum() and takes one parameter called “callback” and is directly related to it’s namesake in the map() function. The purpose of the function is to sum an attribute of each of the given elements. The “callback” parameter is a function that allows you to select the element’s attribute to be summed.

The summing component of the plug-in was taken from a code snippet on DZone.

The complete code along with an example is listed below:

/// <reference path="jquery-1.3.2-vsdoc.js" />

(function ($) {
 if (!Array.prototype.sum)
  Array.prototype.sum = function () {
   for (var i = 0, sum = 0; i < this.length; sum += this[i++]);
   return sum;
  };

 $.fn.sum = function (callback) {
  return this.map(callback).get().sum();
 };
})(jQuery);
var heightOfFirstElement = $(".element").height();
var combinedHeightOfAllElements = $(".element").sum(function () { return $(this).height(); });

Twitter Updates