Friday, May 10, 2013

MVC or Not?

With Phresheez, I've noticed a pattern that comes up over and over and over again given that most Phresheez pages are not web pages, per se, but long lasting apps that auto-refresh content dynamically. I've tried to reconcile this with the MVC (model/view/controller) pattern which is quite common, but I've never been able to tell whether the way I think of things is the same or even close to the MVC pattern.
I'm thinking that they have similarities, but that they're not really the same as MVC as far as I understand it.

So here's what I saw coming up all the time with Phresheez.

  1. There's a dynamic piece of data that needs to be fetched from the server
  2. There's a piece of HTML content I generate from that data
  3. That content needs to be refreshed as new data comes into the server
  4. I want to use that content in multiple places, but they may want to be formatted for different uses
Typically, I'd start out with a monolithic piece of code that did all of things, but I'd almost always regret it as I'd have to extend or refactor the code when I wanted to use some or all of the widget in a different context.

The rest of this post is what I'm thinking the architecture should be.

Goals


First off, I guess I should state what I want to provide.

  • A data abstraction layer that takes care of all of the grotty details of network errors, retransmissions, refreshing, etc, etc. Before refactoring, this code was ~duplicated all over the place each with its own subtle set of bugs. I want just one set of bugs.
  • A cleanish separation between content formatting and container decoration. By container decorations, I mean things like menus/nav, titles, last updated, etc. 
  • I want to be able to facilitate mixing and matching different widgets. That is, I want to be able get content from widget A, widget B and widget C and mix and match them into a new super widget D.
  • A recognition that there's some interplay between some of the aspects and not go crazy trying to ferret out every soi-disant layering violation

Data Layer


The Data Layer is in charge of getting and refreshing data between the client and the server. Other services subscribe to the data layer to get and refresh data from the server. The data layer may or may not auto-refresh depending on settings, but it will only refresh if something is actively subscribed to it.

The Data Layer should also have error callbacks for when something goes wrong. The Data Layer must never affect the DOM in any way: it is up to its subscribers to determine what is displayed.

There may be many Data Layer objects running around with different parameters to the same underlying service. For example, you might have an object that fetches the user information for user1 and another that fetches it for user2. When the Data Layer publishes the data, it must be specific to its instance.

As an optimization, however, it would be good for the Data Layer instances to know about each other such that an instance that has the same url parameters as another will share the server's results with other Data Layer objects so as not to generate useless duplicated calls to the server. I suppose that one way to accomplish that would be for them to subscribe to each other, or maybe a global data cache layer on top of the server callbacks, but that's an implementation detail.

Container Layer


