PSEC: Saving Data

SUMMARY: I can create tasks and see them on the screen. But I don’t yet have a way of saving the data back to the server. That’s what I want to work on today.

What does saving data mean?

  • If I modify a VTask on-screen, I need to copy the data back into its underlying Task. There is a Task array, indexed by TaskID, that stores all the local data.

  • I also have to update the database back on the server with all the TaskIDs.

<

p>The first part is easy. The second part is harder, and will require writing an object relational data mapping layer. This is a new concept to me, since I’ve never programmed databases before. The gist is that the way I’m storing object data on the client is different than the way the database works with rows and tables. They’re incompatible, therefore translation (mapping) is necessary.

I’m not sure what the best way to approach this is, so I’ll just do it and see what I learn.

Step 1: Update the Tasks Database

Right now, I can click on a field and update it. I’ve implicitly made the choice that the application will be as “modeless” as possible, which means no explicit EDIT, CANCEL, SAVE buttons for each task.

Detecting when the text should be updated is easy enough: the blur handler fires when a text field loses focus. I already have an event bound to that to do a textfield / paragraph switcheroo, using jQuery.

Hm, now the problem is that the event callback receives an event object, and the this context is set to the html element that was bound. What I need to do is retrieve the vtask_id associated with this html element.

That’s a little tricky, because the callback function has no idea what its parent vtask is. It could walk up its parent chain to find the parents ID and then look it up in the VTask array, I suppose. It would be better if the value was stored, though, with the object. I was able to solve this, though, by using an anonymous function and the closure behavior of Javascript. The binding call used to look like this:

jobj.find(".editdesc").bind('click', dsCloseDesc);

The dsCloseDesc parameter was a function pointer to the ‘click’ event handler, and it gets passed an Event object. However, we can’t add our task_id information this way. We do THAT by changing the code to use an anonymous function:

jobj.find(".editdesc").bind('click', function(e) {
    VTASK.CloseDesc(e,vobj) 
});

The magic of closures is that the vobj reference will be maintained when this function call fires, even through vobj was defined only locally within the function that this statement appears.

I can rewrite VTASK.CloseDesc() as follows:

VTASK.CloseDesc = function (e, vobj) {
    var $ = jQuery;
    var ct = e.currentTarget;
    $(ct).hide();
    $(ct).siblings(".desc").show().text(ct.value);
    // also update data
    Controller.UpdateTask( { 'description':this.innerText, 'task_id':vobj.task_id } );
}

The e object is the Event object, which has the target of the event that we want to change. The vobj object is a VTask, which stores appearance-related state and the important task_id that it represents.

The code copies data between the textfield and the paragraph, changing the appearance of the task, and then calls the Controller to update the Model and do anything else that might be necessary through a call to Model.UpdateTask().

Here’s what Controller.UpdateTask() looks like:

Controller.UpdateTask = function ( values ) {
    Model.UpdateTask ( values );
}

And here’s what Model.UpdateTask() looks like:

Model.UpdateTask = function ( values ) {
    var updated = false;
    var task_id = values['task_id'];
    if (task_id==undefined) return false;
    var task = Tasks[task_id];
    var action = Actions[task.action_id];
    var desc = values['description'];
    if (desc) { action['description'] = desc; action.dirty = true; updated = true; } 
    return updated;
}

This code grabs the task_id from the array of values passed into the function. It grabs the correct object references out of the Tasks and Actions arrays, which represent the tables in the database. If an updateable value is found, it’s updated and the table row is marked as “dirty”; this will be used later when doing an update of necessary data back to the database.

NOTE: You might wonder why I don’t just call Model.UpdateTask directly. That’s because there may eventually be application-wide repercussions that need to be handled by the Controller, and technically only the Controller can talk directly to both the Model and the View. Remember: the controller is like “central dispatch”.

So that takes care of updating local data. In summary, the data that is in the visual objects are translated back into the local data store, which is organized like the tables in the database on the server.

Step 2. Writing back to the Database

The next step is to write the modified data back to the database, preferably only the data that actually changed. There are really only two tables that I’m storing locally: Tasks and Actions.

Writing a function called SaveDirtyTables() to walk the dirty values and send ’em over. The way it works is as follows:

  • Every time I change a value in the data store, its row is marked “dirty”.
  • When it’s time to save data, all the tasks and actions are walked for “dirty” flags. If a row is marked dirty, its id and values are pushed onto a new array as key-value pairs. There is one array for each database table that needs to be updated
  • A new CMD.SAVE command object is created, and the arrays of values are sent to the server.
  • A transaction is made to update all the updated rows at once. If it fails, the transaction fails and errors are reported back to the client.

