Harnessing the BackPack API - Part III

  This article is in series of BackPack API. It focuses on some of the underlying architecture of this application.
AngryPets.com :: Blog

Difficulty: Intermediate
Time Required: 1-3 hours
Cost: Free
Software: Visual Studio Express Editions,BackPack API
Hardware:
Download:

 

Stumbling Towards the Finish Line

In my previous two articles, I indicated that my focus on the BackPack API would span three articles. In preparing for this article it became apparent that I was either going to have to cram an incredible amount of info into this last article or just gloss over a number of key points without giving them the coverage that they deserved. Neither of those options made sense, so I've decided to add an extra article—which will allow us to focus on some of the underlying architecture of our application a bit more prior to wrapping everything up in the fourth, and final, article.

So What's Left?

Our main goal in examining the BackPack API was to create a Windows Forms application that effectively mimics BackPack functionality while connected to the Internet, and that also provides the ability, while offline, to queue modifications that can be marshaled up to the BackPack servers once connectivity has been restored. In other words, we want to be able to take our BackPack data offline—using XML to store state, modifications, and other needed info. Of course, when editing offline, it will be nice to know which nodes (or data points) have been modified offline, and are therefore out of synch with the server—or master data. To accomplish this, we can just take advantage of the fact that TreeView controls make it fairly easy to toggle an image associated with each node. Therefore, once a node becomes "dirty," it will be visually flagged as such—then, once the change has been successfully made on the server, the icon can be toggled back to its original image—letting us know that the modification is complete, and that BackPack now looks like our local copy of the data.

The final UI will look similar to the image below. A TreeView will allow us to drill into pages, and as we click on certain nodes, we'll have the ability to edit the corresponding data points. As you can see below, when a Task is selected, the Title and Completed status are made available off to the left to allow for editing.

Click here for larger image

(Click image to zoom)

If we make a change to the Task's title, and change the Completion status, as follows:

We'll want a visual indication that the node is in a non-synchronized or "dirty" state until it's been successfully synchronized with the server. Providing that indication will be as simple as providing an icon with an exclamation point and a pencil (if you've got good eyes) that indicates that the node, or the object it represents, is in the process of being modified.

Not until the modification is complete will the title of the node be updated to reflect the change, but the change will be made in our local copy of the data, and an operation to change the data on the BackPack servers will be dispatched asynchronously. In the following diagram, the flow would effectively be:

  1. User saves a change in the UI—which notifies the local data store.
  2. The local data store records the info, and marshals the change to the BackPack servers (meanwhile the icon/state of the node in the UI is still flagged as dirty).
  3. The BackPack server responds, and the local data store is notified of the outcome, whereupon the local data store can do any additional processing of its local data if needed to bring it into sync with the master data stored on the servers.
  4. Once the local data store gets done processing the notification, it, in turn, raises a notification to the UI, letting it know about the status of the operation.
  5. The UI processes the notification, and modifies the UI as needed.

But what if the application is offline?

Taking Our BackPack Off-Road

If the application is off-line, then the local data store isn't able to marshal the change up to the server, and won't be able to send back a notification to the UI that the change has been completed. Instead, the operation will be queued—and will be executed later, once connectivity is restored. Meanwhile, the "dirty" icon hangs out at the corresponding node on the TreeView control—indicating to the user that the underlying node data is unsynchronized with the data on the server. That's all fine and well—but what happens if the user closes the application, with operations pending? Obviously when they restart the application, they'll want to see which nodes are still out of sync, or pending modification. That's where things get fun—because our local data store will now not only need to persist commands to be marshaled against the server, but will also need to persist pending operations so that their state can be accurately reflected in the UI when the user reloads the application in offline mode.

The focus of this article, then, will be on the mechanism that we'll use to make this asynchronous, and potentially offline, communication between the UI, the local data store, and the remote BackPack servers possible. In this article we'll just work on the overall plumbing, and will focus on getting this process to work while online. Once the entire communication process is underway and working, making the whole thing work while offline isn't much more work—though we'll leave the offline details until the next article.

Taming the User Interface

One of the cool things about TreeView controls is that each corresponding TreeNode object has a Tag property—an object that acts as a data-slot into which we can cram information about the object the node represents. In our case, at the most basic level, the Tag property could therefore be used to flag whether a particular node represents a Page, a Note, a Task, or something else. More importantly, we could also use it to keep track of the id of the page, or note, etc. being represented. But rather than cram a bunch of sloppy data into the Tag property, I've opted to create a light-weight object called a ResourceDescriptor—which, well, describes a resource:

Having this descriptor available will make processing NodeClick events much easier—we can just interrogate the clicked node's Tag property to determine what kind of activity should be allowed, as follows:

Visual C#

private void treeView1_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e)
{
ResourceDescriptor rd = e.Node.Tag as ResourceDescriptor;
if(rd == null || this.IsLockedNode(rd))
{
this.ResetFormFields();
return;
}
this._currentResource = rd;
if (e.Button == MouseButtons.Left)
this.BindFormFields(rd);
else if (e.Button == MouseButtons.Right)
{
bool showMenu = false;
this.contextMenuNewTask.Visible = false;
this.contextMenuNewPage.Visible = false;
this.contextMenuNewNote.Visible = false;
switch (rd.ResourceType)
{
case ResourceType.PageList:
this.contextMenuNewPage.Visible = true;
showMenu = true;
break;
case ResourceType.NoteList:
this.contextMenuNewNote.Visible = true;
showMenu = true;
break;
case ResourceType.TaskList:
this.contextMenuNewTask.Visible = true;
showMenu = true;
break;
default:
break;
}

if (showMenu)
{
this.treeView1.ContextMenuStrip.Show();
}
}
}

