Friday, January 6, 2012

Model View Presenter Design Pattern for Application Craft

What I mean by these terms:
  • Presenter: Application Craft is the Presenter, it presents the UI Widgets 
  • Model: The object that contains data and has functions to support modification and access.
  • View: The object that interacts with the Presenter and exchanges data with the Model

This Todo list application has the following main objects:

  • ToDoView - A View object interacting with the "todo" UI Text widget.  It does not need a separate Model. 
  • ToDoList - The only Model. It stores the data, publishes modifications and reports on it's status by Publishing
  • ToDoListView - This is a View. It interacts with the Presenters UI repeatContainer and the text UI widget.  It receives Events from the User and from the Model, ToDoList.
  • Event - A object that Publishs.  For the communication between the Models and View, Events are used.








The layout of the project for reference.  It's a tableLayout containing a repeatContainer holding a Table of 3 fields, the checkbox, the todoText and the removeImage.
This function extends the TodoList and Event so they can Publish and Subscribe

The TodoList is init with the name of the HTML5 storage.

The loadAll on line 12 reads the storage and loads the Model.

BWH are my initials and provide a name space to avoid global garbage.

/**
 *  Initialize the Models and Views
 */
function init() {     
    snack.publisher(BWH.TodoList);
    snack.publisher(BWH.Event);
    
    BWH.TodoList.init('BWH.ToDo');
    BWH.TodoView.init();
    BWH.TodoListView.init();

    BWH.TodoList.loadAll();
}

My initials are BWH and I use them for a unique namespace to avoid garbage in the global memory. The Event class is used to communicate between the Model and Views for "loose coupling".


The TodoView is the text UI that allows for entry of data. It's main function is to detect the enter key and save the data.

On line 22 the data is Published for all Subscribers. This class doesn't know how the data will be handled or who will handle it.

Classes Subscribe for any Events in their init function on line 40 though this class doesn't have any special requirements.

TodoView is also responsible for controlling the display of the warning message on lines 20, 27 and 31.

 Note that there is no associated model - It publishes the channel "addTodo" on line 22 which will result in the TodoList Model object storing the data.

BWH = {};
/** Event object to publish events from AC Widgets
*/
BWH.Event = {};
/**
 * The TodoView interacts w/ the AC widget: text
 */
BWH.TodoView = {
    //text on data entry
    toBeDone: 'What needs to be done?',
    
    setText: function(val) {
        app.setValue("new-todo",val);     
    },    
    getText: function() {
        return app.getValue("new-todo");
    },
    saveTodoIfEnterKey: function(keyev, value) {
        if (keyev.keyCode == 13) { 
            app.visible("saveWarningMessage",false);
            app.setValue("new-todo",'');
            BWH.Event.publish("addTodo",[value]);
            return;
        }           
        var tooltip = app.w("saveWarningMessage");
        tooltip.base().fadeIn(1000);
        app.visible("saveWarningMessage",true);        
    },
    todoOnClick: function(mouse) {
        if (app.getValue("new-todo") === '') {
            app.visible("saveWarningMessage",false);
            app.setValue("new-todo",this.toBeDone);
        }
    },
    newTodoClicked: function(keyev) {
        if (app.getValue("new-todo") == this.toBeDone){
            app.setValue("new-todo","");
        } 
    },
    init: function(){
    }
};



This is the Model for the application. It contains the data in an array of todos (line 8) It's main responsibility is to provide access to this data and to Publish any changes that occur.



The add function creates a todo object on line 9, stores it in the array, publishes the event and saves it to local storage. The methods responsibilities are very clear and there is no "bleeding of concerns".



UpdateText function doesn't Publish the change as there was no business requirement. The remove function does Publish the row delete so the the UI can update.


The clearCompleted is called when the currently selected rows are cleared from the list. The process starts from the bottom of the array so as not to remove a element during traversal. It also publishes the removed elements so that the UI is kept in sync.


The loadAll function on line 31 read the store from localStorage using the name from the init function. It parses the store to re-constitute the array of objects. For each, it publishes an "add" Event so that the UI can be updated.


The status function filters out the remaining array of objects and publishes the counts so that the UI display can be updated. Notice that the function doesn't know nor care how this information is consumed by the Subscribers.



The save function does a JSON stringify on the array and saves to local Storage. It works in unison with the loadAll function.



On line 53, is the init function which initializes the name of the localStorage and subscribes to events. When a Event is received, a function in the class is called to process the event.



The TodoList Model subscribes for 4 Events that the application can through. There are other events that come in from the UI which are later.


