In Building Conference Buddy for WPF (described here with an overview here), Carey Payette and I faced a common problem in line of business applications: iStock_ConnectXSmallthe need to work when disconnected from the network. This blog post will outline the implementation approach that we took to meet that requirement.

Conference Buddy is a system for tracking people we meet at conferences, and recording which Telerik products they are using and which they are interested in.

The Requirements

Conference Buddy is designed to be used at conferences, where the Internet connection may be sporadic, at best.  It is important that the application work well in “off-line” mode, and that no information be lost. It is also important that the information on the local machine be synchronized with the server, which will serve as the canonical repository for all instances of the program.

This is a common requirement of Line of Business applications, and while no single offline solution fits all cases, our  implementation of these requirements can be used as one successful approach. 

We began with an architecture in which we continually updated both the online service (an ASP.NET application currently running on Azure fronting a SQL Server database via Entity Framework) as well as writing the data to disk for safe-keeping.  This became untenable given the time it took to update the service. For our first approximation we were updating all the contacts in all the Events, rather than optimizing for just those records which were added, deleted or changed. 

We refactored the application to work exclusively from local data and to update and synchronize with the server on demand.  This blog post will review this new approach.

The Data

It begins, of course, with the data. There are two principal classes that must be understood: the Event and the Contact. 

The Event Class’s public schema looks like this,

 1: public class Event : ModelBase
 2: {
 3: public Event() { /… }
 4: public int id { get; set; }
 5: public string name { get; set; }
 6: public string city { get; set; }
 7: public string state { get; set; }
 8: public string country { get; set; }
 9: public int attendeeCount { get; set; }
 10: public string telerikEventCoordinator { get; set; }
 11: public string eventOrganizerName { get; set; }
 12: public string eventOrganizerEmail { get; set; }
 13: public string eventOrganizerPhone { get; set; }
 14: public bool isTelerikSponsored { get; set; }
 15: public SponsorhsipLevel telerikSponsorhipLevel { get; set; }
 16: public bool hadBooth { get; set; }
 17: public string telerikSpeakers { get; set; }
 18: public DateTime startDate { get; set; }
 19: public DateTime endDate { get; set; }
 20: public string notes { get; set; }
 21: }

The Event was poorly named because event (lowercase e) is a keyword in C#.  Our Event class refers to a conference or other community event (hack-a-thon, etc.).  It consists of a number of properties, including a unique ID, a name, a city, state and country.

Note that the property names are lower case to agree with the naming convention for the HMTL version of ConferenceBuddy. It turns out this is not necessary (the JSON converter can fix the naming for us) and so will be fixed in a coming revision.

We also track the attendee count, the Telerik coordinator, the event coordinator,  our sponsorship level, our speakers and, as you can see, quite a bit more.

The Contact class refers to people who come to our booth or we otherwise interact with at the conference.  The Contact class has a great deal of information  We start with the person’s name, address and email; then go on to gather information about which Telerik products they use and which they are interested in, and whether they wish to participate in our raffle (we often raffle off our complete suite of software). 

Here is the public interface for the Contact class,

 1: public class Contact : ModelBase
 2: {
 3: public int id { get; set; }
 4: public string email { get; set; }
 5: private string _firstName { get; set; }
 6: public string lastName { get; set; }
 7: public string jobTitle { get; set; }
 8: public string company { get; set; }
 9: public string phoneNumber { get; set; }
 10: public bool raffle { get; set; }
 11: public bool devTools { get; set; }
 12: public bool alm { get; set; }
 13: public bool testing { get; set; }
 14: public bool html5 { get; set; }
 15: public bool mobile { get; set; }
 16: public bool iceniumCustomer { get; set; }
 17: public bool iceniumInterested { get; set; }
 18: public bool AspNetMvcCustomer { get; set; }
 19: public bool AspNetMvcInterested { get; set; }
 20: public bool wpfCustomer { get; set; }
 21: public bool wpfInterested { get; set; }
 22: // more products 
 23: public string notes { get; set; }
 24: public ObservableCollection<ProblemReport> problemReports { get; set; }
 25: }

 

The Contact class gathers “contact” information about the customer (e.g., name, phone number, etc.) as well as whether the contact wishes to enter our raffle, is interested in various large categories of products, and then whether the contact is a current customer or interested in each of our products (the entire list of products is elided here to save room). Finally we gather notes and a collection of problem reports, feedback and requests for follow up (all in the problemReports collection).

The Model

Our Model consists of our data classes along with an EventRepository and a ContactRepository and a few helper classes.  The job of the Repositories is to mediate the loading and saving of the files from the File service. 

Services

We have three Services classes: ClientyProxy which interacts with the server, FileService which interacts with the file system and UIServerSynchronizationService which is responsible for synchronization with the server. 

Storing Locally