The container layer is the business end of this model. It alone is responsible for inserting/updating elements within the DOM. The normal data flow is that the Container Layer subscribes to some set of data layers (or not!) and upon a published update calls the necessary content binding objects to populate the container (normally a div, but it could be anything's innerHTML really). I'm going to say that it's also its job to deal with layout and general look and feel for the container since a container may contain many objects. So it produces HTML as well, but it's much more oriented at layout than content per se.

The container layer is normally the recipient of the Data Layer's publications. It is responsible for painting the placeholders when the data is not yet available (eg, loading spinners, etc), and coordinating when it's interacting with multiple Data Layers. The Container Layer is also responsible for unsubscribing to the data layer when it has lost focus, for example, to cut down on useless chatter to the server. 

Finally, the Container Layer is responsible for implementing any callbacks (eg, onclick) that the container content requires. Which is another way of saying that the Container Layer owns the interaction with the DOM.

Content Binding Layer


The content binding layer binds data from the server to html layout and content. It never interacts with the  DOM. It merely takes the data, typically provided by the data layer, but not necessarily exclusively and grinds out the content for that data. The Content Binding layer should be thought of as effectively "stateless" even though for efficiency the Content Binding Layer ought to provide one other service: sameness. That is, if it produces the same content that the caller was returned the last time, it should inform the caller. This is needed to eliminate useless redraws at the container layer which are annoying. Note that if the content layer needs to do extensive massaging of the dynamic data, it is not a layering violation to hold onto the dynamic data (or a massaged form of it) if it wants to do delta's of old and new datasets, etc. 

It could be argued that the sameness property ought to be up to the Container Layer, but I don't think so. The Container Layer is the one that knows which bits and piece of dynamic content its using, so it alone is the one that knows if they are the same, modulo comparing blobs of HTML at the Container Layer, which I think is bad practice.

Clicks and other Events


It's not entirely clear to me who should be generating the onclick='s (typically). In the MVC pattern, I'm pretty sure that's the job of the controller, which would be more or less equivalent to my Container Layer. But that would be rather messy in practice: if you're just producing a bunch of html in the Content Binding Layer, it's much easier to insert the onclick's inline with the generated HTML. The downside here is that at the very least the onclicks need to be parameterized with the instance they represent from the Container Layer (ie, this click is associated with this container instance). And of course, the click actions across different container layers may well be very different.

What I think is probably the best compromise is let the Content Binding Layer generate the onclicks, but with input from the Container Layer's state. This still runs afoul if you want something like

<a href=# onclick="st.mywidget.clickHandler('mike')">Click me!</a>

since it is the container layer's job to handle that click, so why should the Content Binding Layer be setting up its callback parameters? Suboptimal, but in practice it haven't noticed many instances where I have one use with a set of parameters and in another use with a different set of parameters... they almost always follow a similar pattern, though you may end up with the union of parameters the various uses require.

My guess is that not stressing about this fuzziness and the apparent layering violations is best. The object here is to get better code reuse and separation of function, but that doesn't mean that we have to solve world hunger as well.

Inserts/Updates


For insert/update type callbacks to the server, I think I would keep it completely as a property of the Container Layer. This is the flip side of the argument about clicks in the previous section: the Content Binding Layer is the one that generates the input's, checkboxes, etc, so why shouldn't it provide the interface to fetch their values? I dunno, because it's sort of fuzzy in the same way that clicks are fuzzy and it would probably be messy to have a grand unified theory here. So some give and take between the Container Layer and Content Binding Layer seems reasonable.

Likewise, I don't see any particular advantage to tasking the Data Layer with the ajax callbacks to the server to save/update state. The point of the Data Layer is to deal with multiple users of data, and keeping it refreshed. It doesn't need to be the singular owner of HttpXMLRequest as well.

So is this MVC?


So have I reinvented MVC or not? Maybe it is, or maybe I've extended it a little, or something. Or maybe it's all different. You tell me. Here's my take on what I've done vs. MVC:



Thursday, May 2, 2013

Something old, something new. Something borrowed...

So I have been searching for a way to do a color picker that works ok on mobile phones. Most of the color pickers out there expect that a) mouse over works and b) that your finger is a mouse pointer. Fails on both accounts. So I found a rustic html color picker at w3schools. It uses a gif for the background, and an imagemap which is very, very old school these days. The code to do it is pretty big and definitely ugly. So I decided to do run with it and make it all fashion forward 'n stuff.

The image map contains the coordinates of the polygons for all of the hexagons in the gif. So why not just use a canvas element and render it instead of snarfing up a big png/gif? So I grabbed their source, and a couple of emacs keyboard macros transmogrified the area tags and the corresponding color into a nice compact javascript array. From there it was easy: loop through the array to draw the hexagons, and in parallel create the html for the area tags. To get the image, you just call toDataURL('image/png') on the canvas, and it can be stuffed into an <img> tag.

Their picker took the selected color and sent it back to a server to show different shades of the base color. Yuck. Instead of that, I just converted the colors to hsv and created a set of variations by changing the s and v values. Simple, easy, and most importantly relatively easy to pick with fat fingers. Does it give you the ability to pick infinitely variations on a hue/saturation/luminance? No, but a) I didn't need that, and b) it would be very unwieldy for... fat fingers. Again.

The source and docs are available on google code.


Here's the source:


/*
 *   Copyright (c) May  2 06:46:44  MTCC
 * Author: Michael Thomas
 * Module: colormap.js
 * Created: Thu May  2 06:46:44 2013
 * Abstract:
 *      License: MIT
 *    color mapper
 */

/* Edit History: 
 */


function colormap (prefix, w) {
    this.prefix = prefix;
    this.can = document.createElement ('canvas');
    this.can.width = 234;   // original image dimensions 
    this.can.height = 199; 
    if (w)
 this.scale = w/this.can.width;
    else
 this.scale = 1;
    this.can.width *= this.scale;
    this.can.height *= this.scale;
}