Notice how this class only concerns itself with maintaininig the data and publishing the changes to it's state. It knows nothing about the state of the UI.


/** TodoList is the model for the TodoListView 
 * It contains an array of {text: value, done: value} objects
 * it publishes changes to it's state
 * It subscribes to events that modify it
 * This is what will be persisted
 */
BWH.TodoList = { 
    todos: [],
    add: function(text) {
       var todo = {'done':false, 'text':text};
       this.todos.push(todo);
       this.publish("add",[todo]);
       this.save();
    },
    updateText: function(row,text) {
        this.todos[row].text = text;
        this.save();
    },
    remove: function(row) {
        this.todos.splice(row,1);
        this.save();
        BWH.TodoList.publish("deleteRow",[row]);
    },
    setTodoDone: function(row,value)  {
        this.todos[row].done = value;
        this.save();
    },
    clearCompleted: function(value) {
        for (var row = this.todos.length -1; row>= 0; row--){
            if (this.todos[row].done) { 
                this.todos.splice(row,1);
                BWH.TodoList.publish("deleteRow",[row]);
            }         
        }    
        this.save();
    },
    loadAll: function() {
        var store = window.localStorage.getItem(this.name);
        this.todos = (store && JSON.parse(store)) || [];
        for (var row = 0; row < this.todos.length; row++) {
            this.publish("add",[this.todos[row]]);
        }
        this.status();
    },
    status: function() {
        var remaining = this.todos.filter(function(todo){
            if (!todo.done) {
                 return true;
            }
        });
        this.publish("remaining",[remaining.length]);
        this.publish("completed",[this.todos.length - remaining.length]);
    },
    save: function() {
        window.localStorage.setItem(this.name, JSON.stringify(this.todos));
        this.status();
    },
    init: function(name) {        
        this.name = name;
         BWH.Event.subscribe("row_checkbox_onChange",function(row,value) {
            BWH.TodoList.setTodoDone(row,value);
        });
        BWH.Event.subscribe("removeRow",function(row) {
            BWH.TodoList.remove(row);
        });
        BWH.Event.subscribe("updateText",function(row,todo) {
            BWH.TodoList.updateText(row);
        });
        BWH.Event.subscribe("addTodo",function(text) {
            BWH.TodoList.add(text);
        });

    }
};

The TodoListView interacts with the UI Presentation and Model. It also receives Events from the actions generated by the user, clicks, dblClick, etc.


The add function on line 15 loads the repeatContainer.


On line 24, when the removeImage icon is clicked, this class is called so that it can determine the row that owns the widget. Then it Publishes the removeRow Event passing the row index.


The function rowDbClick, on line 28, is a little tricky, at least for me. Here we're swapping out the current row with a text component so the user can edit the row in place.


The remaining and completed functions Subscribe to the Events 'remaining' and 'completed' on lines 82 and 85. These Events were published by the TodoList Model.


Starting on line 65, the saveTextIfEntryKey function swaps the UI text widget back with the containers row and Publishes that the row needs update.




/**
 * The TodoListView supports the AC widgets: repeatContainer, checkbox
 */
