The Client Project Generated by the Data Abstract New Project Wizard

The aim of this article is provide a deeper look at the project you previously created using the Data Abstract New Project Wizard. It will also show you what you need to do to complete your client application so that it talks to either an instance of Relativity Server or a custom Data Abstract server.

Files in the Project



General overview of the basic files

In depth with DataAccess.h & DataAccess.m

Data Abstract for Cocoa uses the typical Cocoa framework convention of starting the first two letters of the classname to avoid class overlaps. Therefore throughout the Data Abstract classes you will find that they either start with DA (for Data Abstract) and RO which is the prefix used for classes from the Remoting SDK which is the underlying remoting framework that Data Abstract uses to communicate with the server.

With that in mind, the most important files in the project created by the project wizard is the DataAccess class which can be found in DataAccess.h and DataAccess.m. The singleton class, available anywhere via the sharedInstance property, acts as the bridge between your application and the server you have chosen to use. It is responsible for:

  • The connection and login to Relativity Server or a custom Data Abstract server
  • Downloading, persisting and exposing data as needed; for instance to DADataTable instances
  • Tracking changes to the data and applying them back to the server.

The class as it stands is incomplete, you will be working with it to make it suitable for the schema, tables and fields that are specific to your application.

DataAccess.h

Class implements two delegates DARemoteDataAdapterDelegate and DAAsyncRequestDelegate.

  • The DARemoteDataAdapterDelegate delegate is available in DARemoteDataAdapter.h. All the methods in the delegate are optional, however you should implement one of the needsPassword/verifiedPassword methods as appropriate. These are used by the remote data adapter to authenticate you with the server.
@protocol DARemoteDataAdapterDelegate <NSObject>

@optional

- (void)remoteDataAdapter:(DARemoteDataAdapter * )adapter didFailToApplyChange:(DADeltaChange * )change forTable:(DADataTable * )table;
- (void)remoteDataAdapter:(DARemoteDataAdapter * )adapter didFailWithException:(NSException * )exception forAsyncRequest:(DAAsyncRequest * )request;

- (BOOL)remoteDataAdapterNeedsLogin:(DARemoteDataAdapter * )adapter __deprecated;
- (BOOL)remoteDataAdapterNeedsLoginOnMainThread:(DARemoteDataAdapter * )adapter __deprecated;

- (BOOL)remoteDataAdapter:(DARemoteDataAdapter * )adapter needsPassword:(NSString ** )password forLoginString:(NSString * )loginString;
- (BOOL)remoteDataAdapter:(DARemoteDataAdapter * )adapter needsPassword:(NSString ** )password forUsername:(NSString * )username;
- (BOOL)remoteDataAdapter:(DARemoteDataAdapter * )adapter verifiedPassword:(NSString * )password forLoginString:(NSString * )loginString;
- (BOOL)remoteDataAdapter:(DARemoteDataAdapter * )adapter verifiedPassword:(NSString * )password forUsername:(NSString * )username;

@end
  • The DAAsyncRequestDelegate delegate is available in DAAsyncRequest.h, again all the methods in the delegate are optional however if you want to receive updated data from the server you will need to implement one of the didReceive* methods.
@protocol DAAsyncRequestDelegate

@optional

- (void)asyncRequest:(DAAsyncRequest * )request didReceiveSchema:(DASchema * )schema;

- (void)asyncRequest:(DAAsyncRequest * )request didReceiveTable:(DADataTable * )table;
- (void)asyncRequest:(DAAsyncRequest * )request didReceiveTables:(NSDictionary * )tables;
- (void)asyncRequest:(DAAsyncRequest * )request didReceiveData:(NSData * )data;

- (void)asyncRequest:(DAAsyncRequest * )request didFailToApplyChange:(DADeltaChange * )change forTable:(DADataTable * )table;
- (void)asyncRequest:(DAAsyncRequest * )request didFinishApplyingChangesForTables:(NSArray * )tables withErrors:(NSArray * )errors;

- (void)asyncRequest:(ROAsyncRequest * )request didFailWithException:(NSException * )exception;
- (void)asyncRequest:(ROAsyncRequest * )request didReceiveDataSize:(int) size ofExpected:(int) totalSize;

- (void)asyncRequest:(DAAsyncRequest * )request didFinishExecutingCommand:(NSString * )commandName withResult:(int)result andOutputParams:(NSDictionary * )outParams;
@end

The DataAccess class has 4 public properties available to your classes. They are:

