Desktop

The Simple Data Operations desktop sample demonstrates the most basic & common interactions you will make to an instance of Relativity Server. It shows how to:

  • connect to the server
  • retrieve a table from the server
  • add a row to the retrieved table and apply that change to the server
  • update/change the contents of a row and apply that change to the server
  • delete a row from the table and apply that change to the server

Note that while this sample uses synchronous methods to communicate with the server, equivalent asynchronous methods are available. You can see an example of their usage in the SimpleDataOperationAsync sample.

Getting Started

The Desktop sample is typically located in /Developer/RemObjects Software/Samples/Data Abstract/Desktop/SimpleDataOperations, though you may have installed the Data Abstract for Cocoa and it's samples in another location.

To build it, you will of course need Xcode, and like all the samples provided with Data Abstract for Cocoa you will need to be running the Relativity Server with the DASamples Domain and the Simple Schema available.

Running the Sample

This sample allows you to add, edit or delete a department from the Deps table available in the Simple schema which is in the DASamples domain. The UI is split into 2 sections; a toolbar with buttons for all of the available actions and a table for displaying all of the records in the Deps table.

There are 4 buttons on the toolbar and a popup. The "Load" button retrieves the current state of the Deps table, initially the table is empty and won't be populated till the button is pressed. The "Add" button will add a new row to the table which you can edit in place, changes are only made locally until the "Apply" button is pressed. The "Remove" button will delete the currently selected row from the local copy of table, changes will not be made to the server until the "Apply" button is pressed. Finally the "Apply" button applies the local changes to the server version of the table. The "Remove" button is only available when a row is selected, and "Apply" only when the table has been modified in some way (a new row, a deleted row, an edited row).

The last item on the toolbar is the popup which contains a list of available servers discovered by the Zeroconf discovery system. When the sample is run, it starts a Zeroconf discovery service that looks for instances of Relativity Server running on the local network. Any discovered servers will be added to the popup, if no servers are discovered then the dropbox on the toolbar will be empty. You can specify a server address yourself by clicking on the popup and then clicking on "Specify custom URL...".

To edit the Name or Phone values for a particular department double clicking in the appropriate field.

Examining the Code

The focus of this section will be on the code for adding, editing and deleting a row; along with establishing a connection to an instance of Relativity Server.

The sample interacts with the Deps schema table which has has three fields:

  • DepId this is the primary key and use an AutoInc data type.
  • DepName is a string of up to 50 characters and is required for each new row
  • DepPhone is also a string of up to 50 characters, the field is optional.

If you want to examine the table and its parameters yourself, or if you want to see the data in the servers version of the table then you can use Server Explorer to examine it.

App Structure

The sample is built around four classes; AppDelegate, DataAccess, ServiceAccess, and RegisterServiceWindowController.

The DataAccess class handles everything related to interacting with the Data Abstract SDK; including retrieving data from an instance of Relativity Server and applying changes back to the server. The majority of the code we look at below comes from this class.

The ServiceAccess class handles the discovery of any instances of Relativity Server that are available on the local network using ROZeroConf which is a feature available with the Remoting SDK that Data Abstract is built upon. ServiceAccess sets up a ROZeroConfBrowser Class object which searches for any servers that broadcast a value matching the value defined for RELATIVITY_SERVICE_NAME. When a service is found, or indeed disappears, the server list is updated and the popup with the list of available servers is also updated. It also handles the registration of custom server addresses. To explore further the ServiceAccess class and Zeroconf discovery see the article: The ServiceAccess Class and Zeroconf discovery.

The RegisterServiceWindowController class is a subclass of NSWindowController which handles the UI aspects of a user manually adding a server url.

Lastly the AppDelegate class handles the primary setup of the application, registers that it will listen for notifications broadcast by the ServiceAccess and DataAccess classes, and acts as a delegate to the RegisterServiceWindowController, DataAccess and NSTableView classes.

Establishing a Connection

There are three stages to establishing a connection to an instance of Relativity Server.

The first stage sets up the basic connection to the server. We create two RORemoteServices objects (which are part of the Remoting SDK), the first is the data service which will be used to retrieve and update data and schema information. The login service defines the service that should be called if authentication is required. The loginService object is simply a copy of the dataService object, with the serviceName property changed to DEFAULT_LOGIN_SERVICE_NAME, it could be created like the dataService object, just passing in DEFAULT_LOGIN_SERVICE_NAME instead.

We then configure the DARemoteDataAdapter to use those two services by assigning them to the dataService and loginService properties of the DARemoteDataAdapter class. At this point we have a basic working connection.

The second stage is to implement the remoteDataAdapterNeedsLogin method which is declared in the DARemoteDataAdapterDelegate protocol. This method is called when the DARemoteDataAdapter determines that a login session is required; i.e. when using a method to get the table data. Here we simply call the login method of DataAccess.m.

Lastly in the login method of DataAccess.m we compose a special login string (loginString) will will be passed to the DARemoteDataAdapters loginWithString method to establish a login session. The loginWithString method returns a boolean which will be true if the login attempt was successful or false if it failed. The login string is comprised of 4 parts:

  • Username the username to attempt to login with
  • Password the password to use
  • Domain the name of the domain that the schema is located in
  • Schema the name of the schema we wish to use

In this sample the final login string would be "User=simple;Password=simple;Domain=DASamples;Schema=Simple"

NOTE If there is a problem attempting to communicate with the server, or if the schema or domain name is incorrect then an ROException will be thrown and should be handled appropriately.


