다음을 통해 공유


Part V: Keeping WiE Mobile Client in sync with the Cloud, a first pass at synchronization.

The Implementation of WiEModelWithLocalCache

As we discussed in the previous articles, the data layer for the WiE Mobile Client is made up of “providers” that each implement the IWiEModel interface and work together to implement support for the occasionally disconnected mobile scenario. Over the past two weeks article we covered the implementation of the local data store: WiELocalModel and of the remote data store WiERemoteModelSSDSForMobile. In this article we combine both along with a simple synchronization mechanism to implement the complete occasionally connected data store.

As a refresher, the IWiEModel interface is shown below:

interface IWiEModel

{

   event EventHandler<LocationHistoryChangedArgs> RaiseLocationHistoryChangedEvent;

   event EventHandler<DeviceChangedArgs> RaiseDeviceChangedEvent;

   event EventHandler<MemberChangedArgs> RaiseMemberChangedEvent;

   WiELocationDataObject GetLocation(System.Guid p_guidLocationID);

   void RemoveLocation(System.Guid p_guidLocationID);

   List<WiELocationDataObject> GetLocationHistoryBetween(DateTime p_dtStartTimeUTC, DateTime p_dtStopTimeUTC);

   List<WiELocationDataObject> GetLocationHistoryBefore(DateTime p_dtEndTimeUTC);

   void SaveLocation(WiELocationDataObject p_locationDataObject);

   void SaveMember(WiEMemberDataObject p_memberDataObject);

   WiEMemberDataObject GetMember(Guid p_guidMemberID);

   WiEMemberDataObject GetMemberByUserName(string p_strUserName);

   WiEMemberDataObject GetMemberByPhoneNumber(string p_strPhoneNumber);

   void SaveDevice(WiEDeviceDataObject p_deviceDataObject);

   WiEDeviceDataObject GetDevice(System.Guid p_guidDeviceID);

   List<WiEDeviceDataObject> GetDevicesByMember(System.Guid p_guidMemberID);

   void Initialize();

   void Terminate();

}

 

 

Implementing the WiEModelWithLocalCache class

The WiEModelWithLocalCache “front ends” the two previously discussed implementations of the model interface and routes calls to the appropriate provider, which in most cases is the Local Provider, relying on a separate synchronization agent to forward those requests at a later time to the remote provider.

/// <summary>

/// This class implements the Data Model for the application. This model leverages both a

/// local datastore for caching information and for offline availability and then uses a

/// remote version of the model to save / publish the information to a remote server.

/// </summary>

class WiEModelWithLocalCache : IWiEModel

{

   private WiEModelLocal m_localModel;

   private WiEModelRemoteSSDSForMobile m_remoteModel;