@property (assign) id<DataAccessDelegate> delegate;
@property (readonly) NSString *homeFolder;
@property (readonly) BOOL dataReady;
@property (readonly) BOOL busy;
  • The delegate property can be used to store a reference to a class you create that implements the DataAccessDelegate delegate.
  • The homeFolder specifies the folder which the DataAccess class will use to store data. It is generated in the init method using the value specified in APPLICATION_SUPPORT_FOLDER
  • dataReady indicates that the data is available. It is set by the method triggerDataReady which also posts the NOTIFICATION_DATA_READY notification which you can listen for to retrieve the data when its available.
  • busy Is used to indicate if the class is busy transacting with the server. The set method is overridden to post a NOTIFICATION_BUSYSTATUS_CHANGE notification whenever the state changes. Your applications can listen for this notification so that you can queue up operations.

There are also four public methods, plus a class method that returns a singleton instance of the class:

+ (DataAccess *)sharedInstance;

- (void)loadInitialData;
- (void)saveData;
- (void)saveDataInBackground;

- (DAAsyncRequest *)beginApplyUpdates;
  • sharedInstance is a class method that returns a singleton instance of the class, creating it if needed.
  • The loadInitialData method is used to trigger an initial loading of the data in a background thread. Initially DataAccess will attempt to load data that was previously saved to a briefcase and if that isn't available, it will attempt to download the data from the server.

WARNING: Never call loadInitialData asynchronously, as it itself calls threadedLoadInitialData in a background thread. This could cause problems with passing results back into the UI thread.

  • The saveData and saveDataInBackground methods are both used when your application is using a briefcase to store the data offline.

  • The beginApplyUpdates method is used asynchronously apply local data changes back to the server. You will need to edit this method to add the tables whose changes should be sent to the server.

DataAccessDelegate

The DataAccess class header provides a delegate protocol, that a class wanting to be a delegate needs to implement.

@protocol DataAccessDelegate

- (void)alertError:(NSString * )message;
- (BOOL)needLogin:(NSString ** )login password:(NSString ** )password;

@end

The DataAccess class uses the delegate to inform you when it requires login details, or to let you know something has gone wrong. The username and password are passed from your implementation of needLogin:password: to the caller.

DataAccess.m

The actual implementation of DataAccess that we provide is a combination of fully functional code and partial method stubs that you will need to implement. Generally the method stubs contain commented example code showing you how a typical method implementation might look like. As noted above, the class adopts the DARemoteDataAdapterDelegate and DAAsyncRequestDelegate delegate protocols, and implements some, but not all, of the optional delegate methods.

While we provide DataAccess, the expectation is that you will adapted the class to your needs. Implementing the delegate methods of DARemoteDataAdapterDelegate and DAAsyncRequestDelegate as appropriate for your use, and adding methods to retrieve data or schemas as needed.

Below we will step through the finer details of the class.

The Defines

At the beginning of DataAccess.m you will find a large section of #defines.

#pragma mark -
#pragma mark Defines

#define APPLICATION_SUPPORT_FOLDER      @"RemObjects/DAMacApp"
#define BRIEFCASE_FILENAME              @"DAMacApp.briefcase"
#define BRIEFCASE_DATA_VERSION_KEY      @"DAMacApp-BriefcaseVersion"
#define BRIEFCASE_DATA_VERSION          @"0.1"

// The following defines specify the name under which passwords
// will be saved in the keychain
#define KEYCHAIN_APPNAME                @"RemObjects.DAMacApp"

// The following defines specify the KEYS under which username,
// password and server address will be stored in NSUserDefaults:
#define USERDEFAULTS_SERVERADDRESS_KEY  @"ServerAddress"
#define USERDEFAULTS_USERNAME_KEY       @"UserName"
#define USERDEFAULTS_PASSWORD_KEY       @"Password"

// Uncomment the following define to use AES encryption.
// the password should be the same as on server side.
//#define AES_PASSWORD                  @"type AES password here"

#warning Define your server address and Relativity domain name, below.

#define SERVER_URL                  @"http://localhost:7099/bin"

// Do not change, when using Relativity Server
#define SERVICE_NAME                @"DataService"

#define RELATIVITY_DOMAIN           @"MyDomain"
//#define RELATIVITY_SCHEMA         @"MySchema"