colormap.prototype.render = function (color) {
    var ctx = this.can.getContext ('2d');    
    ctx.scale (this.scale, this.scale);
    html = '';
    html += '<map name="'+this.prefix+'.map">';
    for (var i in this.map) {
 var slot = this.map [i];
 html += '<area shape=poly onclick="'+this.prefix+'.select (\''+slot [1]+'\')" coords="';
 ctx.beginPath ();
 for (var j = 0;  j < slot[0].length; j += 2) {
     html += slot [0][j]*this.scale + ',';
     html += slot [0][j+1]*this.scale;
     if (j < slot [0].length-1)
  html += ',';
     if (j == 0)
  ctx.moveTo (slot [0][j], slot [0][j+1]);
     else
  ctx.lineTo (slot [0][j], slot [0][j+1]);      
 }
 ctx.lineTo (slot [0][0], slot [0][1]);      
 html += '"/>';
 ctx.closePath ();
 ctx.fillStyle = slot [1];
 ctx.fill ();
    }    
    html += '</map>';    
    html += x = '<br><div id="'+this.prefix+'.shades">'+this.shades (color)+'</div>';
    return {html:html, img: '<img src="'+this.can.toDataURL ('image/png')+'" usemap="#'+this.prefix+'.map">'};
};

colormap.prototype.select = function (color) {
    if (this.onselect)
 this.onselect (color);
    var el = document.getElementById (this.prefix+'.shades');
    reliableNewc (el, this.shades (color));
};

colormap.prototype.shades = function (color) {
    var rgb = new RGBColor (color);
    var hsv = rgb2hsv (rgb.r, rgb.g, rgb.b);
    var html = '';
    html += '<table align=center><tr>';
    for (var i = 0; i < 256; i += 16) {
 if (i == 128)
     html += '<tr>';
 if (i < 128)
     hsv.v = i;
 else
     hsv.s = i-128;
 var rgb2 = hsv2rgb (hsv.h, hsv.s, hsv.v);
 html += '<td><div onclick="'+this.prefix+'.select(\''+rgb2+'\')" style="position:relative; height:30px;width:30px; background:'+rgb2+'; border: 1px solid black"></div>';
    }
    html += '</table>';
    return html;
};

// scraped from the source of http://www.w3schools.com/tags/ref_colorpicker.asp