   private WiEModelSynch m_syncAgent;

Initialize()

The Initialize() method is called by the application to initialize its model. The WiEModelWithLocalCache leverages that call to create instances of the local and remote providers and the sync provider.

/// <summary>

/// Initialize the Model, creating a local cache and a remote model provider with a

/// synch object that keeps the two in synch where necessary.

/// </summary>

public void Initialize()

{

  // Create the local model and initialize it

  m_localModel = new WiEModelLocal();

  m_localModel.Initialize();

           

  // Create the remote model and initialize it

  m_remoteModel = new WiEModelRemoteSSDSForMobile();

  m_remoteModel.Initialize();

  // Since this model is really a wrapper for the two real model, we should listen to events from

  // the individual model implementations so that we can "bubble" them up to anyone that might

  // care.

  m_localModel.RaiseLocationHistoryChangedEvent += new

           EventHandler<LocationHistoryChangedArgs>(OnLocationHistoryChangedEvent);

  m_localModel.RaiseDeviceChangedEvent += new

           EventHandler<DeviceChangedArgs>(OnDeviceChangedEvent);

  m_localModel.RaiseMemberChangedEvent += new

           EventHandler<MemberChangedArgs>(OnMemberChangedEvent);

  m_remoteModel.RaiseLocationHistoryChangedEvent += new

           EventHandler<LocationHistoryChangedArgs>(OnLocationHistoryChangedEvent);

  m_remoteModel.RaiseDeviceChangedEvent +=new

           EventHandler<DeviceChangedArgs>(OnDeviceChangedEvent);

  m_remoteModel.RaiseMemberChangedEvent +=new

           EventHandler<MemberChangedArgs>(OnMemberChangedEvent);

  // Create/Initiate the synch agent that will keep Local cache and Remote Model "in synch",

  // the synch agent leverages the events from the model to trigger synch actions

  // (in addition to any background processing the agent also supports).

  m_syncAgent = new WiEModelSynch();

  m_syncAgent.LocalModel = m_localModel;

  m_syncAgent.RemoteModel = m_remoteModel;

  m_syncAgent.Initialize();

}

 

The class is then responsible for implementing the various SaveXXX() and GetXXX() methods expected from a model, while I won’t show every method I will highlight a couple so you an idea of the logic.

The model attempts to perform operations against what it considers the “master” for a piece of data, if that source is unavailable it falls back to the secondary store. The definition of “master” depends of the type of data: For location records, the local store is queried first; for member information, the remote store is attempted first.

GetLocation(Guid p_guidLocationID)

/// <summary>

/// Retrieve the specified location object (likely from remote server)

/// </summary>

/// <param name="p_guidLocationID"></param>

/// <returns></returns>

public WiELocationDataObject GetLocation(Guid p_guidLocationID)

{

  WiELocationDataObject locationObject = null;

  // Attempt to retrieve from local first

  locationObject = m_localModel.GetLocation(p_guidLocationID);

  if (locationObject == null)

  {

  // The location object was not available locally (it was most likely already synched

      // and deleted) so try to retrieve it from the remote data store instead

      locationObject = m_remoteModel.GetLocation(p_guidLocationID);

   }

   return (locationObject);

}

 

SaveLocation(WiELocationDataObject p_locationDataObject)

SaveLocation saves location records to the local model (SQL Compact based data store) and expects the synchronization mechanism to move those records to the remote model when appropriate.

/// <summary>

/// Save the location to the local cache datastore (and asynchronously to remote

/// datastore through synchronization)

/// </summary>

/// <param name="p_locationDataObject"></param>

public void SaveLocation(WiELocationDataObject p_locationDataObject)

{

  // Save to local data store, sync makes sure to get it over to the remote data store.

  m_localModel.SaveLocation(p_locationDataObject);

}

 

GetMember(Guid p_guidMemberID)

GetMember assumes that the “best source” for information related to members is the remote model ( SSDS data store) and attempts to query it for that data and falls back to using the local version if the remote version is unavailable.

/// <summary>

/// Retrieves the member specified by p_guidMemberID. It first attempts to read the member

/// from the remote data store as it is viewed as the "master" for member records. If the

/// remote is unavailable (or if the record does not exist), the local data store is queried

/// to get cached version.

/// </summary>

/// <param name="p_guidMemberID"></param>

/// <returns></returns>

public WiEMemberDataObject GetMember(Guid p_guidMemberID)

{

   WiEMemberDataObject memberDataObject = null;

   try

   {

   // Attempt to retrieve the Member information from remote (remote is assumed to always

      // be the best "source")

      memberDataObject = m_remoteModel.GetMember(p_guidMemberID);

   }

   catch (Exception remoteGetMemberException)

   {

   System.Diagnostics.Trace.WriteLine("GetMember() could not connect or retrieve member from

        remote model, trying to retrieve from local model: " +

             remoteGetMemberException.ToString());

   }

   if (null == memberDataObject)

   {

   // We we unable to retrieve the member from the remote data store, so we try to retrieve

      // it from the local datastore.

      memberDataObject = m_localModel.GetMember(p_guidMemberID);

   }

   return (memberDataObject);

}

 

The WiEModelSynch class

The current implementation of WiEModelSynch implements a very limited set of synchronizations: specifically the synchronization of the locally stored Location History with the remote data model.

In the very near future the client will need to implement bi-directional synchronization and synchronization of additional data types. I plan to implement this upcoming full implementation using the Microsoft Sync Framework and sync services for mobile devices. I was hoping to have that implementation completed for this article but it still needs a little TLC and I am waiting for the next CTP of the mobile sync framework. The net result is that the current implementation does not yet leverage the Sync Framework.

The WiEModelSynch class exposes a LocalModel and a RemoteModel property that it expects to be set by the application (in our case, this is done by the WiEModelWithLocalCache). The Sync Agent registers with the models to be notified of the various ChangedEvents raised by the model and uses those notifications to “wake up” the worker thread.

/// <summary>

/// This class implements a simple synchronization server to keep data from the local data model

/// in synch with the remote data model. This class and infrastructure should be replaceable by

/// the new Sync Framework services and I plan to switch to that mechanism once I have this

/// implementation working (to learn and compare the two approaches).

/// </summary>

class WiEModelSynch

{

   private static long DELAY_IN_MINUTES_BEFORE_FIRST_CHECK = 1;

   private static long DELAY_IN_MINUTES_BEFORE_NEXT_CHECK = 5;