A number of them are generated based on your project name (like the APPLICATION_SUPPORT_FOLDER) and don't require you to make any changes to them. However there are a key few which require your input:

  • BRIEFCASE_DATA_VERSION - is your way of indicating that the data stored in a briefcase is out of data and shouldn't be loaded. If you release a new version of the client that needs to use a updated version of the dataset from the server, then bump the BRIEFCASE_DATA_VERSION number and when the new client attempts to load the data in threadedLoadInitialData it will see the version mismatch and instead retrieve the data from the server.
  • AES_PASSWORD this comes commented out. If your server is using AES encryption then you should uncomment it and set the password to match that on the server.
  • SERVER_URL - should be changed to the address of your server (be it an instance of Relativity which is normally on port 7099, or an instance of a custom Data Abstract server). It is set to localhost by default as the assumption is you are developing against a local copy of the data.
  • RELATIVITY_DOMAIN & RELATIVITY_SCHEMA should be changed to match the names of the domain and schema you are using.

You may have noticed the #warning in amongst the #defines. We have used #warning throughout a number of files in the generated project to indicate where you need to make changes to the code. We'll cover more on that in the section below.

Methods

  • init sets up some basic things about the class. The first thing it does is to create a folder that will be used to store briefcase files, and sets up the briefcaseFileName which will be used when you go to save the data locally. It then sets up a RemoteDataAdapter and sets the DataAccess as a delegate for it. If you need to do any additional initilization, then add it to the end of the method.

  • loadInitialData is called from applicationDidFinishLaunching: in AppDelegate.m to begin the process of loading the data. loadInitialData fires off threadedLoadInitialData in a background thread and then returns control to the AppDelegate.

  • threadedLoadInitialData as noted above, this method operates in a background thread to load the data. Initially the method attempts to load the data from a briefcase if a briefcase file exists. If it doesn't OR if there is a version mismatch (BRIEFCASE_DATA_VERSION_KEY) between the client and the briefcase data, then the data is retrieved in a synchronous fashion from the server. Once the data is available, setupData is called to perform any additional setup and then triggerDataReady is called on the main thread and posts the NOTIFICATION_DATA_READY notification. Note that it is intended that this method is never called directly.

  • downloadData is called by threadedLoadInitialData to download data from the remote server. You are required to provide the body of this method otherwise it will do nothing. At its simplest you need to call the getDataTable method of DARemoteDataAdapter, passing in the name of the table you are interested in. Alternatively you could pass in an array of table names to DARemoteDataAdapter's getDataTables. When you have implemented the method, you can delete the #warning line to remove the build warning.

Note that the getDataTable* methods of DARemoteDataAdapter are synchronous methods, meaning that they block the code and will directly return the data. DARemoteDataAdapter also provides asynchronous methods (those starting with async*). For an initial download of the data that is required for your application to work, then the synchronous methods will likely be the most suitable.

  • The totally optional loadDataFromBriefcase: and saveDataToBriefcase: methods work in tandem to load and save your data tables to disk, to persist the data between runs of your application. If you don't need to persist the data, you don't need to worry about implementing the methods. Both methods have their method bodies commented out, but provide example code showing you how to use the briefcase. To save data, simply add your table to the briefcase using the addTable: method of DABriefcase and supply a BRIEFCASE_DATA_VERSION which is checked before the data is loaded; if you don't supply a BRIEFCASE_DATA_VERSION then DataAccess will not load the briefcase data. Retrieving data from the briefcase is as simple as calling the tableNamed method of DABriefcase.

  • setupData is an internal method called by threadedLoadInitialData after the data has either been loaded from a briefcase or downloaded from the server. The intention is that you would use the method to set up any virtual fields (for instance lookup or calculated fields) in your instance of DADataTable. The body of the method is unimplemented, but provides a commented out example of how you might use it to add a calculated field.

  • triggerDataReady sets the dataReady flag to YES and posts a NOTIFICATION_DATA_READY notification which you can listen to in your app; for instance you can use it to know to update the UI. triggerDataReady is called by threadedLoadInitialData after the data has been loaded (either from a briefcase or directly from the server) and is available for use.

  • saveDataInBackground calls fires of saveData in a background thread. In the project created it is only called by asyncRequest:didFinishApplyingChangesForTables:withErrors to save a local copy of the data to a briefcase. You might also call this method from your own app when the user goes to quit your app.

  • saveData prepares the data to be stored in a briefcase by calling saveDataToBriefcase, and then the data is written to disk by calling the writeBriefcase method of DABriefcase.

  • The setter method (setBusy:) of the busy property is overridden to post a NOTIFICATION_BUSYSTATUS_CHANGE notification when the value is changed. A number of methods check the state of busy, dropping out if DataAccess is busy. You can listen for the notification so that you can attempt to call whichever operation you were attempting.

  • remoteDataAdapterNeedsLogin: is a delegate method of DARemoteDataAdapterDelegate declared in the header of DARemoteDataAdapter. The delegate method is called anytime the remote data adapter DARemoteDataAdapter needs to authenticate the caller. This is one of the longest methods in DataAccess as it handles both the Mac and iOS platforms. It starts by checking if the class that implements the DataAccessDelegate protocol has implemented the needLogin:password: method, if it doesn't then it returns NO. Otherwise it first attempts to get the login username and password from NSUserDefaults, and then generates a login string which is passed to the loginWithString method of DARemoteDataAdapter. If that fails (or there was no username/password in NSUserDefaults) then the username and password are retrieved from the needLogin:password:, a new login string is created and passed to loginWithString. If this fails 3 times, then the login attempt is declared a failure.

  • beginApplyUpdates attempts to apply any local changes to the data to the remote server in an asynchronous way. For it to work properly, you first need to specify which tables require their changes to be saved, you do that by adding them to the NSMutableArray, a, which will then be passed to the beginApplyChangesForTables of DARemoteDataAdapter. The DataAccess class is then added as a delegate of the DAAsyncRequest class (as noted above, the DataAccess class implements the DAAsyncRequestDelegate protocol), this means that when remote operation is complete, the appropriate asyncRequest* method is called on DataAccess. The context property allows you to specify an ID for the request, that you can then later check for when the delegate method is called with the results of the DAAsyncRequest.