colormap.prototype.map = [
    [[171,180,180,184,180,195,171,199,162,195,162,184], '#993333'],
    [[153,180,162,184,162,195,153,199,144,195,144,184], '#800000'],
    [[135,180,144,184,144,195,135,199,126,195,126,184], '#990000'],
    [[117,180,126,184,126,195,117,199,108,195,108,184], '#993300'],
    [[99,180,108,184,108,195,99,199,90,195,90,184], '#CC3300'],
    [[81,180,90,184,90,195,81,199,72,195,72,184], '#996600'],
    [[63,180,72,184,72,195,63,199,54,195,54,184],'#663300'],
    [[180,165,189,169,189,180,180,184,171,180,171,169],'#990033'],
    [[162,165,171,169,171,180,162,184,153,180,153,169],'#CC0000'],
    [[144,165,153,169,153,180,144,184,135,180,135,169],'#FF0000'],
    [[126,165,135,169,135,180,126,184,117,180,117,169],'#FF3300'],
    [[108,165,117,169,117,180,108,184,99,180,99,169],'#CC6600'],
    [[90,165,99,169,99,180,90,184,81,180,81,169],'#FF9900'],
    [[72,165,81,169,81,180,72,184,63,180,63,169],'#CC9900'],
    [[54,165,63,169,63,180,54,184,45,180,45,169],'#996633'],
    [[189,150,198,154,198,165,189,169,180,165,180,154],'#660033'],
    [[171,150,180,154,180,165,171,169,162,165,162,154],'#CC0066'],
    [[153,150,162,154,162,165,153,169,144,165,144,154],'#FF5050'],
    [[135,150,144,154,144,165,135,169,126,165,126,154],'#FF6600'],
    [[117,150,126,154,126,165,117,169,108,165,108,154],'#FF9933'],
    [[99,150,108,154,108,165,99,169,90,165,90,154],'#FFCC00'],
    [[81,150,90,154,90,165,81,169,72,165,72,154],'#FFFF00'],
    [[63,150,72,154,72,165,63,169,54,165,54,154],'#CCCC00'],
    [[45,150,54,154,54,165,45,169,36,165,36,154],'#999966'],
    [[198,135,207,139,207,150,198,154,189,150,189,139],'#993366'],
    [[180,135,189,139,189,150,180,154,171,150,171,139],'#CC6699'],
    [[162,135,171,139,171,150,162,154,153,150,153,139],'#FF0066'],
    [[144,135,153,139,153,150,144,154,135,150,135,139],'#FF6666'],
    [[126,135,135,139,135,150,126,154,117,150,117,139],'#FF9966'],
    [[108,135,117,139,117,150,108,154,99,150,99,139],'#FFCC66'],
    [[90,135,99,139,99,150,90,154,81,150,81,139],'#FFFF66'],
    [[72,135,81,139,81,150,72,154,63,150,63,139],'#CCFF33'],
    [[54,135,63,139,63,150,54,154,45,150,45,139],'#99CC00'],
    [[36,135,45,139,45,150,36,154,27,150,27,139],'#666633'],
    [[207,120,216,124,216,135,207,139,198,135,198,124],'#990099'],
    [[189,120,198,124,198,135,189,139,180,135,180,124],'#CC3399'],
    [[171,120,180,124,180,135,171,139,162,135,162,124],'#FF3399'],
    [[153,120,162,124,162,135,153,139,144,135,144,124],'#FF6699'],
    [[135,120,144,124,144,135,135,139,126,135,126,124],'#FF9999'],
    [[117,120,126,124,126,135,117,139,108,135,108,124],'#FFCC99'],
    [[99,120,108,124,108,135,99,139,90,135,90,124],'#FFFF99'],
    [[81,120,90,124,90,135,81,139,72,135,72,124],'#CCFF66'],
    [[63,120,72,124,72,135,63,139,54,135,54,124],'#99FF33'],
    [[45,120,54,124,54,135,45,139,36,135,36,124],'#669900'],
    [[27,120,36,124,36,135,27,139,18,135,18,124],'#333300'],
    [[216,105,225,109,225,120,216,124,207,120,207,109],'#993399'],
    [[198,105,207,109,207,120,198,124,189,120,189,109],'#CC0099'],
    [[180,105,189,109,189,120,180,124,171,120,171,109],'#FF33CC'],
    [[162,105,171,109,171,120,162,124,153,120,153,109],'#FF66CC'],
    [[144,105,153,109,153,120,144,124,135,120,135,109],'#FF99CC'],
    [[126,105,135,109,135,120,126,124,117,120,117,109],'#FFCCCC'],
    [[108,105,117,109,117,120,108,124,99,120,99,109],'#FFFFCC'],
    [[90,105,99,109,99,120,90,124,81,120,81,109],'#CCFF99'],
    [[72,105,81,109,81,120,72,124,63,120,63,109],'#99FF66'],
    [[54,105,63,109,63,120,54,124,45,120,45,109],'#66FF33'],
    [[36,105,45,109,45,120,36,124,27,120,27,109],'#009900'],
    [[18,105,27,109,27,120,18,124,9,120,9,109],'#336600'],
    [[225,90,234,94,234,105,225,109,216,105,216,94],'#660066'],
    [[207,90,216,94,216,105,207,109,198,105,198,94],'#CC00CC'],
    [[189,90,198,94,198,105,189,109,180,105,180,94],'#FF00FF'],
    [[171,90,180,94,180,105,171,109,162,105,162,94],'#FF66FF'],
    [[153,90,162,94,162,105,153,109,144,105,144,94],'#FF99FF'],
    [[135,90,144,94,144,105,135,109,126,105,126,94],'#FFCCFF'],
    [[117,90,126,94,126,105,117,109,108,105,108,94],'#FFFFFF'],
    [[99,90,108,94,108,105,99,109,90,105,90,94],'#CCFFCC'],
    [[81,90,90,94,90,105,81,109,72,105,72,94],'#99FF99'],
    [[63,90,72,94,72,105,63,109,54,105,54,94],'#66FF66'],
    [[45,90,54,94,54,105,45,109,36,105,36,94],'#33CC33'],
    [[27,90,36,94,36,105,27,109,18,105,18,94],'#009933'],
    [[9,90,18,94,18,105,9,109,0,105,0,94],'#003300'],
    [[216,75,225,79,225,90,216,94,207,90,207,79],'#9900CC'],
    [[198,75,207,79,207,90,198,94,189,90,189,79],'#CC00FF'],
    [[180,75,189,79,189,90,180,94,171,90,171,79],'#CC33FF'],
    [[162,75,171,79,171,90,162,94,153,90,153,79],'#CC66FF'],
    [[144,75,153,79,153,90,144,94,135,90,135,79],'#CC99FF'],
    [[126,75,135,79,135,90,126,94,117,90,117,79],'#CCCCFF'],
    [[108,75,117,79,117,90,108,94,99,90,99,79],'#CCFFFF'],
    [[90,75,99,79,99,90,90,94,81,90,81,79],'#99FFCC'],
    [[72,75,81,79,81,90,72,94,63,90,63,79],'#66FF99'],
    [[54,75,63,79,63,90,54,94,45,90,45,79],'#00FF00'],
    [[36,75,45,79,45,90,36,94,27,90,27,79],'#00CC00'],
    [[18,75,27,79,27,90,18,94,9,90,9,79],'#006600'],
    [[207,60,216,64,216,75,207,79,198,75,198,64],'#9900FF'],
    [[189,60,198,64,198,75,189,79,180,75,180,64],'#9933FF'],
    [[171,60,180,64,180,75,171,79,162,75,162,64],'#9966FF'],
    [[153,60,162,64,162,75,153,79,144,75,144,64],'#9999FF'],
    [[135,60,144,64,144,75,135,79,126,75,126,64],'#99CCFF'],
    [[117,60,126,64,126,75,117,79,108,75,108,64],'#66CCFF'],
    [[99,60,108,64,108,75,99,79,90,75,90,64],'#66FFFF'],
    [[81,60,90,64,90,75,81,79,72,75,72,64],'#66FFCC'],
    [[63,60,72,64,72,75,63,79,54,75,54,64],'#00FF99'],
    [[45,60,54,64,54,75,45,79,36,75,36,64],'#00CC66'],
    [[27,60,36,64,36,75,27,79,18,75,18,64],'#339933'],
    [[198,45,207,49,207,60,198,64,189,60,189,49],'#6600CC'],
    [[180,45,189,49,189,60,180,64,171,60,171,49],'#6600FF'],
    [[162,45,171,49,171,60,162,64,153,60,153,49],'#6666FF'],
    [[144,45,153,49,153,60,144,64,135,60,135,49],'#6699FF'],
    [[126,45,135,49,135,60,126,64,117,60,117,49],'#3399FF'],
    [[108,45,117,49,117,60,108,64,99,60,99,49],'#33CCFF'],
    [[90,45,99,49,99,60,90,64,81,60,81,49],'#00FFFF'],
    [[72,45,81,49,81,60,72,64,63,60,63,49],'#00FFCC'],
    [[54,45,63,49,63,60,54,64,45,60,45,49],'#00CC99'],
    [[36,45,45,49,45,60,36,64,27,60,27,49],'#339966'],
    [[189,30,198,34,198,45,189,49,180,45,180,34],'#666699'],
    [[171,30,180,34,180,45,171,49,162,45,162,34],'#3333CC'],
    [[153,30,162,34,162,45,153,49,144,45,144,34],'#3366FF'],
    [[135,30,144,34,144,45,135,49,126,45,126,34],'#0066FF'],
    [[117,30,126,34,126,45,117,49,108,45,108,34],'#0099FF'],
    [[99,30,108,34,108,45,99,49,90,45,90,34],'#00CCFF'],
    [[81,30,90,34,90,45,81,49,72,45,72,34],'#33CCCC'],
    [[63,30,72,34,72,45,63,49,54,45,54,34],'#009999'],
    [[45,30,54,34,54,45,45,49,36,45,36,34],'#669999'],
    [[180,15,189,19,189,30,180,34,171,30,171,19],'#333399'],
    [[162,15,171,19,171,30,162,34,153,30,153,19],'#3333FF'],
    [[144,15,153,19,153,30,144,34,135,30,135,19],'#0000FF'],
    [[126,15,135,19,135,30,126,34,117,30,117,19],'#0033CC'],
    [[108,15,117,19,117,30,108,34,99,30,99,19],'#0066CC'],
    [[90,15,99,19,99,30,90,34,81,30,81,19],'#0099CC'],
    [[72,15,81,19,81,30,72,34,63,30,63,19],'#006699'],
    [[54,15,63,19,63,30,54,34,45,30,45,19],'#006666'],
    [[171,0,180,4,180,15,171,19,162,15,162,4],'#000066'],
    [[153,0,162,4,162,15,153,19,144,15,144,4],'#0000CC'],
    [[135,0,144,4,144,15,135,19,126,15,126,4],'#000099'],
    [[117,0,126,4,126,15,117,19,108,15,108,4],'#003399'],
    [[99,0,108,4,108,15,99,19,90,15,90,4],'#3366CC'],
    [[81,0,90,4,90,15,81,19,72,15,72,4],'#336699'],
    [[63,0,72,4,72,15,63,19,54,15,54,4],'#003366']
    ];