Here’s the client-side Javascript:

    // write dirty tables 
    SaveDirtyTables: function () {

        var cmd = Dispatcher.GetNewCommandPacket(CMD.SAVE);
        var savedata = false; // if nothing is dirty, then don't save
        
        // prepare Tasks that have changed
        var tasks = [];
        for (var key in this.Tasks) {
            var task = this.Tasks[key];
            if (task.dirty) {
                tasks.push ( { 'id':key, 'data':{ 'name':task.name }} );
            }
        }
        // if there were any tasks, attach to command packet
        if (tasks.length>0) { cmd.tasks = tasks; savedata = true; }

        // prepare Actionsthat have changed
        var actions = [];
        for (var key in this.Actions) {
            var action = this.Actions[key];
            if (action.dirty) {
                actions.push( { 'id':key, 'data':{ 'action':action.action,'deliverable':action.deliverable,'description':action.description }} );
            }
        }
        // if there were any actions, attach to command packet
        if (actions.length>0) { cmd.actions = actions; savedata = true; }
        
        // is there data to save? If not, then quit
        if (!savedata) return;
        
        // Queue the command.
        Dispatcher.QueueCommand ( cmd, function ( response ) {
            // response callback handler
            if (response.success) {
                // clear all dirty flags
                DBG.Out("SaveDityTables: Success! Clearing dirty keys...");
                for (var key in Model.Tasks) Model.Tasks[key].dirty = false;
                for (var key in Model.Actions) Model.Actions[key].dirty = false;
            } else {
                DBG.Out("SaveDirtyTables: Error saving data");
                if (response.info) DBG.Out(response.info);
            }
        });
    }

And here’s the AJAX handler in PHP:

// save incoming data function dseah_ajax_sec_save() { global $wpdb; $table_prefix = $wpdb->prefix.’second_’; $rarr = dseah_receive_protocol(); try { // get additional parameters from server $tasks = (array) $_POST[‘tasks’]; $actions = (array) $_POST[‘actions’]; @mysql_query(“BEGIN”, $wpdb->dbh); $table = $table_prefix . “tasks”; $tasks_ok = true; if (count($tasks)>0) { $tasks_ok = true; foreach ($tasks as $key=>$value) { SDBG::out($value[‘id’].’: ‘.var_export($value[‘data’],true)); $updated = $wpdb->update( $table, $value[‘data’], array(‘id’=>$value[‘id’]), array(‘%s’,’%s’,’%s’) ); if ($updated===false) { throw new Exception(“dseah_ajax_sec_sav(): error updating tasks!”); // JS: tasks.push ( { ‘id’:key, ‘data’:{ ‘name’:task.name }} ); $rarr[‘info’] .= ‘task_id ‘.$value[‘id’].’ ‘; $tasks_ok = false; } } // repeat until all rows updated } $table = $table_prefix . “actions”; $actions_ok = true; if (count($actions)>0) { foreach ($actions as $key=>$value) { SDBG::out($value[‘id’].’: ‘.var_export($value[‘data’],true)); // JS: actions.push( { ‘id’:key, ‘data’:{ ‘action’:action.action,’deliverable’:action.deliverable,’description’:action.description }} ); $updated = $wpdb->update( $table, $value[‘data’], array(‘id’=>$value[‘id’]), array(‘%s’,’%s’,’%s’) ); if ($updated===false) { throw new Exception(“dseah_ajax_sec_sav(): error updating actions table!”); $rarr[‘info’] .= ‘action_id ‘.$value[‘id’].’ ‘; $actions_ok = false; } } // repeat until all rows updated } // transaction successful! if ($actions_ok & $tasks_ok) $rarr[‘success’]=true; @mysql_query(“COMMIT”, $wpdb->dbh); } catch ( Exception $e ) { // cancel transaction @mysql_query(“ROLLBACK”, $wpdb->dbh); SDBG::out(“error ” . $e->getMessage()); $rarr[‘success’] = false; $rarr[‘info’] = $e->getMessage(); } // end AJAX call properly dseah_return_protocol($rarr); } [/

Most of the difficulty I had in writing this was figuring out how to pass the rows of data so I could parse them in PHP, and how to detect empty arrays. Passing pure associative arrays with ids as the keys and an array of KVPs as the values seemed most straightforward, but phantom keys appeared on the PHP side. To avoid this, just passed regular arrays that contained associative arrays with ‘id’ and ‘values’ stuffed in them.

I need to make some kind of reference card for the differences between Javascript and PHP’s common array and object syntax. I keep forgetting which goes with which.

Data Saved!

Anyway, the code to save values to the database is now working. The approach I took provides a template for basic creation, saving, and loading of data. Woo hoo!

I’m STILL not done, though. For one thing, I need to be able to delete and reorder tasks. And I might have to think of undo functionality. I think I want to take a step back, though, and do a code review.