The intention is that you would call this method from your main application at a point in time that is most applicable to you or the user (for instance, it might be called explicitly by the user using an "Apply Updates" button).

  • The asyncRequest:didReceiveTable and asyncRequest:didReceiveTables delegate methods are called after the successful completion of a DAAsyncRequest request, one of the arguments is the new version of the DADataTable. While both methods appear in DataAccess, neither provides a default implementation. You need to do something with the table, be it call a delegate on another class, post a notification that another class is listening for, or update some internal data store. If multiple requests are possible, then you can filter based on the context property of DAAsyncRequest.

  • There are then three delegate methods for handling error conditions. The default implementations output the error message to NSLog and then passes the error message to the alertError: delegate method. It is expected that you would modify these methods to be more appropriate for your application:

    • asyncRequest:didFailToApplyChange:forTable: and asyncRequest:didFinishApplyingChangesForTables:withErrors are basically the same, they handle the case that an attempt to apply a delta change to the server data fails. If you attempt to update multiple tables at the same time but only implement the asyncRequest:didFailToApplyChange:forTable: delegate, then it will be called once per table.
    • asyncRequest:didFailWithException is intended to handle more general exceptions like the network connection failing.

The last section of the DataAccess class that you need to work with is #pragma mark Getting More Data section. This section is intended for you to add data request methods that will fetch additional data on demand, for instance if you want to refresh the data you received on start up (maybe the user is working with a shared shopping list) to pick up any recent remote changes. The commented out code gives an example of creating both an synchronous and asynchronous data request.

A Closer look at the warnings

As noted in the article about creating a project, if you try and build the newly created project you will get a bunch of build warnings, like those in the figures below.



These warnings aren't because we shipped a broken template; instead they are used to indicate points in the code where you need to make changes to make the project work with your server. Once you've made the required changes, you can safely delete the #warning definitions.

The warning in AppDelegate's needLogin:password: is to get you to properly implement requesting the username and password information from the user. The intention is that this will only be called when no login information is stored in Settings/the Keychain. By default it returns the stock username and password which are used for a new domain in Relativity before an alternative login provider is specified.

There is then a warning in the #defines of DataAccess.m where you need to change the SERVICE_URL to point to your server, and then set the RELATIVITY_DOMAIN and RELATIVITY_SCHEMA to match those you are using in your server.

There is a warning in downloadData of DataAccess. As noted above you need to implement this class so that it downloads the initial data you need for your application.

Then there is a warning in both saveDataToBriefcase and loadDataFromBriefcase: of DataAccess, the intention is that you specify the tables to be saved, and then correspondingly specify those that should be loaded.

In the iOS version of the project there are 3 additional warnings, which can be found in MasterViewController.m.

The first warning is in the method myTable which should return an instance of the DADataTable you are using. You would in theory rename it to something more suitable for your application.

Next there is a warning in tableView:cellForRowAtIndexPath: where you need to implement how the data should be displayed in the table's cells.

Finally you will find a warning in tableView:didSelectRowAtIndexPath:, here you should implement the method to react to cells that have been touched.