   private static long DELAY_IN_MINUTES_MINIMUM_BETWEEN_CHECK = 1;

   private Thread m_threadSynchWorker = null;

   private volatile bool m_bKeepRunning = true;

   private AutoResetEvent m_threadResetEvent = null;

   private DateTime m_dtNextCheck;

   private IWiEModel m_localModel;

   private IWiEModel m_remoteModel;

...

   public IWiEModel LocalModel

   {

  get {return m_localModel;}

     set {

        m_localModel = value;

        if (m_localModel != null)

        {

          // We need to listen to the events raised from the model

          m_localModel.RaiseLocationHistoryChangedEvent += new

             EventHandler<LocationHistoryChangedArgs>(OnLocationHistoryChangedEvent);

          m_localModel.RaiseDeviceChangedEvent += new

             EventHandler<DeviceChangedArgs>(OnDeviceChangedEvent);

          m_localModel.RaiseMemberChangedEvent += new

             EventHandler<MemberChangedArgs>(OnMemberChangedEvent);

        }

     }

  }

. . .

 

OnLocationHistoryChangedEvent(object sender,LocationHistoryChangedArgs e)

/// <summary>

/// Event handler responsible for processing data changed events about the

/// location history (i.e. new records posted, or records removed).

///

/// Depending on implementation these events may not be raised if these records are

/// only saved to remote location.

/// </summary>

/// <param name="sender"></param>

/// <param name="e"></param>

void OnLocationHistoryChangedEvent(object sender, LocationHistoryChangedArgs e)

{

  if (sender == m_localModel)

  {

  // We've added some new location records to the local data model, we need to make sure these

    // are sent to the remote server so that the device's location can accurately be tracked and

    // rendered.

    WakeUpWorker();

  }

}

...

private void WakeUpWorker(){ DateTime dtMinimumWaitUntil = DateTime.Now.AddMinutes (DELAY_IN_MINUTES_MINIMUM_BETWEEN_CHECK);

// Force the backgroud process to attempt synch, but make sure there has been at least

// some decent amount of time since the last time we attempted to sync.

if (m_dtNextCheck > dtMinimumWaitUntil)

m_dtNextCheck = dtMinimumWaitUntil;

m_threadResetEvent.Reset();}

 