During the normal use of Conference Buddy, our data is not stored to or retrieved from the server. Instead it is maintained locally, on the file system, mediated by the FileService.    The job of the FileService is to manage the directories and file names and to read from and write to the files.  The files themselves are stored as JSON strings, and conversion between JSON and our objects is performed by the Repositories, making use of the Newtonsoft.JSON library which can be downloaded via NuGet (see figure)

Manage NuGet Packages

 

We have one JSON file that describes all the events, and individual files for each event that describe all the contacts for that event.  These latter files are identified by the event id, so we have, e.g., eventsMetaData.json, 1.json, 2.json, 3.json and so forth.   All of these files are stored in a known location (for now, the subfolder ConferenceBuddy).  This is illustrated in the UML diagram,

UML of Conf Buddy

Synchronization

The main menu of the application has a Synchronization button in MainEvent.xaml,

 1: <Button Name="Synchronize"
 2:         Content="Synchronize"
 3:         Width="70"
 4:         Command="{Binding SynchronizeEverything}" />

The property this is bound to is, of course, in MainEventViewModel,

 1: public SynchronizeCommand SynchronizeEverything
 2: {
 3:     get { return _synchronizeEverything; }
 4:     set
 5:     {
 6:         _synchronizeEverything = value;
 7:         RaisePropertyChanged( "SynchronizeEverything" );
 8:     }
 9: }

