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 inDARemoteDataAdapter.h
. All the methods in the delegate are optional, however you should implement one of theneedsPassword
/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 inDAAsyncRequest.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 thedidReceive*
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 theDataAccessDelegate
delegate. - The
homeFolder
specifies the folder which theDataAccess
class will use to store data. It is generated in theinit
method using the value specified inAPPLICATION_SUPPORT_FOLDER
dataReady
indicates that the data is available. It is set by the methodtriggerDataReady
which also posts theNOTIFICATION_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 aNOTIFICATION_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. InitiallyDataAccess
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
andsaveDataInBackground
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 #define
s.
#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 theBRIEFCASE_DATA_VERSION
number and when the new client attempts to load the data inthreadedLoadInitialData
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 thebriefcaseFileName
which will be used when you go to save the data locally. It then sets up a RemoteDataAdapter and sets theDataAccess
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 fromapplicationDidFinishLaunching:
inAppDelegate.m
to begin the process of loading the data.loadInitialData
fires offthreadedLoadInitialData
in a background thread and then returns control to theAppDelegate
. -
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 thentriggerDataReady
is called on the main thread and posts theNOTIFICATION_DATA_READY
notification. Note that it is intended that this method is never called directly. -
downloadData
is called bythreadedLoadInitialData
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 thegetDataTable
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'sgetDataTables
. 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:
andsaveDataToBriefcase:
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 theaddTable:
method of DABriefcase and supply aBRIEFCASE_DATA_VERSION
which is checked before the data is loaded; if you don't supply aBRIEFCASE_DATA_VERSION
then DataAccess will not load the briefcase data. Retrieving data from the briefcase is as simple as calling thetableNamed
method of DABriefcase. -
setupData
is an internal method called bythreadedLoadInitialData
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 thedataReady
flag toYES
and posts aNOTIFICATION_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 bythreadedLoadInitialData
after the data has been loaded (either from a briefcase or directly from the server) and is available for use. -
saveDataInBackground
calls fires ofsaveData
in a background thread. In the project created it is only called byasyncRequest: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 callingsaveDataToBriefcase
, and then the data is written to disk by calling thewriteBriefcase
method of DABriefcase. -
The setter method (
setBusy:
) of thebusy
property is overridden to post aNOTIFICATION_BUSYSTATUS_CHANGE
notification when the value is changed. A number of methods check the state ofbusy
, dropping out ifDataAccess
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 ofDARemoteDataAdapterDelegate
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 inDataAccess
as it handles both the Mac and iOS platforms. It starts by checking if the class that implements theDataAccessDelegate
protocol has implemented theneedLogin:password:
method, if it doesn't then it returnsNO
. Otherwise it first attempts to get the login username and password from NSUserDefaults, and then generates a login string which is passed to theloginWithString
method of DARemoteDataAdapter. If that fails (or there was no username/password in NSUserDefaults) then the username and password are retrieved from theneedLogin:password:
, a new login string is created and passed tologinWithString
. 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 theNSMutableArray
,a
, which will then be passed to thebeginApplyChangesForTables
of DARemoteDataAdapter. TheDataAccess
class is then added as a delegate of theDAAsyncRequest
class (as noted above, theDataAccess
class implements theDAAsyncRequestDelegate
protocol), this means that when remote operation is complete, the appropriateasyncRequest*
method is called onDataAccess
. 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 theDAAsyncRequest
.
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
andasyncRequest:didReceiveTables
delegate methods are called after the successful completion of aDAAsyncRequest
request, one of the arguments is the new version of the DADataTable. While both methods appear inDataAccess
, 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 ofDAAsyncRequest
. -
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 thealertError:
delegate method. It is expected that you would modify these methods to be more appropriate for your application:asyncRequest:didFailToApplyChange:forTable:
andasyncRequest: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 theasyncRequest: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.