 BackgroundWorker()

The WiEModelSynch implementation consists of a background worker thread that queries the local model for all available Location data objects and then attempts to save the retrieved location data objects with the remote model.

/// <summary>

/// Entry point for the sync agent background thread.

///

/// Current implementation only synchs: localModel.Locations --> remoteModel.Locations.

/// </summary>

public void BackgroundWorker()

{

   while (m_bKeepRunning)

   {

   // Wait until we are notified of a change to process or timeout

      m_threadResetEvent.WaitOne(1000,false);

      if (m_bKeepRunning)

      {

      // Is it time to attempt to synch?

        if (DateTime.Now >= m_dtNextCheck)

        {

        PerformSync();

          // Let's wait a little bit before we try to synch again

          m_dtNextCheck = DateTime.Now.AddMinutes(DELAY_IN_MINUTES_BEFORE_NEXT_CHECK);

        }

       }

     }

}

 

PerformSync()

private void PerformSync()

{

  // Retrieve all the location records that were recorded before the time of the next check.

  // Note this logic is a little bit of a hack, since I assume that successfully synched items

  // have been removed from the local model (i.e. a "move" synch), when I implement a more

  // generic synch approach that supports both "move" and "copy" type synch, the logic will need

  // to be based on the actual data, i.e. all items "changed" since a timestamp.

  // Convert the current time to UTC since locations from the GPS are collected with respect

  // to GMT / UTC.

  List<WiELocationDataObject> listOfLocationDataObjects =

         m_localModel.GetLocationHistoryBefore(m_dtNextCheck.ToUniversalTime());

  foreach (WiELocationDataObject locationDataObject in listOfLocationDataObjects)

  {

     // Attempt to save the locationDataObject to the remote data store, give up when we

     // start failing. Again a little bit of a hack, I should not assume that failures indicate

     // bad connectivity and have a separate check for that.

     try

     {

     // Save to the remote model

       m_remoteModel.SaveLocation(locationDataObject);

       // Remove from the local model if the Save succeeded (didn't raise an exeception)

       m_localModel.RemoveLocation(locationDataObject.LocationID);

     }

     catch (Exception ex)

     {

     // We failed to save the location, so we should give up for now

       System.Diagnostics.Trace.WriteLine("Unable to save the location during Synch: " +

               ex.ToString());

       break;

     }

  }

}

 

Putting all the pieces together…

The mobile client application’s main form creates an instance of the WiEModelWithLocalCache class and uses it for all its data storage operations.

/// <summary>

/// Constructor for the main form

/// </summary>

public MainForm()

{

   InitializeComponent();

   // Try to load previous settings if any

   LoadSettings();

   // Try to create the (data) model for the application

   m_model = new WiEModelWithLocalCache();

           

   // Initialize the model as we will be using it to retrieve

   m_model.Initialize();

 

Revisiting OnLocationChanged()

You can see the use of m_model in the OnLocationChanged event handler for GPS events.

/// <summary>

/// Event callback when a new location is received from the GPS module.

/// </summary>

/// <param name="sender"></param>

/// <param name="args"></param>

void OnLocationChanged(object sender, LocationChangedEventArgs args)

{

  // Retrieve the GPS position information from the args

  m_currentPosition = args.Position;

  try

  {

  // Only do this if we got GPS data.

    if ((m_gps.Opened) && (m_currentPosition != null))

    {

       // We need at least the longitude and the latitude for this to be worth saving...

  if (m_currentPosition.LatitudeValid && m_currentPosition.LongitudeValid &&

           m_currentPosition.TimeValid && m_currentPosition.SatellitesInViewCountValid)

       {

       // Ok, now populate the new Location Data Object

         WiELocationDataObject newLocation = new WiELocationDataObject();

         // Set up the base required fields (Long, Lat, Time, SatellitesInView)

         newLocation.LocationID = System.Guid.NewGuid();

         newLocation.MemberID = m_guidMemberID;

         newLocation.DeviceID = m_guidDeviceID;

         newLocation.Longitude = m_currentPosition.Longitude;

         newLocation.Latitude = m_currentPosition.Latitude;

         newLocation.DateCollected = m_currentPosition.Time;

         newLocation.NumSatellites = m_currentPosition.SatellitesInViewCount;

         // Now set the optional fields

         if (m_currentPosition.SpeedValid)

              newLocation.Speed = m_currentPosition.Speed;

         if (m_currentPosition.HeadingValid)

              newLocation.Heading = m_currentPosition.Heading;

         if (m_currentPosition.SeaLevelAltitudeValid)

       newLocation.AltitudeWRTSeaLevel = m_currentPosition.SeaLevelAltitude;

         if (m_currentPosition.EllipsoidAltitudeValid)

              newLocation.AltitudeWRTEllipsoid = m_currentPosition.EllipsoidAltitude;

  // Now avoid saving too much data by applying a data capture governot that

         // only saves every [m_nMinimumDistanceBetweenLocationInMeters] meters or every

         // [m_nMaximumEllapsedTimeBetweenLocationInSeconds] seconds, whichever comes

         // first.

         double dDistanceFromPreviousPoint = (null == m_previousLocation) ?

                 m_nMinimumDistanceBetweenLocationInMeters :

                  newLocation.DistanceTo(m_previousLocation);

         double dSecondsEllapsedSincePreviousPoint = (null == m_previousLocation) ?

                 m_nMaximumEllapsedTimeBetweenLocationInSeconds :

                  ((TimeSpan)newLocation.DateCollected.Value.Subtract

                      (m_previousLocation.DateCollected.Value)).TotalSeconds;

 

  if ((dDistanceFromPreviousPoint >= m_nMinimumDistanceBetweenLocationInMeters) ||

             (dSecondsEllapsedSincePreviousPoint >=

                      m_nMaximumEllapsedTimeBetweenLocationInSeconds))

         {

  // Attempt to save the the location

            m_model.SaveLocation(newLocation);

        // Remember the location for next time around in case we want to implement

            // a simple governor to minimize amount of data collected.

            m_previousLocation = newLocation;

         }

       }

     }

  }

  catch (Exception ex)

  {

  // We ran into an issue with the GPS module.

       System.Diagnostics.Trace.WriteLine("Issue in the GPS event handler: " +

       ex.ToString());

  }

  // Update the display to show the current location

  Invoke(m_updateDisplayHandler);

}

 

So what’s next for WiE?

In upcoming articles we will start implementing a simple web client for the community which will query and interact with the SQL Server Data Services (SSDS) data store and visualize the community using the Virtual Earth SDK (and the new Virtual Earth ASP.Net server control).

Once the client and the web viewer are complete we will incorporate a spatial notification rules engine built using SQL Server 2008.

We will also replace the current sync implementation with a fuller implementation that leverages the Microsoft Sync Framework and the sync services provider for mobile devices to enable bidirectional sync and synchronization of additional data types to and from SSDS and SQL Server 2008.

Comments