Visual Basic

Private Sub treeView1_NodeMouseClick(ByVal sender As Object, 
ByVal e As TreeNodeMouseClickEventArgs) Handles TreeView1.NodeMouseClick
Dim rd As ResourceDescriptor = CType(e.Node.Tag, ResourceDescriptor)
If ((rd Is Nothing) OrElse Me.IsLockedNode(rd)) Then
Me.ResetFormFields()
Return
End If
Me._currentResource = rd
If (e.Button = MouseButtons.Left) Then
Me.BindFormFields(rd)
ElseIf (e.Button = MouseButtons.Right) Then
Dim showMenu As Boolean = False
Me.contextMenuNewPage.Visible = False
Me.contextMenuNewTask.Visible = False
Me.contextMenuNewNote.Visible = False
Select Case (rd.ResourceType)
Case ResourceType.PageList
Me.contextMenuNewPage.Visible = True
showMenu = True
Case ResourceType.NoteList
Me.contextMenuNewNote.Visible = True
showMenu = True
Case ResourceType.TaskList
Me.contextMenuNewTask.Visible = True
showMenu = True
End Select

If showMenu Then
Me.treeView1.ContextMenuStrip.Show()
End If
End If
End Sub

As the code above indicates, if the node was right-clicked and it is a suitable container for child nodes (such as a PageList, NoteList, or TaskList), then a corresponding context menu item can be toggled and made visible. If the node represents a piece of data that can be modified, the ResourceDescriptor is dispatched to a method that will be tasked with altering the user interface and wiring it up to allow the end-user to edit the resource in question, as seen in the following code:

Visual C#

private void BindFormFields(ResourceDescriptor descriptor)
{
this.ResetFormFields();

this._currentlyEditedResource = this._currentResource;
switch (descriptor.ResourceType)
{
case ResourceType.Page:
this.BindFormFormToEditPage(descriptor.PageId);
break;
case ResourceType.Link:
break;
case ResourceType.Note:
this.BindFormToEditNote(descriptor.PageId, descriptor.ChildId);
break;
case ResourceType.Tag:
break;
case ResourceType.Task:
this.BindFormToEditTask(descriptor.PageId, descriptor.ChildId);
break;
default:
break;
}
}

Visual Basic

Private Sub BindFormFields(ByVal descriptor As ResourceDescriptor)
Me.ResetFormFields()

Me._currentlyEditedResource = Me._currentResource
Select Case (descriptor.ResourceType)
Case ResourceType.Page
Me.BindFormFormToEditPage(descriptor.PageId)
Case ResourceType.Link
Case ResourceType.Note
Me.BindFormToEditNote(descriptor.PageId, descriptor.ChildId)
Case ResourceType.Tag
Case ResourceType.Task
Me.BindFormToEditTask(descriptor.PageId, descriptor.ChildId)
End Select
End Sub