- (void)setupConnectionWithService:(id)service {

    if (!service)
        return;

    RORemoteService * dataService;

    if ([service isKindOfClass:[NSNetService class]]) {
        NSNetService * netService = (NSNetService * )service;
        @try {
            dataService = [RORemoteService remoteServiceWithNetService:netService];
        }
        @catch (NSException * exception) {
            [self.delegate alertError:@"Connection problem" withDetails:exception.reason];
        }
    } else if ([service isKindOfClass:[RORemoteService class]]) {
        dataService = (RORemoteService * )service;
    }
    RORemoteService * loginService = [dataService copy];

    dataService.serviceName = DEFAULT_DATA_SERVICE_NAME;
    loginService.serviceName = DEFAULT_LOGIN_SERVICE_NAME;

    self.dataAdapter.dataService = dataService;
    self.dataAdapter.loginService = loginService;
}

- (BOOL)remoteDataAdapterNeedsLogin:(DARemoteDataAdapter *)adapter {
    return [self login];
}

- (BOOL)login {
    NSString * loginString = [NSString stringWithFormat:@"User=%@;Password=%@;Domain=%@;Schema=%@",
                             RELATIVITY_DATA_USER, RELATIVITY_DATA_PASSWORD, RELATIVITY_DOMAIN, RELATIVITY_SCHEMA];

    NSLog(@"Login with string: %@", loginString);
    return [self.dataAdapter loginWithString:loginString];
}

Retrieving a Table

To retrieve a copy of a table from the schema we need to use the getDataTable method of DARemoteDataAdapter which takes the name of the table as a string and returns a DADataTable.

Variants of getDataTable exist that allow you to retrieve only a subset of a table using an SQL statement, or Dynamic Select and Dynamic Where objects, or retrieve multiple tables at once. There are also asynchronous variants of all of the methods.

Here we spin off the call to getDataTable into a background dispatch queue which will dispatch a notification on the main queue that the data is available when getDataTable successfully returns, otherwise it displays an alert to indicate a failure.

NOTE Passing an invalid table name to the getDataTable method will result in an ROException being thrown which should be handled accordingly.

- (void)beginLoadData {

    NSLog(@"Begin obtaining the data...");
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{

        DADataTable * result = nil;

        @try {
            result = [self.dataAdapter getDataTable:@"Deps"];
        }
        @catch (NSException * exception) {
            NSString * error = @"Error when downloading data";
            NSString * message = [NSString stringWithFormat:@"%@: %@", error, [exception reason]];
            NSLog(@"%@", message);
            [self.delegate alertError:error withDetails:[exception reason]];
        }

        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"Data has been retrieved.");
            self.dataTable = result;
            [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_DATA_READY object:self];
        });
    });
}

Inserting a row

Each table is made up of multiple DADataTableRow objects. Each one representing a row in the schema table and its columns. To add a new row to the table, we use the addNewRow factory method of DADataTable which appends a new row to the table and returns a reference to it. The values for all of the columns will be initially set to null, except for any columns that use an AutoInc value in which case they will be assigned a negative value. The Id will be updated to the actual Id value after the changes have been applied to the server.

- (DADataTableRow *)addRow {
    return [self.dataTable addNewRow];
}

Setting values in a field

To set a value for a field, you use a dictionary accessor passing in the name of the key and assigning a string object as a value. For example to set the DepName field to "Security" we would use dataTableRow[@"DepName"] = @"Security".

In this sample editing of fields is tied to the table, in AppDelegate.m the NSTableViewDataSource method tableView:setObjectValue:forTableColumn:row: is implemented to retrieve the selected row, and then assigning value to the specific field (row[fieldName]).

NOTE Passing an invalid column name to dictionary accessor would result in an ROException being thrown, however here the column name is retrieved and used as the fieldName..

Remember that at this stage only the local copy of the DADataTable has the edited row until we apply those changes to the server.

//AppDelegate.m
- (void) tableView:(NSTableView *)tableView setObjectValue:(id)value forTableColumn:(NSTableColumn *)c row:(NSInteger)r {
    NSString * fieldName = c.identifier;

    DADataTableRow * row = self.table.rows[r];
    row[fieldName] = value;
}

Deleting a row

To remove a row from a table you need to use the removeRow method which takes the DADataTableRow you want to remove. The change is added to the changedRows list, which is a list of all DADataTableRows that have modifications.

There is a variant of removeRow, removeRowAtIndex, that takes an index for the row you wish to delete; note that this index value is simply the position the row has in the table (starting from 0) and has no relation to any id value.

//DataAccess.m
- (void)removeRow:(DADataTableRow *)row {
    [self.dataTable removeRow:row];
}

Applying changes to the server

As noted earlier all of the changes made like adding, editing or deleting a row are only made on the local copy of the table until we apply those changes to the server. To do that we use the applyChangesForTable of DARemoteDataAdapter, it takes a DADataTable and extracts a delta of changes made to the table and passes that to the server. A delta of changes are then received from the server and merged into the local table.

Here the call of applyChangesForTable is executed from a background dispatch queue. If the call has a problem then an exception will be thrown which should be handled, here a message is logged to the console and an alert window shown. Otherwise if the call was successful then a notification is dispatched to the main thread that the data is available which will cause the table to be reloaded.

//DataAccess.m
- (void)beginApplyChanges {
    NSLog(@"Begin applying changes...");
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{

        @try {
            [self.dataAdapter applyChangesForTable:self.dataTable];
        }
        @catch (NSException * exception) {
            NSString * error = @"Error when applying changes";
            NSString * message = [NSString stringWithFormat:@"%@: %@", error, [exception reason]];
            NSLog(@"%@", message);
            [self.delegate alertError:error withDetails:[exception reason]];

            return;
        }

        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"Changes has been applied.");
            [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_DATA_READY object:self];
        });
    });
}