BWH.TodoListView = {
    //When not visible
    clear: "http://barton.applicationcraft.com/"
           + "service/Resources/9684296d-913f-4c36-b705-2cda8667905e.png",
    //On click it's enabled
    enabled: "http://barton.applicationcraft.com/"
           + "service/Resources/feeb9250-5ba0-477d-82d3-acb7e39f64b1.png",
    //On mouse-enter it's disabled
    disabled: "http://barton.applicationcraft.com/"
           + "service/Resources/6f1d1e28-288b-45d8-86c2-84a4fa40fdfb.png",

    add: function(todo) {
        app.w('repeatContainer').insertRow('bottom', 
             {"checkbox": todo.done, 
               "todoText": todo.text,
               "image": null});
    },
    deleteRow: function(row) {
        app.w('repeatContainer').deleteRow(row);
    },
    removeImageOnClick: function(mouseev){
        var rowIndex = _widget.parent().parent().parent().rowIndex(); 
        BWH.Event.publish("removeRow",[rowIndex]);
    },
    rowDbClick: function(mouse) {
        var rowIndex = _widget.parent().rowIndex();
        var row = app.w('repeatContainer').getRow(rowIndex);
        app.visible(row.w("aToDoRow"),false);
        app.visible(row.w("text"),true);
        app.setValue(row.w("text"),app.getValue(row.w("todoText")));     
    },
    //A row with a checkbox has changed
    checkboxOnChange: function(value) { 
        var txt = _widget.parent().parent().w("todoText");
        //Would like to do linethrough but style is
        //not support at this time in AC
        var font = app.getProperty(txt,"font");
        if (value) {
            font = font.replace("bold","italic");
        } else {
            font = font.replace("italic","bold");
        }
        app.setProperty(txt,"font",font);
        var rowIndex = _widget.parent().parent().parent().rowIndex(); 
        BWH.Event.publish("row_checkbox_onChange",[rowIndex,value]);
    },
    remaining: function(count) {
        app.setValue("itemsLeftCnt",count);
    },
    completed: function(marked){
        var txt = '';
        app.setValue("clearLbl","");
        app.setProperty("clearLbl","enabled",false);
        if (marked > 0) {
            txt = "Clear " + marked + " completed item";
            if (marked > 1) {
               txt += 's';
           }
           app.setValue("clearLbl",txt);
        }
    },
    saveTextIfEnterKey: function(keyev, value){ 
        if (keyev.keyCode == 13) {
            var rowIndex = _widget.parent().rowIndex();
            var row = app.w('repeatContainer').getRow(rowIndex);
            app.visible(row.w("aToDoRow"),true);
            app.visible(row.w("text"),false);    
            app.setValue(row.w("aToDoRow").w("todoText"),value);
            BWH.Event.publish("updateText",[rowIndex,value]);
         }
    },
    init: function() {
        BWH.TodoList.subscribe('add', function(todo) {
            BWH.TodoListView.add(todo);
        });
        BWH.TodoList.subscribe('deleteRow',function(row){
            BWH.TodoListView.deleteRow(row);
        });
        BWH.TodoList.subscribe('remaining',function(count){
            BWH.TodoListView.remaining(count);
        });
        BWH.TodoList.subscribe('completed',function(count){
            BWH.TodoListView.completed(count);
        });
       
    }  
};


Line 2 is getting a small library for the Publisher and Subscribe functionality. It is passed a function init that is called when the getScript function is complete. This is what starts the whole process. It's kind of like having the Application Start function.


The rest of the code here is just the functions that the Presentation layer provides. I initially started out having all the Event handlers Publish their event. But it didn't "smell" right. 

It was clear that the Event was specific to a specific object and there was no requirements to share the Events. 

So I changed that the Event handlers to directly call the right object.  If the business requirements are such, this can easily be transformed into Publish approach.




//ok - now start getting the include
$.getScript("http://barton-snack.googlecode.com/"
               + "svn/trunk/builds/snack-min.js", init);

/**
 * TodoList method
 */
function handler_clearLbl_onClick(mouseev){
    BWH.TodoList.clearCompleted(mouseev);
}
/**
 * TodoListView event handlers
 */
function handler_removeImage_onClick(mouseev){
    BWH.TodoListView.removeImageOnClick(mouseev);
}
function handler_text_onKeyUp(keyev, value){
    BWH.TodoListView.saveTextIfEnterKey(keyev,value);
}
function handler_aToDoRow_onDbClick(mouseev){
    BWH.TodoListView.rowDbClick(mouseev);
}
function handler_checkbox_onChange(value){
    BWH.TodoListView.checkboxOnChange(value);
}
/**
 *  TodoView event handlers
 */
function handler_todo_onClick(mouseev){
    BWH.TodoView.todoOnClick(mouseev);
}
function handler_new_todo_onClick(mouseev){ 
    BWH.TodoView.newTodoClicked(mouseev);
}
function handler_new_todo_onKeyUp(keyev, value){
    BWH.TodoView.saveTodoIfEnterKey(keyev,value); 
}
/**
 * Mouse enter/leave stuff
 */
function handler_aToDoRow_onMouseEnter(mouseev){
    var img = _widget.parent().w("removeImage");
    img.setData(BWH.TodoListView.enabled);
}
function handler_aToDoRow_onMouseLeave(mouseev){
    var img = _widget.parent().w("removeImage");
    img.setData(BWH.TodoListView.clear);
}
function handler_removeImage_onMouseEnter(mouseev){
    var img = _widget.parent().w("removeImage");
    img.setData(BWH.TodoListView.enabled);
}
function handler_removeImage_onMouseLeave(mouseev){
    var img = _widget.parent().w("removeImage");
    img.setData(BWH.TodoListView.clear);
}



In summary, this was a good exercise for me. I believe I have  garnered a crisper understanding of the roles and responsibilities the objects should have in AC.  I have reduced the "bleeding of concerns" issue, I have looser coupling , and the code appears to me to be more maintainable and resilient.  I'm pleased with my results and it brings clarity on how I plan to work within the AC development IDE.