This simple bit of code just evaluates the type of object being represented, and makes a subsequent dispatch into a method that will look up the object to edit, and bind its properties to the Winform. If the user wanted to edit a Page object, the following code would be executed:

Visual C#

private void BindFormFormToEditPage(string pageId)
{
this.ResetFormFields();

// toggle field values..
this.txtTitle.Visible = true;
this.txtBody.Visible = true;
this.lblTitle.Text = "Page Title";
this.lblTitle.Visible = true;
this.lblBody.Text = "Page Body";
this.lblBody.Visible = true;

if (pageId != null)
{
Page page = this._pageManager.GetPageById(pageId);
this.txtTitle.Text = page.Title;
this.txtBody.Text = page.Body;
}

this.btnSave.Text = "Save Page";
this.btnSave.Visible = true;

this.txtTitle.Focus();
}

Visual Basic

Private Sub BindFormFormToEditPage(ByVal pageId As String)
Me.ResetFormFields()

' toggle field values..
Me.txtTitle.Visible = True
Me.txtBody.Visible = True
Me.lblTitle.Text = "Page Title"
Me.lblTitle.Visible = True
Me.lblBody.Text = "Page Body"
Me.lblBody.Visible = True

If (Not (pageId) Is Nothing) Then
Dim page As Page = Me._pageManager.GetPageById(pageId)
Me.txtTitle.Text = page.Title
Me.txtBody.Text = page.Body
End If

Me.btnSave.Text = "Save Page"
Me.btnSave.Visible = True

Me.txtTitle.Focus()
End Sub

If the pageId is null, then blank fields for the Title and Body will be displayed in the Winform. If the pageId isn't null, the page represented by the node will be loaded from the PageManager (our local data store), and the page's title and body will be bound to the Winform for editing.

As you can see, ResourceDescriptors can be quite handy. They not only provide an easy way to determine what kind of an object a particular node represents—but they can also be used to painlessly retrieve that object from the PageManager which serves as our local data store. And, while it's nice to be able to use a Node to look up its corresponding object, we'll eventually need that 'lookup' process to be a two-way street. In other words, we'll want and need the ability to find the TreeNode that represents a given piece of BackPack data. The easiest way to do this is to bind the ResourceDescriptor's "signature" (its overridden .ToString() output) into a hash table with the node in question as nodes are initially created and bound to the Winform when pages are loaded. The following helper function handles the details for us:

Visual C#

private void RegisterNode(TreeNode node)
{
ResourceDescriptor rd = node.Tag as ResourceDescriptor;
if (rd != null)
this._nodeLookups.Add(rd.ToString(), node);
}

Visual Basic

Private Sub RegisterNode(ByVal node As TreeNode)
Dim rd As ResourceDescriptor = CType(node.Tag, ResourceDescriptor)
If (Not (rd) Is Nothing) Then
Me._nodeLookups.Add(rd.ToString, node)
End If
End Sub

With this simple lookup in place, the UI can use a notification from the data store about a particular data point being updated to find the node that represents the modified data and refresh, or update, the UI as needed (to remove the "dirty" icon, and other things).

ResourceDescriptors Go on a World Tour

In addition to allowing simple access to information about the object that a particular node represents, a ResourceDescriptor can also be used as part of a state object that can be sent clear into the BackPackGateway object, and returned back to the user interface as part of an accompanying EventArgs class announcing the completion of a remotely executed operation having completed. But, rather than pass it in all by its lonesome, we'll actually end up bundling it into a PendingOperation object—which will not only describe the piece of data being modified, but can also be used to describe the operation being attempted.

Having such an object will make "bouncing" data back and forth between our tiers much easier to manage. It will also provide an excellent way to serialize pending operations—which we'll look at in the next article.

The New Face of Modifications

In the first article on the BackPack API, we looked at how to translate user input into dynamically generated XML that could then be sent to the BackPack servers. Now that we have an object model in place (created in the second BackPack API article), and a better UI, as well as accompanying "descriptors" to describe the objects and operations being attempted, the flow for modifying data has changed a good deal. For example, consider the code below which handles the assembly of user input to save a modified (or newly created) page:

Visual C#

private void SavePage(ResourceDescriptor descriptor)
{
// TODO: validate input:
string newTitle = this.txtTitle.Text.Trim();
string newBody = this.txtBody.Text.Trim();

if (descriptor.PageId == null)
{
string tempId = this._pageManager.CreatePage(newTitle, newBody);

ResourceDescriptor newPage = new ResourceDescriptor(tempId, null, ResourceType.Page);
this.LockNode(newPage);
this.FlagNodeAsDirty(newPage, OperationType.Create);
}
else
{
Page page = this._pageManager.GetPageById(descriptor.PageId);

if (newTitle != page.Title)
{
page.UpdateTitle(newTitle);
this.FlagNodeAsDirty(descriptor, OperationType.Update);
}

if (newBody != page.Body)
{
page.UpdateBody(newBody);
this.FlagNodeAsDirty(descriptor, OperationType.Update);
}
}
}

Visual Basic

Private Sub SavePage(ByVal descriptor As ResourceDescriptor)
' TODO: validate input:
Dim newTitle As String = Me.txtTitle.Text.Trim
Dim newBody As String = Me.txtBody.Text.Trim

If (descriptor.PageId = Nothing) Then
Dim tempId As String = Me._pageManager.CreatePage(newTitle,
newBody)
Dim newPage As ResourceDescriptor = New
ResourceDescriptor(tempId, Nothing, ResourceType.Page)
Me.LockNode(newPage)
Me.FlagNodeAsDirty(newPage, OperationType.Create)
Else
Dim page As Page =
Me._pageManager.GetPageById(descriptor.PageId)

If (newTitle <> page.Title) Then
page.UpdateTitle(newTitle)
Me.FlagNodeAsDirty(descriptor, OperationType.Update)
End If

If (newBody <> page.Body) Then
page.UpdateBody(newBody)
Me.FlagNodeAsDirty(descriptor, OperationType.Update)
End If
End If
End Sub

Note that a ResourceDescriptor is passed into this method as the only argument—this descriptor is the Tag corresponding to the last TreeNode to have been place in edit mode. Having the descriptor available in this method allows us to fetch a copy of the underlying object being modified and allows us to compare it to the currently attempted changes. Some simple logic determines if the requested operation represents the modification of an existing page, or the attempt to create a new page, and handles bundling up the corresponding user input as needed. Once that's complete, the method then dispatches the user data to the appropriate BackPack object for processing (if it's a page, the page exposes methods for modifying its Title and Body, and if a new page is requested, the PageManager exposes a method for the creation of a new page). Once the actual operation to make the change has been routed to the appropriate object, there remains the task of toggling the node's icon to indicate that the underlying data is now out of sync. The "toggling" process is handled by a simple helper method that flags the node as dirty by changing its icon, and registering the node in a hash table that keeps a reference count to the current number of operations queued against the given node. If the operation involves the creation of a new object, then, in addition to being flagged as pending, the node is also locked—meaning that attempts to edit it (or add children) will be blocked—which needs to be the case, as we don't yet have an Id to use for modifying the object on the BackPack servers.

Logic for translating user input into XML and for generating the URL to access on the servers remains largely the same, as can be seen in the following code that handles the creation of a new page on the PageManager.

Visual C#

public string CreatePage(string title, string body)
{
Page page = new Page(title, body);
page.Id = PageManager.GenerateTemporaryObjectId(typeof(Page));

this.AddPage(page);

XmlDocument args = new XmlDocument();
XmlElement pageNode = args.CreateElement("page");
XmlElement titleNode = args.CreateElement("title");
titleNode.InnerText = title;
XmlElement description = args.CreateElement("description");
description.InnerText = body;

pageNode.AppendChild(titleNode);
pageNode.AppendChild(description);

ResourceDescriptor rd = new ResourceDescriptor(page.Id, null, ResourceType.Page);
PendingOperation op = new PendingOperation(rd, OperationType.Create, null);

string methodUrl = "/ws/pages/new";

this.InvokeOperation(methodUrl, pageNode, op);
return page.Id;
}

Visual Basic