To wire this up, we declare a new Command in our Command folder, SynchronizeCommand, which derives from our BaseCommand class. The BaseCommand class is an abstract class that looks like this:

 1: public abstract class BaseCommand : ICommand
 2: {
 3: public abstract bool CanExecute( object parameter );
 4: public abstract void Execute( object parameter );
 5:  
 6: public event EventHandler CanExecuteChanged
 7:    {
 8:       add { CommandManager.RequerySuggested += value; }
 9:       remove { CommandManager.RequerySuggested -= value; }
 10:    }

The SynchronizeCommand class must implement the two abstract methods: CanExecute and Execute. We hard-wire CanExecute to return true, and do the synchronization work in Execute. We also create and manage two events, to signal the start and end of synchronization; these are used by the UI to display and remove the busy indicator, respectively.

 1: public class SynchronizeCommand : BaseCommand
 2:  {
 3: public  EventHandler SynchronizationStarted;
 4: public  EventHandler SynchronizationEnded;
 5:  
 6: public override bool CanExecute( object parameter )
 7:      {
 8: return true;
 9:      }
 10:  
 11: public async  override void Execute( object parameter )
 12:      {
 13: if (SynchronizationStarted != null)
 14:              SynchronizationStarted( this, new EventArgs() );
 15:  
 16:         await UIServerSynchronizationService.
 17:                    SynchronizeClientWithServerAsync();
 18:  
 19: if (SynchronizationEnded != null)
 20:             SynchronizationEnded( this, new EventArgs() );
 21:      }
 22:  }

Notice that this command defines two events: SynchronizationStarted and SynchronizationEnded. As you’ll see below, we use this to inform the user that synchronization is in progress.

The Execute method is the heart of this Command.  It surrounds its call to SynchronizeClientWithServerAsync with the two events. 

The call to SynchronizeClientWithServerAsync sets off all the synchronization work.  We’ll return to that in just a moment. 

A Quick Note On Comments: We take the radical view that code should not be commented. If the identifiers and methods are given sufficiently descriptive names, commenting is unnecessary. Given that comments rust over time, uncommented readable code is very much to be desired. We understand this is controversial, and consider this effort an experiment.

One use of comments that you will find in our code is to explain why we are doing something that is not otherwise obvious.

In the ViewModel we instantiate our command and register for the two events in the constructor,

 1: public MainEventViewModel()
 2: {
 3: //...
 4:    SynchronizeEverything = new SynchronizeCommand();
 5:    SynchronizeEverything.SynchronizationStarted += 
 6:                              SynchronizationStarted;
 7:    SynchronizeEverything.SynchronizationEnded += 
 8:                              SynchronizationEnded;
 9: }

The two event handlers set a boolean property, IsSynchronizing,

 1: private void SynchronizationEnded( 
 2: object sender, EventArgs e )
 3: {
 4:     IsSynchronizing = false;
 5: }
 6:  
 7: private void SynchronizationStarted( 
 8: object sender, EventArgs e )
 9: {
 10:     IsSynchronizing = true;
 11: }

IsSynchronizing is the property we bind to in our XAML,

 1: private bool isSynchronizing;
 2: public bool IsSynchronizing 
 3: {
 4:     get { return isSynchronizing; }
 5:     set
 6:     {
 7:         isSynchronizing = value;
 8:         RaisePropertyChanged( "IsSynchronizing" );
 9:     }
 10: }

Back in MainEvent.xaml we add a RadBusyIndicator and bind it to the IsSynchronizing property of the ViewModel,

 1: <telerik:RadBusyIndicator IsBusy="{Binding IsSynchronizing}"
 2:                               IsIndeterminate="True" />

SynchronizeClientWithServerAsync

You will remember that back in the command we call SynchronizeClientWithServerAsync.  This is a method of the new UIServerSynchronizationService.  The method makes calls to two private helper methods,

 1: public static async Task SynchronizeClientWithServerAsync()
 2: {               
 3:     await WriteEventsAndContactsToServer();
 4:     await ReadEventsAndContactsFromServer();
 5: }

The job of the first method is to write our events and contacts to the server. Once this is done, we’ll read them back from the server and store them locally. 

Writing To The Server

We start by updating the server with everything we have locally. It is the servers job to sort out what is new, what is deleted and what is changed. We begin by getting references to the EventRepository and the ClientRepository respectively, and asking the EventRepository for the JSON for the events from the file system,

 1: var eventRepo = new EventRepository();
 2: var contactRepo = new ContactRepository();
 3: var eventListJSON = 
 4:      await eventRepo.GetEventsJSONFromLocalAsync();

We then write these events to the server, using the ClientProxy class provided by Jeff Fritz, which talks to the back-end server,

 1: await ClientProxy.WriteEventsToServerAsync( eventListJSON );

We next get all the file names in the ConferenceBuddy directory (stripping off the path) and iterate through the files finding the event id for each (remember that the file name is eventid.json, such as 1.json).  Once we have the eventID we can obtain the JSON of all the contacts from the file system through the ContactRepository,

 1: var contactJSON = 
 2:    await contactRepo.GetContactsJSONFromLocalAsync( eventId );

We are now ready to write the JSON to the server.

 1: if (!string.IsNullOrEmpty( contactJSON ))
 2: { 
 3:     await ClientProxy.WriteContactsToServerAsync( 
 4:             contactJSON, eventId );
 5: }

Since this is in a loop, we send the contact information for all the events we have locally.

Reading From The Server

Now that we’ve updated the server, it is time to update the client.  First we will get the list of events from the server,

 1: var events = await ClientProxy.LoadEventsFromServerAsync();

We then serialize these events and write them to our file as JSON,

 1: string eventsJSON = JsonConvert.SerializeObject( events );
 2: await FileService.WriteToFileAsync( 
 3:    eventsJSON, "ConferenceBuddy", "EventsMetaData.JSON" );

Next, we iterate through the events, retrieving each set of contacts from the server based on the eventID and writing these to the file system as JSON,

 1: foreach (Event theEvent in events)
 2: {
 3:     var contacts = 
 4:       await ClientProxy.LoadContactsFromServerAsync( 
 5:                                               theEvent.id );
 6: string contactsJSON = JsonConvert.SerializeObject( 
 7:                                                  contacts );
 8:     await FileService.WriteToFileAsync( 
 9:            contactsJSON, 
 10: "ConferenceBuddy", 
 11:            theEvent.id.ToString() + ".JSON" );
 12: }

While the synchronization is continuing, the loading indicator (RadBusyIndicator)  is displayed. As soon as the synchronization is complete, the loading indicator is removed. This was handled by subscribing to the events and toggling the ViewModel’s IsSynchronizing property.

Because the controls on the page are databound, with implementations of INotifyPropertyChanged on each property, the UI is immediately updated with the newly retrieved data.

Busy Indicator

Open The Pod Bay Doors, Hal

A lot of things can go wrong while synchronizing to the server.  The server can choke on the data, you can lose your connection, your machine can be shut down and so forth. 

For now we have implemented “last data update wins” as well as “server wins.” That means that the last client to update the server will, if necessary, overwrite data from other clients (this should be rare), and the data on the server overwrites the data on the client.

While we believe that this architecture is quite robust, it is not, yet, foolproof (what happens if you read the Events metadata from the server and then lose the connection before you get all the clients?).  A future post will detail the steps we will take to make this rock-solid.


Get the most of XAML Download RadControls for WPF Download RadControls for Silverlight

About the author

Jesse Liberty

Jesse Liberty

Jesse Liberty is a Technical Evangelist for Telerik and has three decades of experience writing and delivering software projects. He is the author of 2 dozen books as well as courses on Pluralsight on Windows 8 and WPF. Jesse has been a Distinguished Software Engineer for AT&T and a VP for Information Services for Citibank and a Software Architect for PBS. You can read more on his personal blog and his Telerik blog or follow him on twitter


jesseLiberty
About the Author

Jesse Liberty

 has three decades of experience writing and delivering software projects. He is the author of 2 dozen books and has been a Distinguished Software Engineer for AT&T and a VP for Information Services for Citibank and a Software Architect for PBS. You can read more on his personal blog or follow him on twitter

Comments

Comments are disabled in preview mode.