Public Function CreatePage(ByVal title As String, ByVal body As String) 
As String
Dim page As Page = New Page(title, body)
page.Id = PageManager.GenerateTemporaryObjectId(GetType(Page))

Me.AddPage(page)

Dim args As XmlDocument = New XmlDocument
Dim pageNode As XmlElement = args.CreateElement("page")
Dim titleNode As XmlElement = args.CreateElement("title")
titleNode.InnerText = title
Dim description As XmlElement = args.CreateElement("description")
description.InnerText = body

pageNode.AppendChild(titleNode)
pageNode.AppendChild(description)

Dim rd As ResourceDescriptor = New ResourceDescriptor(page.Id,
Nothing, ResourceType.Page)
Dim op As PendingOperation = New PendingOperation(rd,
OperationType.Create, Nothing)

Dim methodUrl As String = "/ws/pages/new"

Me.InvokeOperation(methodUrl, pageNode, op)
Return page.Id
End Function

The big difference is the creation of a ResourceDescriptor to describe the object being passed for modification out to the BackPack servers (I could have just passed in the existing ResourceDescriptor from the UI, but I didn't want them coupled that closely), as well as the creation of a PendingOperation object used to describe the operation being attempted. Note, too, that a call to this.AddPage() is also made in this method—before we even attempt to send our change to the BackPack Servers. This means that our local copy of the BackPack data has already been updated as if the operation had successfully completed. If the attempted operation fails we can rollback by interrogating the PendingOperation details and using the previous version of the object—if one was available (the third argument for creating a new PendingOperation is a slot for an object representing the previous 'version' of the object that is being modified).

When the operation is invoked, and routed to the BackPackGateway, the gateway effectively just ignores the PendingOperation object sent in, and merely attaches it to the EventArgs object used to notify the page manager that the operation has completed. The PageManager handles the event with the GatewayResponded method:

Visual C#

public void GatewayResponded(object sender, RemoteOperationCompleteEventArgs e)
{
// handle the event (details covered below)
}

Visual Basic

Public Sub GatewayResponded(ByVal sender As Object,
ByVal e As RemoteOperationCompleteEventArgs)
'handle the event (details covered below)
End Sub

The first thing that this method does is evaluate whether or not an error occurred—and then handles it accordingly (we'll cover rollbacks and other issues in the next article):

Visual C#   

    OperationCompleteEventArgs eventArgs = null;
if (e.OperationStatus == CompletionStatus.Fail)
{
if (e.PendingOperation.OperationType == OperationType.Read)
throw new Exception("Error Reading Data From Server", e.Exception);

eventArgs = new OperationCompleteEventArgs(
e.PendingOperation, CompletionStatus.Fail, e.Exception);
}
if (eventArgs != null)
this.OnDataOperationComplete(eventArgs);

Visual Basic 

    Dim eventArgs As OperationCompleteEventArgs = Nothing
If (e.OperationStatus = CompletionStatus.Fail) Then
If (e.PendingOperation.OperationType = OperationType.Read) Then
Throw New Exception("Error Reading Data From Server",
e.Exception)
End If

eventArgs = New OperationCompleteEventArgs(e.PendingOperation,
CompletionStatus.Fail, e.Exception)
End If
If (Not (eventArgs) Is Nothing) Then
Me.OnDataOperationComplete(eventArgs)
End If

If there were no errors detected, the PageManager then needs to evaluate the operation type and do any additional processing necessary to bring the local data store into sync with the BackPack server—such as swapping out the tempId with the newId returned by the BackPack Servers in the case of objects that were successfully created, as follows:

Visual C#   

    else
{
// determine if extra processing is required and handle it if needed.
switch (e.PendingOperation.OperationType)
{
case OperationType.Read:
if (e.PendingOperation.Descriptor.ResourceType == ResourceType.PageList)
this.PagesListed(e.ResponseText);
else if (e.PendingOperation.Descriptor.ResourceType == ResourceType.Page)
this.SinglePageReturned(e.ResponseText);
// this operation doesn't need to be announced to the UI (we're done)
break;
case OperationType.Create:
this.ReplaceTempIdWithPermanentId(e.PendingOperation, e.ResponseText);

eventArgs = new OperationCompleteEventArgs(e.PendingOperation);
break;
case OperationType.Remove:
// covered in the next article
break;
default:
// just let the UI know to unbind/etc the matching node:
eventArgs = new OperationCompleteEventArgs(e.PendingOperation);
break;
}
}

// remove the operation from the list of pending operations so that it won't be
//serialized upon form close.
if (this._pendingOperations.Contains(e.PendingOperation))
this._pendingOperations.Remove(e.PendingOperation);

// notify the UI/Winform if needed:
if (eventArgs != null)
this.OnDataOperationComplete(eventArgs);

Visual Basic   

     Else
' determine if extra processing is required and handle it if
' needed.
Select Case (e.PendingOperation.OperationType)
Case OperationType.Read
If (e.PendingOperation.Descriptor.ResourceType =
ResourceType.PageList) Then
Me.PagesListed(e.ResponseText)
ElseIf (e.PendingOperation.Descriptor.ResourceType =
ResourceType.Page) Then
Me.SinglePageReturned(e.ResponseText)
End If
' this operation doesn't need to be announced to the UI
' (we're done)
Case OperationType.Create
Me.ReplaceTempIdWithPermanentId(e.PendingOperation,
e.ResponseText)

eventArgs =
New OperationCompleteEventArgs(e.PendingOperation)
Case OperationType.Remove
' covered in the next article
Case Else
eventArgs =
New OperationCompleteEventArgs(e.PendingOperation)
End Select
End If

' remove the operation from the list of pending operations so that it
' won't be serialized upon form close.
If Me._pendingOperations.Contains(e.PendingOperation) Then
Me._pendingOperations.Remove(e.PendingOperation)
End If

' notify the UI/winform if needed:
If (Not (eventArgs) Is Nothing) Then
Me.OnDataOperationComplete(eventArgs)
End If

In addition to any local processing needed, the PageManager also removes the PendingOperation object from the internal list it is using to track pending operations (so that if the application is persisted, this operation won't end up being flagged as an artifact). Once the PageManager is done with processing, it, in turn, raises an event notifying the UI of the completed operation—and the UI can then respond with logic designed to bring the state of the UI into sync with the local data store (which is now synchronized with the BackPack servers). Think of it sort of as a ripple effect: the UI plunks data into the data store (PageManager) which causes it to notify the remote servers—and once the remote operation is complete, the PageManager is notified, and it ultimately notifies the UI. The code for handling that final notification, in the UI, is as follows:

Visual C#

private void DataOperationComplete(object sender, OperationCompleteEventArgs e)
{
if (e.OperationStatus == CompletionStatus.Fail)
{
if (e.Operation.OperationType == OperationType.Read)
{
string inner = string.Empty;
if (e.Exception != null)
inner = System.Environment.NewLine + e.Exception.Message;
MessageBox.Show("Error Reading Pages From the Server.");
}
else
{
// TODO:
// rollback the node to it's previous state.
}
}
else
{
if (e.Operation.OperationType == OperationType.Create)
{
ResourceDescriptor replacement = null;
TreeNode modified =
this._nodeLookups[e.Operation.Descriptor.ToString()]
as TreeNode;
if (modified != null)
{
string newId = (string)e.Operation.State;

if (e.Operation.Descriptor.ResourceType == ResourceType.Page)
{
replacement =
new ResourceDescriptor(newId, null, ResourceType.Page);
}
else
{
replacement = new ResourceDescriptor(
e.Operation.Descriptor.PageId, newId,
e.Operation.Descriptor.ResourceType);
}


lock (this._nodeSync)
{
this._nodeLookups.Remove(e.Operation.Descriptor.ToString());
this._nodeLookups.Add(replacement.ToString(), modified);

// now modify the pending nodes:
int count =
(int)this._pendingNodes[e.Operation.Descriptor.ToString()];
this._pendingNodes.Remove(e.Operation.Descriptor.ToString());
this._pendingNodes.Add(replacement.ToString(), count);
}
this.BeginInvoke(new TreeNodeTagBinder(this.BindReplacementTag),
new object[] { modified, e.Operation.Descriptor, replacement });
}
this.UnlockNode(e.Operation.Descriptor);
this.UnFlagNode(replacement);
}
else
{
this.UnFlagNode(e.Operation.Descriptor);
}
}
}

Visual Basic

Private Sub DataOperationComplete(ByVal sender As Object, _
ByVal e As OperationCompleteEventArgs)
If (e.OperationStatus = CompletionStatus.Fail) Then
If (e.Operation.OperationType = OperationType.Read) Then
Dim inner As String = String.Empty
If (Not (e.Exception) Is Nothing) Then
inner = (System.Environment.NewLine + e.Exception.Message)
End If
MessageBox.Show("Error Reading Pages From the Server.")
Else
' TODO:
' rollback the node to it's previous state.
End If
ElseIf (e.Operation.OperationType = OperationType.Create) Then
Dim replacement As ResourceDescriptor = Nothing
Dim modified As TreeNode =
CType(Me._nodeLookups(e.Operation.Descriptor.ToString),
TreeNode)
If (Not (modified) Is Nothing) Then
Dim newId As String = CType(e.Operation.State, String)

If (e.Operation.Descriptor.ResourceType =
ResourceType.Page) Then
replacement = New ResourceDescriptor(newId, Nothing,
ResourceType.Page)
Else
replacement = New
ResourceDescriptor(e.Operation.Descriptor.PageId,
newId, e.Operation.Descriptor.ResourceType)
End If

modified.Tag = replacement
SyncLock Me._nodeSync

Me._nodeLookups.Remove(e.Operation.Descriptor.ToString)
Me._nodeLookups.Add(replacement.ToString, modified)
' now modify the pending nodes:
Dim count As Integer =
CType(Me._pendingNodes(e.Operation.Descriptor.ToString),
Integer)

Me._pendingNodes.Remove(e.Operation.Descriptor.ToString)
Me._pendingNodes.Add(replacement.ToString, count)
End SyncLock
Me.BeginInvoke(New TreeNodeTagBinder(
AddressOf Me.BindReplacementTag), New Object() {
modified, e.Operation.Descriptor, replacement})

End If
Me.UnlockNode(e.Operation.Descriptor)
Me.UnFlagNode(replacement)
Else
Me.UnFlagNode(e.Operation.Descriptor)
End If
End Sub

As you can see, that's a fairly hefty amount of code—and maybe it will deserve a refactoring in the final version of the application. The flow, however, isn't actually as complex as it looks at first blush. First the method checks to see if the operation failed remotely—in which case it will handle the rollback needed to put the UI back in sync with the BackPack servers. If the operation was successfully completed, things are very straight-forward UNLESS the operation was the creation of a new object. If it wasn't the creation of a new object, the descriptor accompanying the operation is handed off to a helper method that will un-toggle the "dirty" icon for the node in question. The helper method will also use the descriptor to "reach back in" to the underlying data store (where data is now synchronized with the servers) and grab the new title of the node, if there is one. It can then use the updated data to refresh the Node's title as needed.

If the pending operation was for the creation of an object, things are a bit messy—but effectively just involve replacing the tempId used as a placeholder with the actual Id of the object as returned from BackPack. Additional work is then needed to make sure references to the temporary node are replaced with references to the node with its new Id—so that modifications can be made, and children can be added.

Are We There Yet?

We're not quite there yet. But we've covered a decoupled way of marshalling data back and forth between our various tiers, which accomplishes two things: 1) it will let us provide end users with a visual indicator that their data is out of sync, and 2) because of the disconnected nature of everything, we'll be able to easily manage taking our application offline in the next article by persisting the state of our objects, as well as persisting information about pending operations.

We still have a way to go yet, but for now you can take the current implementation for a test drive. You won't be able to delete objects with the application in its present state, but you will be able to modify pages, tasks, and notes—as well as create new ones by right-clicking the appropriate nodes. It may not seem like much to the uninitiated, but once you know what's going on under the covers, it's pretty cool to watch in action. Once you've done that, try tracing the flow of the various messages throughout the application, and see if you can spot the areas where serializing our application and its state make sense. Once you've found those, think about how you'd go about serializing them—we've covered almost all of the necessary information, from an XML standpoint, in previous articles—so there really won't be too much for us to do in the next article, apart from winding up a number of loose ends, ensuring serialization of state, and detecting network availability.

Follow the Discussion

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.