You are browsing the archive for Alex.

Avatar of Alex

by Alex

Strong Typed Rows in the DataAbstract for Cocoa

September 1, 2014 in Uncategorized

Challenge

In DataAbstract for Cocoa, you can retrieve the values for a particular table field using the KVC approach, where the field name is passed as a string key:

DADataTableRow *row = [[self table] rowAtIndex:idx];
NSString *name = [row valueForKey:@"ClientName"];

Later, with help of the new indexing syntax for arrays and dictionaries, this code can be rewritten in a more simple manner:

DADataTableRow *row = self.table.rows[idx];
NSString *name = row[@"ClientName"];

But there is one flaw to that approach – the field name is passed as a string constant and thus can’t be checked in design time or selected in the code completion system.
The user still has to work with DADataTabeRow instances, where data is represented by the dictionary, rather than by business objects.

Solution

With the help of some “Magic” in Objective-C, we can now offer another approach for getting values for a particular column in a row.
You can just operate with a particular field in the table row as with a usual property.
This means that you can call any property in a row instance like this:

id fieldValue = [self.dataTable.rows[idx] MyFieldName];

If there is no such field in the table, you will get a proper exception, otherwise you will get its value.

It would also be great though if we could choose available fields using the Code Completion system inside the IDE.
This would simplify the code and allow to avoid various possible mistakes in specifying field names when working with data of the row.

And the answer is yes, we can do that.
All we need to do is to represent the available table fields as properties of a special protocol (interface) that the row can support.

This approach can be used in two ways:
The most simple way is to just generate protocols ad later cast the row instance to the row protocol. For example:

 
// Protocol
 
@protocol DepsTableRow_Protocol <NSObject>
@property (strong) NSString *DepName;
@property (strong) NSNumber *DepId;
@property (strong) NSString *DepPhone;
@end
 
 
// Code for obtaining data 
 
id row = self.dataTable.rows[0];
id<DepsTableRow_Protocol> typedRow = row;
NSNumber *depId = [typedRow DepId];
NSString *depName = [typedRow DepName];
NSString *depPhone = typedRow.DepName;// you also can use dot(.) for accessing field property
 
NSLog(@"%@(%@): Phone:%@:", depName, depId, depPhone);

Another approach is to provide a special row class for the particular table.
This class must be inherited from the base DADataTableRow class, but in addition, it can expose different properties:

//Protocol:
@protocol DepsTableRow_Protocol <NSObject>
@property (strong) NSString *DepName;
@property (strong) NSNumber *DepId;
@property (strong) NSString *DepPhone;
@end
 
// Custom row class 
@interface DepsTableRow : DADataTableRow <DepsTableRow_Protocol>
// you don't need to implement DepsTableRow_Protocol for declaring its properties
@end
 
// Code for obtaining data:
// First we need to register our custom row class for the particular table
// It should be done once per application start 
// The init method of the DataAccess module is usually a good place for doing that
[DADataTable registerRowClass:[DepsTableRow class] forTableName:@"Deps"];
 
// then later each row in the table rows array is the instance of our custom row
DepsTableRow *typedRow = self.dataTable.rows[0];
NSNumber *depId = [typedRow DepId];
NSString *depName = [typedRow DepName];
NSString *depPhone = typedRow.DepName; // You can use a dot(.) to access fields as properties
 
NSLog(@"%@(%@): Phone:%@:", depName, depId, depPhone);

Supporting Strong Typed Rows in Nougat

The same approaches are also available for Nougat projects and Oxygene:

// Protocol
DepsTableRow_Protocol= public interface
    property DepName: strong NSString read write;
    property DepId: strong NSNumber read write;
    property DepPhone: strong NSString read write;
end;
 
// Code for obtaining data 
var row: id := self.dataTable.rows[0];
var typedRow : DepsTableRow_Protocol :=  duck<DepsTableRow_Protocol>(row);
 
var depId := typedRow.DepId;
var depName := typedRow.DepName;
var depPhone := typedRow.DepPhone;
 
NSLog("%@(%@): Phone:%@:", depName, depId, depPhone);

and for RemObjects C# (Hydrogene):

// Protocol
public interface DepsTableRow_Protocol
{
    __strong NSString DepName { get; set; } 
    __strong NSNumber DepId { get; set; }
    __strong NSString DepPhone { get; set; }
}
 
// Code for obtaining data 
id row = self.dataTable.rows[0];
DepsTableRow_Protocol typedRow = duck<DepsTableRow_Protocol>(row);
 
NSNumber depId = typedRow.DepId;
NSString depName = typedRow.DepName;
NSString depPhone = typedRow.DepPhone;
 
NSLog("%@(%@): Phone:%@:", depName, depId, depPhone);

Generating protocols

The one remaining question is how to get those row protocols?

For Nougat projects created in MS Visual Studio, these protocols will be generated automatically by the New Project Wizard.
You can also update/regenerate these protocols any time you want – just run the wizard by clicking Create Table Definition Classes in the Solution Explorer window. TableDefinitions Wizard

For Xcode projects, you can generate the protocols directly from the schema node inside the Relativity Server in the Server Explorer for Cocoa tool. It allows to generate Objective-C, Oxygene and RemObjects C# protocols for Nougat projects. TableDefinitions Generator

Samples

Take a look at the StrongTypedRows (iOS) samples written in Nougat, Oxygene and RemObjects C#. We are providing them with the DataAbstract for Cocoa package.

Thanks for reading!

Avatar of Alex

by Alex

Using DataAbstract and RemObject SDK for Cocoa libraries in Swift

September 1, 2014 in Cocoa, Uncategorized

As you probably already know, Apple recently announced a new programming language called Swift, a new and fresh look at programming for the Mac.
The main and obvious purpose of Swift is to replace Objective-C in the future.

To develop for the Mac, RemObject Software offers two products, DataAbstract for Cocoa and RemObject SDK for Cocoa.
These products are designed using native Apple technologies and are written in Objective-C.
They are available as frameworks and as static libraries.

This brings us to the important question: Can I use existing libraries written in Objective-C in a program written in Swift?

The answer to that question is yes.

In order to access classes from Objective-C libraries from Swift, you need to create a so-called Bridging Header File and name it according to your product module name followed by -Bridging-Header.h.
Then you need to edit this file to expose your Objective-C code to your Swift code.
Just import every Objective-C header you want to expose to Swift. In our case, this will be the root Data Abstract header:

//
//  DAWithSwift-Bridging-Header.h
//
 
#import <DataAbstract/DataAbstract.h>

Then go to the project settings and locate the “Swift Compiler – Code Generation” section at the “Build Settings” tab, where you need to specify the bridge header file for the “Objective-C Bridging Header” build setting.
The path must go directly to the file itself, not to the directory it’s in.
The path should be relative to your project, similar to the way your Info.plist path is specified in Build Settings.
For example:

DAWithSwift/DAWithSwift-Bridging-Header.h

Also, do not forget to set the other build options to allow your application to locate and link it with the RemObjects libraries:

Library Search Path = "/Developer/RemObjects Software/Bin/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)"
Other Linker Flags = -ObjC -all_load -lxml2 -lDataAbstract

And that’s it, now all public Objective-C headers listed in this bridging header file will be visible to Swift.

The Objective-C functionality will be available in any Swift file within that project automatically, without any import statements.
You can now use DA/RO classes with the same Swift syntax that you can use with system classes.

 
//SWIFT
 
// Set up rda and server access
var dataAdapter: DARemoteDataAdapter
let url: NSURL = NSURL(string:"http://localhost:7099/bin")
self.dataAdapter = DARemoteDataAdapter(targetURL:url)
dataAdapter.dataService.serviceName = "DataService"
println("dataAdapter: \(dataAdapter.dataService.channel.targetURL)")

At the moment, most of Swift is covered by the Apple NDA until it is released, but after that we are going to simplify even those steps above.

Thanks for reading!

Avatar of Alex

by Alex

Data Abstract Sample for Oxygene for Cocoa

June 17, 2013 in Uncategorized

Hi guys,

Recently I’ve spotted a question about Nougat samples in the support thread. Unfortunately, we didn’t offer any sample applications at that time, so I’ve tried to quickly compose one.

We’ve already proposed some New Project templates for Data Abstract for Cocoa though:

Nougat Templates

Using these templates, and after going through several steps in the wizard, we can get an almost complete data-aware application (we just need to review and adjust the code in several places marked with a #warning directive).

So I’ve started with this New Project wizard and created a client for the PCTrade sample domain exposed by Relatvitiy server.

In this sample, I want to show several things:

  • Working with Relativity server.
  • Downloading data at the client side in an asynchronous way.
  • Using Cocoa NotificationCenter.
  • Master-Detail navigation in the data.
  • Using Calculated fields for the DADataTable.
  • Conditional formatting of the table view cells.
  • The application should be universal and support both iPhone and iPad.

At the same time, I’ve tried to keep things as simple as possible to make it easy to understand common ideas.

So let’s start:

Working with Relativity domains

Almost all implementation required for connecting to the Relativity server, including autentication at the Relativity side, is done by the template and new project wizard. Most important places in the code are highlighted with the #warning directive. For example, in the DataAccess.pas file you can see the following place:

    {$WARNING  Define your server address and Relativity domain name, below.}
    const SERVER_URL  =                    'http://192.168.0.22:7099/bin';
    const SERVICE_NAME =                   'DataService';
    const RELATIVITY_DOMAIN  =             'PCTrade Sample';
    const DOMAIN_SCHEMA  =                 'PCTrade';

Here you can adjust the URL of your Relativity server (my server uses http://192.168.0.22:7099/bin).
These constants will also be used for configuring the RemoteDataAdapter instance.

Another place that requires our attention is in the AppDelegate.pas and holds the passwords to log in to the Relativity domain:

method AppDelegate.needLogin(var aLogin: String) password(var aPassword: String): Boolean;
begin
  // The DataAccess automatically takes care of storing login information in Settings and Key Chain
  // and retrieving it as necessary. This method is only called when no login is stored (i.e. on first
  // run) or if the retrieved login was rejected by the server.
  // Typically, you would implement this method to bring up the UI that asks the user for his credentials.
 
  // By default, new Relativity Server domains will use "Data"/"Relativity" as login, unless
  // the login provider setup has been changed by the administrator.
  aLogin := 'Data';
  aPassword := 'Relativity';
 
  result := true; // return YES if the user provided login details, or NO if the user canceled
end;

Of course, in the real application, you can implement a popup window here with to enter login and password, or even get credentials from the application preferences, but I’m trying to keep it simple here.

Downloading data

Again, like in the point above, almost all implementation for downloading data from the server was generated automatically by the new project wizard.

You may notice that all the tables we selected in one of the last wizard step, were created with the appropriate properties in DataAccess.pas and code has been generated to fill them.

method DataAccess.downloadData;
begin
  // ToDo: Add code to download data from server
  // You will need to define fields and, if necessary, properties for your tables.
 
  var tableNames := new NSMutableArray<String>;
  tableNames.addObject('Clients');
  tableNames.addObject('OrderDetails');
  tableNames.addObject('Orders');
 
  var tables := fRDA.getDataTables(tableNames);
 
  Clients := tables['Clients'];
  OrderDetails := tables['OrderDetails'];
  Orders := tables['Orders'];
end;

It is important to note here that the given downloadData method is called asynchronously (in a separate thread), so we get a responsive and non-frozen UI.

method DataAccess.loadInitialData;
begin
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), method begin
      ...
      downloadData();
      ...
      dispatch_async(dispatch_get_main_queue(), -> triggerDataReady);

Using notifications

Since we are downloading data in a separate thread, we need to notify our main UI thread with the results.
And here the Cocoa Notification Center comes in to help us.
In the code above you may have notice that after downloading the data, we perform a triggerDataReady call at the main thread. This method just posts a notification to the Notification Center that the job is done.

method DataAccess.triggerDataReady;
begin
  dataReady := true;
  NSNotificationCenter.defaultCenter.postNotificationName(NOTIFICATION_DATA_READY) object(self);
  NSLog('data is ready.');
 
  // ToDo: Add any processing you want to do once the data is fully ready
end;

Thus, if our controller needs to be informed about loading data results, we need to subscribe it to this notification. You can find the code in the ClientsViewController.pas file.

 
method ClientsViewController.viewDidLoad;
begin
  inherited viewDidLoad;
  // Do any additional setup after loading the view, typically from a nib
  ...
  NSNotificationCenter.defaultCenter.addObserver(self) 
                                     &selector(selector(dataReady:)) 
                                     name(DataAccess.NOTIFICATION_DATA_READY) 
                                     object(DataAccess.sharedInstance);
  ...
end;
 
method ClientsViewController.dataReady(notification: NSNotification);
begin
  tableView.reloadData();
end;

That means that when our ClientsViewController receives the DataReady notification, the dataReady: method must be invoked, which then refreshes the content of the table view.

Master-Detail navigation

This feature is part of the New Project template. By default, it generates already configured Master and Detail view controllers and the corresponding storyboards files with proper transitions between views inside.

Concrete implementation depends on device type.
For iPhone/iPod devices with their relatively small screens, when user selects a certain cell, the proper transition will be invoked and we can hook on it and set the info for the detail controller.

method ClientsViewController.prepareForSegue(segue: UIStoryboardSegue) sender(sender: id);
begin
  if segue.identifier.isEqualToString('showOrders') then begin
    var ip := tableView.indexPathForCell(sender);
    var tableRow := self.dataTable.rowAtIndex(ip.row);
    segue.destinationViewController.setDetailItem(tableRow);
  end;
end;

For the iPad device, the master and detail view are visible at the same time – no transition required. So we can just hook at the didSelectRow event and provide the details data for the detail controller as shown below:

method ClientsViewController.tableView(tableView: UITableView) didSelectRowAtIndexPath(indexPath: NSIndexPath);
begin
  if UIDevice.currentDevice.userInterfaceIdiom = UIUserInterfaceIdiom.UIUserInterfaceIdiomPad then begin
    var tableRow := self.dataTable.rowAtIndex(indexPath.row);
    OrdersViewController.detailItem := tableRow;
  end;
end;

About the details data: Since all necessary data is already present at the client-side, we are performing just a local data filtering using NSPredicates:

method DataAccess.OrdersByClient(client: String): NSArray;
begin
  var filterPredicate := NSPredicate.predicateWithFormat('Client == %@', client);
  result := self.Orders.rowsFilteredUsingPredicate(filterPredicate);
end;

Using Calculated fields for the DADataTable

As you might know, we can easily extend any existing DADataTable with our custom lookup and calculated fields which could help us get various related information. For this sample I need to know the amount of orders for each of our clients and also want to know the total for each client order (which can be calculated by summing the OrderDetails rows).

There is the special setupData method in the DataAccess class. It will be called while loading the initial data, so I can be sure that my tables will be extended in a proper way.

Declaring calculated fields is easy, we just need to specify the field name, its type and the target and selector for the method that will be used for calculation.

method DataAccess.setupData;
begin
  // Use this method to implement any setup that is needed on your data tables, such as
  // creating lookup or calculated fields, etc.
  self.Clients.addCalculatedFieldName('OrdersCnt') 
                            dataType(__enum_DADataType.datInteger) 
                              target(self) 
                           &selector(selector(calculateOrdersCntForClientRow:)); 
 
  self.Orders.addCalculatedFieldName('OrderSum') 
                             dataType(__enum_DADataType.datCurrency) 
                               target(self) 
                            &selector(selector(calculateSumForOrderRow:)); 
end;

As a rule, calculation methods take a row instance as the parameter and return an object (id) as the result of the calculation. So here is the sample of our calculation method:

method DataAccess.calculateSumForOrderRow(row: DADataTableRow): id;
begin
  // Get the Order ID value from the row 
  var orderID: Integer := row.valueForKey('OrderId').integerValue;
  // Prepare filter to obtain details on given order
  var condition: NSPredicate := NSPredicate.predicateWithFormat('Order == %d', orderID);
  // Get the order total
  var orderSum: Double := self.OrderDetails.rowsFilteredUsingPredicate(condition).valueForKeyPath('@sum.Total').doubleValue();
 
  // Wrap it into an id object and return
  exit NSNumber.numberWithDouble(orderSum);
end;

Conditional cell formatting

It has become common practice to give a slightly different design to each cell depending on the data it represents. For example, the developer can play with font, size, color, etc. All this makes the data more obvious and easy to perceive.

We will use a similar approach for our application. We already have the method tableView() cellForRowAtIndexPath(): UITableViewCell which composes the cell for any table row we want to show.

method ClientsViewController.tableView(tableView: UITableView)
                 cellForRowAtIndexPath(indexPath: NSIndexPath): UITableViewCell;
begin
  var CellIdentifier := "Cell";
  // Try to obtain cached cell
  result := tableView.dequeueReusableCellWithIdentifier(CellIdentifier);
 
  // This is required only if you are targetting iOS 5.1 or lower
  if not assigned(result) then
    // If there is no such cell cached, then just create a new one
    result := new UITableViewCell withStyle(UITableViewCellStyle.UITableViewCellStyleValue1) 
                                  reuseIdentifier(CellIdentifier);  
  
  // Note that we used UITableViewCellStyleValue1 as the UITableViewCellStyle
  // which makes the cell with the value aligned to the left side and the detail value aligned to the right side of the cell
  var tableRow := self.dataTable.rowAtIndex(indexPath.row);
  result.textLabel.text := tableRow['ClientName'];
 
  // Get the count of orders (as you remember, OrdersCnt is the calculated field)
  var ordersCnt: Integer := tableRow['OrdersCnt']:integerValue();
 
  // And depending on the orders count we can use different colors and different icons for the given cells
  case ordersCnt of
    0: begin 
        result.detailTextLabel.textColor :=  UIColor.blueColor();
        result.imageView.image := UIImage.imageNamed('1');
        result.detailTextLabel.text := 'No orders'; 
    end;
    1: begin
      result.detailTextLabel.textColor := UIColor.orangeColor();
      result.imageView.image := UIImage.imageNamed('2');
      result.detailTextLabel.text := NSString.stringWithFormat('%d order', ordersCnt);
    end;
    2: begin
      result.detailTextLabel.textColor := UIColor.magentaColor();
      result.imageView.image := UIImage.imageNamed('3');
      result.detailTextLabel.text := NSString.stringWithFormat('%d orders', ordersCnt);
    end;
    else begin
       result.detailTextLabel.textColor :=  UIColor.redColor();
       result.imageView.image := UIImage.imageNamed('4');
       result.detailTextLabel.text := NSString.stringWithFormat('%d orders', ordersCnt);
    end;
  end;
  
end;

As a result of our efforts above, we now have the following application:

Nougat DA for iOS application

And finally, as a small bonus, this is how it looks in iOS7 ;)

Nougat DA for iOS application iOS7

You can get sources of the sample download here

Thanks for your attention! ;)

Avatar of Alex

by Alex

Data Abstract for Cocoa: New methods for adding rows

June 5, 2013 in iOS, Mac

In the last release we have introduced two new methods in the DADataTable which allows adding new row with data in one go

The first one is public -addNewRowWithDataFromDictionary:inEditMode:

It returns new row with values from specified dictionary which contains key-value pairs with field name as the key and value – as value for given field.

NSDictionary *data = @{
  @"WorkerBirthDate" : date,
  @"WorkerLastName"  : @"Doe",
  @"WorkerPosition"  : @"Boss",
  @"WorkerFirstName" : @"John"
};
 
DADataTableRow *row = [table addNewRowWithDataFromDictionary:data inEditMode:NO];

Since it takes a dictionary, and we know to what field given value belongs, then there is no matter in what order you will put the data – it can differs from order of fields in the table.

This method also perform some basic validation. If you will specify value for field which does not belongs to the table then it throws an exception saying that given field does not exists in the table.

I also need to say few words about adding rows for tables with autoincrement fields. As you might know, when you insert new row to the table with autoincrement key, Data Abstract, at the client side, will provide temporary key value. It is an negative integer value in order not to interfere with existing positive values.

-addNewRowWithDataFromDictionary:inEditMode: method does not allow you to specify custom value for such autoincrement key. Though it does not firbid to change temporary key value before sending an updateData request.

To avoid ambiguity, if you include value for autoinc field to the dictionary then adding new row will throw an exception saying that you cannot specify custom value for autoincrement field.

So general recomendation here will be – If your table has an autoincrement key field then just omit passing its value. It will be generated automatically.

And finally you don’t need to pass values for ALL fields. You can specify only ones you need. It also can be handy for setting defaults.

The second method -addNewRowWithDataFromArray:inEditMode: is less safe. It just takes an array of values for new row. Thus, orders of values here plays the very important role. There is no special validation and checkings of the passed arrays. So you must be sure what you are passing there.

For these reasons, we make this method as internal. You can use it when import proper header file like shown in code snippet below.

The benefit in using this method is in its simplicity and conciseness.

#import <DataAbstract/Internal.h>
...
NSArray *data = @[
   [NSNull null],
   @"Bond",
   @"James",
   date,
   [NSNull null],
   @"Spy"];
 
DADataTableRow *row = [table addNewRowWithDataFromArray:data inEditMode:NO];

As I’ve said, order of values here is important so when I want to skip value for certain field then I need to pass a null object there.

Avatar of Alex

by Alex

New features in Schema Modeler 7

December 12, 2012 in Data Abstract, Windows

As you might know, we are switching to the new Schema Modeler tool in our Data Abstract products. We wanted to make it better, handier and up-to-date with the latest conceptions introduced in Data Abstract. In the last release we fixed some bugs and added a set of new features. Today I want to highlight some of these features. So let’s get started.

UI improvements to simplify navigation

During the beta, we’ve had some complaints that navigation in the new Schema Modeler is more complex than in the old one. In order to see and select a schema table field, we need to expand the Table node, then expand the Fields node, and after that we will see the particular fields. We tried to simplify this by adding a new Content section to the bottom of the schema table view.

Inside this section you can see the table content, including fields, parameters and statements. You can quickly navigate to any particular item – just double-click it. You can navigate to fields, parameters and statements collection nodes by clicking the corresponding hyperlinks (you can find them above the container).
And finally, you can easily create and remove table elements easily from the table view. There are two buttons at the top right corner of each element view.

See video

Tip: You can select several fields and remove them in one go, rather than deleting them one by one.

There are similar improvements planned for the schema command view in the next release.

Improved fields collection view

Now it shows size, scale, precision, and many other field properties. It also highlights read-only fields with gray, so you can easily notice them.

Improved recreating or updating schema table fields

We added the ability to choose a statement for recreating or updating schema table fields.
The previous version used a selected statement, which was not obvious and handy.

If the schema table has only one statement, it will be used for recreating fields automatically. But for cases where your table has several statements, you will see a dialog where you can choose the statement you want to use.

See it in action

Connection Types

Some time ago in the Data Abstract v5, we introduced the new concept of the ConnectionType, which replaces specifying the exact connection with the more general Connection Type. It allows us to implement a more flexible way of working with several connections in the application.

The new Schema Modeler simplifies setting the connection type for the statement by offering a list of available types in the combo-box. You don’t need to remember what connection types your connection manager contains. Just select the appropriate one from the list.

See this in action

Loading connection content by demand

We have implemented a lazy loading pattern for Connections Manager documents in the new Schema Modeler, which is more efficient and decreases memory consumption.

See this in action

Added ability to open *.daConnections files

Now we can open connection files by double-clicking in the Windows Explorer, or in the Visual Studio Solution Explorer, and edit them separately from the schema.

Working with *.daRemoteSchema files

daRemoteSchema files are just a reference to a remote Data Abstract server. Schema Modeler can open these files: it connects to the server and downloads the schema from there. If the server requires a login or is encrypted with an AES envelope, Schema Modeler shows a login dialog where you can provide the login and password.

Since we cannot update a schema on the server in runtime, daRemoteSchema project opens in Read-only mode and can be used for informational purposes only.

See this in action

Working with *.RelativityClient files

A RelativityClient file is the another type of shortcut file that can reference a particular schema in a particular Relativity domain. The important point here is the necessity to work with connections using Relativity abilities only. Actions like obtaining metadata, a list of database objects or driver information should be performed by the Relativity application. Saving changes in the schema and connection manager will lead to uploading data directly to the Relativity server, no local or temporary files will be created.

See this in action

Working with Internal connections

Internal connections is a quite new concept in the Relativity world, representing a general connection that can be managed by Relativity only. Its main aim is to abstract the details of connections even for Relativity administrators. With an internal connection, it doesn’t matter what driver is used for working with data. It can be SQLite, or a more exotic driver, Relativity will know how to deal with it.

Improved automatic mappings regeneration

Now it understands SQL field aliases and generates proper mappings for it.

See this in action

Improved generation delta commands for schema tables

Now it creates commands and sets them for the table.

See this in action

So far that’s all for the current release.

If you have any remarks or ideas, please let us know.
Thanks!

Avatar of Alex

by Alex

Showing hierarchical data in the NSOutlineView

June 29, 2012 in Data Abstract, Mac, Xcode

Today I want to talk about representing hierarchical data in a Mac application.

Cocoa offers the nice UI control NSOutlineView for this.
As the hierarchical data we can use a Data Abstract Schema instance which can contain various tables, fields, parameters, statements and many other objects.

Getting the DASchema

Retrieving the DASchema object from a Data Abstract server is quite simple:

NSString *serviceName = @"DASampleService";
NSString *urlString = @"http://MyServer:8099/bin";
NSURL *url = [NSURL URLWithString:urlString];
 
DARemoteDataAdapter *rda = [[DARemoteDataAdapter alloc] initWithTargetURL:url];
[[rda dataService] setServiceName:serviceName];
 
[rda beginGetSchemaWithBlock:^(DASchema *result) {
    [self setSchema:result];
}];

Defining our tree structure

The first thing we need to do is specify our tree-like hierarchy.
In order to simplify the sample, we will not implement the complete DASchema structure, only parts of it. The root object will be presented by the schema object with the tables and commands collection as children. Each table has a collection of fields as children.

So in addition to the existing DASchema elements, we need to declare some virtual elements in our tree. The schema, for example, has a collection of tables and commands. To avoid them being messed up, we want to have the collections separately, each in its own subfolder. The same is true for the fields collection inside each schema table.

These virtual classes should expose their name, note, icon and collection of children. All this info will be used by NSOutlineView for building the tree.

 
//------------------------------------------------------
//
//  OutlineNode.h
//
 
#import <DataAbstract/DASchema.h>
 
@interface DASchemaItem : NSObject
-(NSString *)name;
-(NSString *)note;
-(BOOL)isLeaf;
-(NSImage *)image;
-(NSArray *)children;
@end
 
@interface DASchemaCollection : DASchemaItem {
  DASchemaBaseObject *parent;
}
-(id)initWithParent:(DASchemaBaseObject *)aParent;
@end
 
@interface DASchemaDataTableCollection : DASchemaCollection
@end
 
@interface DASchemaDataTableFieldCollection : DASchemaCollection
@end
 
@interface DASchemaCommandCollection : DASchemaCollection
@end
 
//------------------------------------------------------
//
//  OutlineNode.m
//
#import "OutlineNode.h"
 
@implementation DASchemaItem
 
-(BOOL)isLeaf {
  return NO;
}
 
-(NSImage *)image {
  return nil;
}
 
-(NSArray *)children {
  return nil;
}
 
-(NSString *)name {
  return @"<collection>";
}
 
-(NSString *)note {
  return @"<virtual element>";
}
 
@end
 
@implementation DASchemaCollection
 
-(id)initWithParent:(DASchemaBaseObject *)aParent {
  self = [super init];
  if (self != nil) {
    parent = (DASchemaBaseObject *)aParent;
  }
  return self;
}
 
- (void) dealloc {
  [super dealloc];
}
 
-(NSImage *)image {
  return [NSImage imageNamed:@"Collection.png"];
}
 
@end
 
@implementation DASchemaDataTableCollection
 
-(NSArray *)children {
  return [(DASchema *)parent tables];
}
 
-(NSString *)name {
  return [NSString stringWithFormat:@"TABLES (%ld)",
           [[(DASchema *)parent tables] count]];
}
 
@end
 
@implementation DASchemaDataTableFieldCollection
 
-(NSArray *)children {
  return [(DASchemaDataTable *)parent fields];
}
 
-(NSString *)name {
  return [NSString stringWithFormat:@"Fields (%ld)",
          [[(DASchemaDataTable *)parent fields] count]];
}
 
@end
 
@implementation DASchemaCommandCollection
 
-(NSArray *)children {
	return [(DASchema *)parent commands];
}
 
-(NSString *)name {
	return [NSString stringWithFormat:@"COMMANDS (%ld)",
            [[(DASchema *)parent commands] count]];
}

We then need to extend some DASchema classes with the same generic methods as above (which return information about children and icons). Fortunately, Objective-C allows to do that quite easily with help of Categories.

 
// Append it to OutlineNode.h
 
@interface DASchemaBaseObject (OutlineNode)
-(NSArray *)children;
-(BOOL)isLeaf;
@end
 
//------------------------------------------------------
// Append it to OutlineNode.m
 
@implementation DASchemaBaseObject (OutlineItem) 
 
-(NSImage *)image {    
  return nil;
}
 
-(NSArray *)children {
   return nil;
}
 
-(BOOL)isLeaf { 
  return [[self children] count] < 1;
}
 
@end
 
@implementation DASchema (OutlineItem) 
 
-(NSArray *)children {  
  DASchemaCollection *tables = 
    [[[DASchemaDataTableCollection alloc] initWithParent:self] autorelease];
  DASchemaCollection *commands = 
    [[[DASchemaCommandCollection alloc] initWithParent:self] autorelease];
  NSArray *result = [NSArray arrayWithObjects:tables, commands, nil];
  return result;
}
 
@end
 
@implementation DASchemaDataTable (OutlineItem)
 
-(NSArray *)children {    
  DASchemaCollection *fields = 
    [[[DASchemaDataTableFieldCollection alloc] initWithParent:self] autorelease];
  NSArray *result = [NSArray arrayWithObjects:fields, nil];
  return result;
}
 
-(NSImage *)image {    
  return [NSImage imageNamed:@"Table.png"];
}
 
@end
 
@implementation DASchemaDataTableField (OutlineItem)
 
-(NSArray *)children {  
    return nil;
}
 
-(NSImage *)image {
  if (self.isPrimaryKey)
    return [NSImage imageNamed:@"KeyField.png"];
  return [NSImage imageNamed:@"RegularField.png"];
}
 
@end
 
@implementation DASchemaCommand (OutlineItem)
 
-(NSArray *)children {
  return nil;
}
 
-(NSImage *)image {  
  return [NSImage imageNamed:@"Command.png"];
}
 
@end

Now we are ready to bind our schema object to NSOutlineView. I prefer to do that using the Cocoa Bindings approach. It requires almost no coding and the whole job can be done in the Interface builder that loads in the working area when we select the xib file.

Binding NSOutlineView

As the data source for the NSOutlineView, we can use the NSTreeController class. You can locate it in the Object Library, then drag and drop it to the document designer.

After that, select the recently added tree controller object and go to the Utilities view Attribute Inspector . In the Tree Controller section, we need to set the key paths for obtaining the children collection and for defining if the tree node is a leaf or can be expanded.

Also in the Object Controller section, we need to set the Class as the Mode and the DASchemaBaseObject as the Class Name , then add name & note as Keys. These keys will be available for selecting later when we will bind the outline view columns to this tree controller.

Go to the Bindings Inspector and bind the tree controller to the children method of the schema property.

Now the tree controller is configured and we can bind the outline view columns to the key paths exposed by the tree controller.
Bind the SchemaItem column.

Then bind the Note column.

And that’s all. When we run the application, we will get something like this:

Note that Cocoa Bindings provides two-way binding, so we can edit tree node items just in outline view without any additional efforts.

Adding Images to the outline view items

You may notice on the screens above that NSOutlineView shows its content with images. To do that, we’ve reused the ImageAndTextCell class taken from the SourceView Sample.

We have included it to our sample project and selected it as the custom class of the outline view column cell.

We then need to declare the support of the NSOutlineViewDelegate protocol for our window controller and implement the
outlineView:willDisplayCell:forTableColumn:item: method which will be called by the outline view control before drawing its cells.

- (void)outlineView:(NSOutlineView *)outlineView
    willDisplayCell:(id)cell
     forTableColumn:(NSTableColumn *)tableColumn
               item:(id)item {
    // draw images for first column only
    if ([@"Schema Item" compare:[[tableColumn headerCell] title]] == NSOrderedSame) {
 
        NSImage *image = nil;
        if ([item respondsToSelector:@selector(image)])
            image = [item image];
        else
            if ([item isKindOfClass:[NSTreeNode class]])
                image = [[item representedObject] image];
 
        ImageAndTextCell *c = (ImageAndTextCell *)cell;
        [c setImage:image];
    }
}

The source code for the sample described in this article can be downloaded from here:
OutlineViewSample.zip

Thanks for your attention.

Avatar of Alex

by Alex

Using Remote Commands with Data Abstract for Xcode

June 5, 2012 in Data Abstract, iOS, Mac, Xcode

Recently, we have added some new functionality to the DARemoteDataAdapter for working with Data Abstract Commands.

Let me briefly refresh your memory about what Commands are and when they can be helpful.

Data Abstract Commands are part of the DASchema and represent a reference to certain Database Stored Procedures or just keep custom SQL expressions we need to execute.

The most common commands perform INSERT/UPDATE/DELETE actions for given tables.
In addition, they can be used to perform calculations in the database and return the results to the user.

Before now, we could only perform command calls by using the DataAbstractService_Proxy directly.
I’ve posted the workaround for that some days ago Support for RemoteCommands in DA/Xcode?.

But with the next release it will be much simpler.

To execute commands from the sample mentioned above, we just have to perform the following code:

[remoteDataAdapter executeCommand:@"Insert_BPWorkers" 
      withParameterValuesAndNames:@"John1",        @"FirstName",
                                  @"Doe1",         @"LastName",
                                  @"444-22-33-22", @"Phone",  
                                  [NSDate date],   @"BirthDate",
                                  @"Manager",      @"Position", nil];

The same can easily be done in an asynchronous way.

But let me show you how it works on a more complex sample with stored procedures that have output parameters.
Imagine you have an MSSQL database with the following simple TestStoredProcedure:

CREATE PROCEDURE [dbo].[TestStoredProcedure] 
  @groupId INT = 0, 
  @name nvarchar(255) OUTPUT,
  @COUNT INT OUTPUT
AS
BEGIN
  SET NOCOUNT ON;
  SELECT TOP 1 @name = p.Name FROM Products p WHERE p.GroupId = @groupId;
  SELECT @COUNT = COUNT(*) FROM Products p WHERE p.GroupId = @groupId;
  RETURN 157;
END

As you have noticed, this stored procedure returns @name & @count as output parameters and some “magic number” as the result. So let’s use it.

Note
Don't forget that Data Abstract doesn't work with the database objects directly, 
but with schema objects (tables, commands etc).

If you need to get back the output parameters, you need to use the ExecuteCommandEx method.

Beside the usual arguments like command name and parameters, it takes another NSDictionary as the output argument which will contain the output values.

The code for the asynchronous call will look something like this:

 
-(IBAction)callTheCommandEx:(id)sender {
  NSString *COMMAND_NAME = @"TestStoredProcedure";
  NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:10], @"groupId", nil];     
  DAAsyncRequest *request = [dataAdapter beginExecuteCommandEx:@"TestStoredProcedure"
                                                withParameters:params 
                                                         start:NO];
  [request setDelegate:self];
  [request start];
}
 
     - (void)asyncRequest:(DAAsyncRequest *)request 
didFinishExecutingCommand:(NSString *)commandName 
               withResult:(int)result 
          andOutputParams:(NSDictionary *)outParams
{
    NSLog(@"Called ExecuteCommandEx for %@ with result %i", commandName, result);
    NSLog(@"%@", outParams);
}

Or the same but using blocks:

-(IBAction)callTheCommandEx:(id)sender {
  [dataAdapter beginExecuteCommandEx:@"TestStoredProcedure"
                      withParameters:params 
                           withBlock:^(int result, NSDictionary *outParams) {
                                        NSLog(@"Called ExecuteCommandEx for %@ with result %i", @"TestStoredProcedure", result);
                                        NSLog(@"%@", outParams);
                                     }];

In both cases we’ve got the following results:

Called ExecuteCommandEx for TestStoredProcedure with result -1
{
    "RETURN_VALUE" = 157;
    count = 42;
    name = "APC ProtectNet PNET1GB";
}
Avatar of Alex

by Alex

Updating Data in iOS Client Applications

October 26, 2011 in Cocoa, Data Abstract, iOS, Mac, Relativity, Uncategorized, Xcode

As promised earlier, I’m going to tell you how to change data in an iOS application and apply changes to the DA Server. As client application we will use the iOS Relativity Client we have previously created.

In the course of this session we will try to implement an editor for changing data from the Orders table. We will also use UIPickerView & UIDatePicker for changing the Order’s fields.
The source code can be obtained from here.
Let’s start!

The first thing we need to do is to add a new editor view and its controller.
After performing File->New…->New File, select the “UIViewController subclass” item. As the base class we will use the UITableViewController.
Now we need to add an edit button to the existing OrdersViewController. Clicking on this button should open an editor for the selected row.

- (void)viewDidLoad {
    [super viewDidLoad];
 
    // create & set Edit button    
    UIBarButtonItem *actionButton = 
     [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemEdit 
                                                   target:self 
                                                   action:@selector(edit:)];
    [[self navigationItem] setRightBarButtonItem:actionButton];
    [actionButton release];
    ...
}
 
- (IBAction)edit:(id)aSender {
 
    OrderEditorController *editor = 
      [[[OrderEditorController alloc] initWithStyle:UITableViewStyleGrouped] autorelease];
 
    // set OrderViewController as the delegate to be able to notify it about the results of the editing process
    [editor setDelegate:self];
 
    // find the currently selected row
    NSIndexPath *path = [self.tableView indexPathForSelectedRow];
    NSInteger idx = [path indexAtPosition:1];
    DADataTableRow *row = [orderRows objectAtIndex:idx];
 
    // and set it for our editor 
    [editor setOrderRow:row];
 
    // push the editor view to be a topmost view
    [[self navigationController] pushViewController:editor animated:YES];
}
 
// delegate the method that the editor will call for applying changes
-(void)endEditWithOk:(DADataTableRow *)row {
    // applying changes on Relativity server
    [[DataAccess sharedInstance] applyUpdatesForTable:[row table]];
    // reload view for synchronizing client-side data
    [[self tableView] reloadData];
}
 
// delegate the method that the editor will call for canceling changes
-(void)endEditWithCancel:(DADataTableRow *)row {
    // cancel the change
    [row cancel];
    // reload table
    [[self tableView] reloadData];
}

Now it is time to start implementing our OrderEditorController. The Orders table has several fields we can edit: OrderDate and OrderStatus, but we can also change the client of the order, though I don’t think we need to change the document in such a radical way ;) .

Note: There is one small problem we need to resolve first. Unfortunately, the current version of the PCTrade database doesn’t have an OrderStatus reference, so the Orders table only has its integer values (which by default are 0).
Let’s assume that we need to implement several available document statuses. A new uncompleted order usually has the “New” status. When it is completed but not payed yet, it has a “Pending” status. When we received the money, it changes to “Closed” and finally, there is the additional “Deleted” status. In the future we probably may want to track the document history, so we loose any of the orders, even if they were deleted. So our database will keep those orders, but with the “Deleted” status. In order to keep the correspondence between the available status numbers and their names, we will add a new NSArray into our DataAccess class as shown below.

/////////////////////////////
//  DataAccess.h
@interface DataAccess : NSObject <DARemoteDataAdapterDelegate, DAAsyncRequestDelegate>
{
    ...
    NSArray *orderStatusNames;
}
...
@property (readonly) NSArray *orderStatusNames;
-(NSUInteger)idForOrderStatusName:(NSString *)name;
-(NSString *)nameForOrderStatus:(NSUInteger)status;
@end
 
/////////////////////////////
//  DataAccess.m
 
- (id) init {
	self = [super init];
	if (self) {
		...
 
        orderStatusNames = [[NSArray alloc] initWithObjects:
                         @"New", 
                         @"Pending", 
                         @"Closed", 
                         @"Deleted", 
                         nil];
 
	}
	return self;
}
 
- (void) dealloc {
   ...  
   [orderStatusNames release], orderStatusNames = nil;
   [super dealloc];
}
 
 
-(NSString *)nameForOrderStatus:(NSUInteger)status {
 
    if (status >= [orderStatusNames count])
        return @"Unknown state";
 
    return [orderStatusNames objectAtIndex:status];
}
 
-(NSUInteger)idForOrderStatusName:(NSString *)name { 
 
    for (NSUInteger i = 0; i < [orderStatusNames count]; i++) {
        NSString *statusName = [orderStatusNames objectAtIndex:i];
        if ([statusName compare:name] == NSOrderedSame) 
            return i;
    }
    return 0;
}
 
-(NSArray *)orderStatusNames {
 
    return orderStatusNames;
}

Now we are ready to start the editor implementation. Please open the OrderEditorController.h file and add the following:

@interface OrderEditorController : UITableViewController {
 
    DADataTableRow *orderRow;
 
    UIBarButtonItem *saveButton;
    UIBarButtonItem *cancelButton;
 
    id delegate;
    NSDateFormatter *df; 
}
 
@property (retain) DADataTableRow *orderRow;
@property (assign) id delegate;
 
- (IBAction)save:(id)aSender;
- (IBAction)cancel:(id)aSender;
@end

Our editor should have references to the edited row, the delegate reference, the save & cancel buttons and the appropriate methods. This will implement them:

#pragma mark - View lifecycle
 
- (void)viewDidLoad {
    [super viewDidLoad];
 
    // create & add cancel button    
    cancelButton = [[UIBarButtonItem alloc] initWithTitle:@"Cancel" 
                                                    style:UIBarButtonItemStylePlain 
                                                   target:self action:@selector(cancel:)];
    self.navigationItem.leftBarButtonItem = cancelButton;
 
    // create & add save button    
    saveButton = [[UIBarButtonItem alloc] initWithTitle:@"Save" 
                                                  style:UIBarButtonItemStyleDone 
                                                 target:self action:@selector(save:)];
    self.navigationItem.rightBarButtonItem = saveButton;
 
    // we will also need this formatter for converting NSDate to NSString 
    df = [[NSDateFormatter alloc] init];
    [df setDateStyle:NSDateFormatterMediumStyle];
}
 
- (IBAction)save:(id)aSender {
 
    if ([delegate respondsToSelector:@selector(endEditWithOk:)])
        [delegate endEditWithOk:orderRow];
 
    [self.navigationController popViewControllerAnimated:YES];
 
}
 
- (IBAction)cancel:(id)aSender {
 
    if ([delegate respondsToSelector:@selector(endEditWithCancel:)])
        [delegate endEditWithCancel:orderRow];
 
    [self.navigationController popViewControllerAnimated:YES];
}

In the majority of cases, we can represent the editor view as the table view, where each cell represents the field name at the left side and the current value at the right side. We can also group certain fields together based on general meaning or application logic. To do that we have to initialize our OrderEditorController with the UITableViewStyleGrouped style. When the user taps on a certain cell, we will need to allow him to edit that value.

So now we need to configure our table view as shown below:

 
#pragma mark - Table view data source
 
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    // we want to have two groups, 
    // one for OrderDate & OrderStatus, and one for the Client field 
    return 2;
}
 
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    switch (section) {
        case 0:
            // the first section will have two cells (showing OrderDate & OrderStatus)
            return 2;
        case 1:
            // the second section will have a single cell (showing the Client field)
            return 1;
 
        default:
            return 0;
    }
}
 
static NSString *TABLE_CELL_ID = @"OrderEditorCell";
 
- (UITableViewCell *)tableView:(UITableView *)tableView 
         cellForRowAtIndexPath:(NSIndexPath *)indexPath {
 
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:TABLE_CELL_ID];
    if (cell == nil) {
      cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 
                                     reuseIdentifier:TABLE_CELL_ID] autorelease];
    }
 
    // Configure the cell...
    int section = [indexPath section];
    int index = [indexPath indexAtPosition:1];
    switch (section) {
      case 0: { // for the first section ...
        switch (index) {
          case 0: { // configure the cell to show the OrderDate field
                    [[cell textLabel] setText:@"Order Date:"];
                    NSDate *value = [orderRow valueForKey:@"OrderDate"];
                    NSString *valueAsString = value ? [df stringFromDate:value] : @"Click to select";
                    [[cell detailTextLabel] setText:valueAsString];
                    break;
          }
          case 1: { // configure the cell to show the OrderStatus field
                    [[cell textLabel] setText:@"Order Status:"];
                    NSNumber *value = [orderRow valueForKey:@"OrderStatus"];
                    NSString *valueAsString = value ? 
                         [[DataAccess sharedInstance] nameForOrderStatus:[value intValue]] : 
                         @"Click to select";
                    [[cell detailTextLabel] setText:valueAsString];
                    break;
          }
        }
        break;
      }
      case 1: { // for the second section
        switch (index) {
          case 0: { // configure the cell to show the Client field
                    [[cell textLabel] setText:@"Client:"];
                    NSString *value = [orderRow valueForKey:@"CustomerName"];
                    NSString *valueAsString = value ? value : @"Click to select";
                    [[cell detailTextLabel] setText:valueAsString];
                    break;
          }
        }
        break;
     }
     default:
       break;
    }
    [cell setNeedsDisplay];
	return cell;
}

Now, when you compile and run the project, you should see something like the following:

Next we can start to implement the editing for the OrderDate and OrderStatus fields. For editing dates we can use the really good NSDatePicker control with the appropriate configuration. For editing the OrderStatus we can use our custom picker populated with available statuses.
I also want the picker view to appear at the bottom of the current editor using animation, like the standard Contacts application does. Having spent some time with Google, I’ve found an interesting Data Cell sample with the required functionality. I’ve just slightly adjusted it for our purposes, namely to support animation for custom pickers.

- (void)viewDidLoad {
 
  ...
  // create & configure our DatePicker
  datePickerView = [[UIDatePicker alloc] init];
  [datePickerView setDatePickerMode:UIDatePickerModeDate];
  [datePickerView addTarget:self 
                     action:@selector(pickerDidChange) 
           forControlEvents:UIControlEventValueChanged];
 
  pickerDoneButton = [[UIBarButtonItem alloc] initWithTitle:@"Done" 
                                                      style:UIBarButtonItemStyleDone 
                                                     target:self action:@selector(pickerDone)];
  pickerCancelButton = [[UIBarButtonItem alloc] initWithTitle:@"Cancel" 
                                                        style:UIBarButtonItemStyleDone 
                                                       target:self action:@selector(pickerCancel)];
}
 
#pragma mark - Common picker routines
-(void)showPicker:(UIPickerView *)picker {
 
    if (!picker)
        return;
 
    if (activePicker) 
        [self hidePicker:activePicker];
 
    activePicker = picker;
 
    if (picker.superview == nil) {
        [self.view.window addSubview:picker];
 
        // size up the picker view to our screen and compute the start/end 
        // frame the origin for our slide-up animation
        // compute the start frame
        CGRect screenRect = [[UIScreen mainScreen] applicationFrame];
        CGSize pickerSize = [picker sizeThatFits:CGSizeZero];
        CGRect startRect = CGRectMake(0.0,
                                      screenRect.origin.y + screenRect.size.height,
                                      pickerSize.width, pickerSize.height);
        picker.frame = startRect;
 
        // compute the end frame
        CGRect pickerRect = CGRectMake(0.0,
                                       screenRect.origin.y + screenRect.size.height - pickerSize.height,
                                       pickerSize.width,
                                       pickerSize.height);
        // start the slide-up animation
        [UIView beginAnimations:nil context:NULL];
        [UIView setAnimationDuration:0.3];
 
        // we need to perform some post operations after the animation is complete
        [UIView setAnimationDelegate:self];
 
        picker.frame = pickerRect;
 
        // shrink the table's vertical size to make room for the date picker
        CGRect newFrame = self.tableView.frame;
        newFrame.size.height -= picker.frame.size.height;
        self.tableView.frame = newFrame;
        [UIView commitAnimations];
 
        // add the "Done" & "Cancel" buttons to the nav bar
        self.navigationItem.rightBarButtonItem = pickerDoneButton;
        self.navigationItem.leftBarButtonItem = pickerCancelButton;
    }
}
 
-(void)removePicker:(UIPickerView *)picker {
 
    if (!picker)
        return;
 
    if (picker.superview != nil) {
        [picker removeFromSuperview];    
    }
}
 
-(void)hidePicker:(UIPickerView *)picker {
 
    if (!picker)
        return;
 
    activePicker = nil;
 
    CGRect screenRect = [[UIScreen mainScreen] applicationFrame];
    CGRect endFrame = picker.frame;
    endFrame.origin.y = screenRect.origin.y + screenRect.size.height;
 
    // start the slide-down animation
    [UIView beginAnimations:nil context:picker];
    [UIView setAnimationDuration:0.3];
 
    // we need to perform some post operations after the animation is complete
    [UIView setAnimationDelegate:self];
    [UIView setAnimationDidStopSelector:@selector(animationDidStop:finished:context:)];
 
    picker.frame = endFrame;
    [UIView commitAnimations];
 
    // grow the table back again in vertical size to make room for the date picker
    CGRect newFrame = self.tableView.frame;
    newFrame.size.height += picker.frame.size.height;
    self.tableView.frame = newFrame;
 
    // remove the "Done" button and restore the nav bar
    self.navigationItem.rightBarButtonItem = saveButton;
    self.navigationItem.leftBarButtonItem = cancelButton;
 
    // deselect the current table row
    //NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
    //[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
 
}
 
-(void)animationDidStop:(NSString *)animationID 
               finished:(NSNumber *)finished 
                context:(void *)context {
    [self removePicker:context];
}
 
// called when we click the Done button
-(void)pickerDone {
 
    // change the data
    if (activePicker == (UIPickerView *)datePickerView)
        [orderRow setValue:datePickerView.date forKey:@"OrderDate"];
 
    // hide picker
    [self hidePicker:activePicker];
    // reload table view to show changes
    [[self tableView] reloadData];
}
 
// called when we click on the Done button
-(void)pickerCancel {
    // just hide the picker
    [self hidePicker: activePicker];
    [[self tableView] reloadData];
}
 
// the following method will sync the current picker value with the value in the table view
- (void)pickerDidChange {
 
    NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
    UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
 
    if (activePicker == (UIPickerView *)datePickerView)
        cell.detailTextLabel.text = [df stringFromDate:datePickerView.date];
}

Now we need to launch the UIDatePicker when a user taps on the OrderDate field:

- (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
 
    [self hidePicker:activePicker];
    int section = [indexPath section];
    int index = [indexPath indexAtPosition:1];
 
    switch (section) {
      case 0: {
        switch (index) {
          case 0: {
            datePickerView.date = [orderRow valueForKey:@"OrderDate"];
            [self showPicker:(UIPickerView *)datePickerView];
            break;
          }
          case 1: {
 
            break;
	  }
        }
      }
    }
}

When you compile and run the application, you should get something like the following:

Finally, we need to implement our custom UIPickerView, which is quite simple. At the beginning we need to create a custom UIPickerView and set its data source and delegate properties:

- (void)viewDidLoad {
    ...    
    statePickerView = [[UIPickerView alloc] init];
    [statePickerView setDataSource:self];
    [statePickerView setDelegate:self];
    [statePickerView setShowsSelectionIndicator:YES];
}

Then we need to add a support UIPickerViewDataSource protocol to our editor class and implement several methods, as shown below:

 
#pragma mark - Custom Picker routines
 
// callback method for filling content for our custom statuses picker
- (NSString *)pickerView:(UIPickerView *)pickerView 
             titleForRow:(NSInteger)row 
            forComponent:(NSInteger)component {
 
    if (pickerView == statePickerView)
        return [[DataAccess sharedInstance] nameForOrderStatus:row];
    return @"";
}
 
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView{
 
    // it should be a simple picker with a single component
    if (pickerView == statePickerView)
        return 1;
    return 0;
}
 
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
 
    if (pickerView == statePickerView)
        return [[[DataAccess sharedInstance] orderStatusNames] count];
    return 0;
}

Change the tableView:didSelectRowAtIndexPath: method to show our state picker:

#pragma mark - Table view delegate
 
- (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
   ...    
    switch (section) {
      case 0: {
        switch (index) {
          case 0: {
            ...
          }
          case 1: {
            [self showPicker:statePickerView];
            break;
          }
        }
      }
    }
}

That’s all. Now when you tap on the status cell of the editor, you should get something like this:

You can choose a new status, then click the done button and finally save the current order.

I’m slightly worried that you may find this blog post a little confusing and not easy to reproduce. We wrote quite a lot of code here, and I deliberately skipped some minor details, trying to make it easier, but you can download the whole source code from here and play with the project yourself.

Thank you for your attention ;)

Avatar of Alex

by Alex

Creating a Simple iOS Client for Relativity Server

September 23, 2011 in iOS, Relativity, Xcode

Today I want to talk about how to create a simple iPhone client application for Relativity server using the DataAbstract for Xcode templates. Though the focus will be on Xcode 4, the same mechanisms can be applied to Xcode 3.

Going through a set of actions, we will use a DataAbstract iOS Application template to create an application with several views representing a Master/Detail relation.

Let’s start with the first step.

Creating a new project using a DataAbstract iOS Application template

Open your Xcode application and perform the File>New>New project… command.
In the opened sheet, choose the RemObjects Software category from the iOS templates section and select the Data Abstract iOS Application template.

Specify the product name and don’t forget to set the Talk to Relativity Server option.

Finally, select the destination folder for your new project and press the Create button. Your new project should now be open:

Let’s take a few seconds to review what you have at the moment. You should have a compilable application (with some warnings – we will review them a bit later) that has an AppDelegate class and a RootViewController with its xib. There should also be a DataAccess class, which will be responsible for all manipulation with data. The new project also has all the necessary linker flags and properly configured paths as shown in the screen-shot below:

Implementation of the iOS client

Now you can compile the application, after which you will see a set of warnings:

These warnings were generated by the template system to highlight places that require adjusting or proper implementation to get your application working. Let’s walk through all of them and eliminate them one by one.

1. Adjusting the connection configuration

First, let’s change some defaults in the generated DataAccess class to specify the Relativity server URL, domain and schema. For this example, let’s assume that you have the Relativity server running on a Windows computer with the IP 192.168.0.150.

Open the DataAccess.m file and specify the proper URL for the Relativity server, domain and schema.
You can comment the warning there, since we are done with it (as shown below).

//#warning Define your server address and Relativity domain name, below.
#define SERVER_URL		@"http://192.168.0.150:7099/bin"
#define SERVICE_NAME		@"DataService" // Do not change when using Relativity server
 
#define RELATIVITY_DOMAIN	@"PCTrade Sample"
#define RELATIVITY_SCHEMA	@"PCTrade"

2. Defining tables you want to work with

Since the DataAccess class encapsulates all functionality needed for working with data, you need to define your tables there. Let’s define them as retain properties. See the code below:

@interface DataAccess : NSObject <DARemoteDataAdapterDelegate, DAAsyncRequestDelegate>
{
    ...    
    DADataTable *clientsTable;
    DADataTable *ordersTable;
    DADataTable *orderDetailsTable;
}
 
@property (retain) DADataTable *clientsTable;
@property (retain) DADataTable *ordersTable;
@property (retain) DADataTable *orderDetailsTable;

Don’t forget to add the proper @syntesize directive and release the properties in the dealloc method in the DataAccess.m file.

#pragma mark -
#pragma mark Properties
@synthesize clientsTable, ordersTable, orderDetailsTable;
...
- (void) dealloc {
    ...
    [clientsTable release], clientsTable = nil;
    [ordersTable release], ordersTable = nil;
    [orderDetailsTable release], orderDetailsTable = nil;
 
	[super dealloc];
}

3. Implementing the initial loading of data

Now let’s implement the method for the initial loading of data from the Relativity server.
Locate the downloadData method of the DataAccess class and add the following code:

Note: Our templates contain various vital tips and explanations on how to use a particular method. Please carefully review this kind of information, it can be very useful.

- (void)downloadData {
	//#warning Implement this method to download any data your application needs on first start
 
	NSArray *tableNames = [NSArray arrayWithObjects: 
                           CLIENTS_TABLE_NAME, 
                           ORDERS_TABLE_NAME, 
                           ORDERDETAILS_TABLE_NAME, nil];
 
	NSDictionary *tables = [rda getDataTables:tableNames];
 
	[self setClientsTable:[tables valueForKey:CLIENTS_TABLE_NAME]];
	[self setOrdersTable:[tables valueForKey:ORDERS_TABLE_NAME]];
	[self setOrderDetailsTable:[tables valueForKey:ORDERDETAILS_TABLE_NAME]];
}

Note that we used a synchronous call to getDataTables here, because the whole downloadData method will be executed in a background thread (i.e. asynchronously).

4. Adding briefcase support to your application

In order to allow your application to work in offline mode, you need to persist downloaded data somewhere on the device. Let’s use the DABriefcase functionality for this.
Locate the loadDataFromBriefcase: and saveDataToBriefcase: methods in the DataAccess class and implement them as shown below:

- (void)loadDataFromBriefcase:(DABriefcase *)briefcase {
 
	//#warning Implement this method to re-load your data from the briefcase when re-launched
 
	[self setClientsTable:[briefcase tableNamed:CLIENTS_TABLE_NAME]];
	[self setOrdersTable:[briefcase tableNamed:ORDERS_TABLE_NAME]];
	[self setOrderDetailsTable:[briefcase tableNamed:ORDERDETAILS_TABLE_NAME]];
}
 
- (void)saveDataToBriefcase:(DABriefcase *)briefcase {
	//#warning Implement this method to save your data to the briefcase
 
	[briefcase addTable:[self clientsTable]];
	[briefcase addTable:[self ordersTable]];
	[briefcase addTable:[self orderDetailsTable]];
 
	// Uncomment this to finalize briefcase support
	// (without this value written, DataAccess will ignore the briefcase when starting up, 
        // see threadedLoadInitialData)
	[briefcase.properties setValue:BRIEFCASE_DATA_VERSION forKey:BRIEFCASE_DATA_VERSION_KEY];
}

5. Extending the Orders table with lookup and calculated fields

Let’s look at the orders table structure. It only has a minimal set of the fields like id, client id, date and status. That’s not a lot and it would be be great to see (for example) the customer name instead of its ID and the total sum for the given order (which could be calculated from the OrderDetails table). Toachieve this, you’ll have to add lookup and calculated fields to the Orders table. Locate the setupData method of the DataAccess class and add the following implementation:

- (void)setupData {
 
	// Use this method to implement any setup that's needed on your data tables, such as
	// creating lookup or calculated fields, etc.
 
        [[self ordersTable] addCalculatedFieldName:@"OrderTotal" 
                                          dataType:datCurrency
                                            target:self
                                          selector:@selector(calculateSumForOrder:)];
 
	[[self ordersTable] addLookupFieldName:@"CustomerName" 
                                   sourceField:[[self ordersTable] fieldByName:@"Client"] 
                                   lookupTable:[self clientsTable]
                                lookupKeyField:[[self clientsTable] fieldByName:@"ClientId"] 
                             lookupResultField:[[self clientsTable] fieldByName:@"ClientName"]];
}
 
-(id)calculateSumForOrder:(DADataTableRow *)row {
 
	double result = 0;
	NSPredicate *p = [NSPredicate predicateWithFormat:@"Order == %@", [row valueForKey:@"OrderId"]];
	NSArray *rows = [[orderDetailsTable rows] filteredArrayUsingPredicate:p];
	for (DADataTableRow *orow in rows) {
		double summ = [(NSNumber *)[orow valueForKey:@"Total"] doubleValue];
		result = result + summ;
	}
	return [NSNumber numberWithDouble:result];
}

6. Checking the login and password stuff in your Application delegate

Relativity server provides secured services protected by login and password. Thus, when asking for data the first time, a SessionNotFound exception will be raised on the Relativity side to tells you that you need to login to the Relativity domain first. When you log in to it, it will be able to find your registered session and allow you to proceed. The iOS client application can catch this kind of exception and provide a login operation in the background, and, after successful login, it will repeat the last failed request. When you open the PCTradeClientAppDelegate.h file you will see that this class supports the DataAccessDelegate protocol, which has a needLogin:password: method that will be used for obtaining the proper login and password.
Since you are mostly using default settings, you can leave the already generated method untouched. It should look like this:

- (BOOL)needLogin:(NSString **)login password:(NSString **)password {
 
	//#warning Implement this method to ask the user for login information when needed
 
	// The DataAccess automatically takes care of storing login information in Settings and Key Chain,
	// and retrieving it as necessary. This method is only called when no login is stored (i.e. on first
	// run) or if the login retrieved was rejected by the server.
	// Typically, you would implement this method to bring up a UI that asks the user for his credentials.
 
	// By default, new Relativity Server domains will use "Data"/"Relativity" as login, unless
	// the login provider setup has been changed by the administrator.
 
	*login = @"Data";
	*password = @"Relativity";
 
	return YES; // return YES if the user provided login details, or NO if the user canceled
}

7. Implementing the RootViewController

You can now configure the root view to show the list of clients:

Open the RootViewController.m file and locate the myTable method that specifies the main data table for the whole current view. Since the default name myTable is a bit nondescript, let’s use the Refactoring>Rename option to rename it viewData.
Now add the following implementation:

- (DADataTable *)viewData {
 
	//#warning Implement this method to return the table this view will work on
	return [[DataAccess sharedInstance] clientsTable];
}

Also, let’s give the root view a more meaningful title.
By default, the root view is called “Root View”. Let’s change it to something more appropriate:
Add a string define with the title and use it in the viewDidLoad method as shown below:

#define VIEW_TITLE @"Our Clients"
 
- (void)viewDidLoad 
{
	[super viewDidLoad];
	[self setTitle:VIEW_TITLE];
...

Now you can compile the project and see the results in the iPhone simulator; it should look something like this:

8. Adding a detail view “Orders”

As next step, let’s implement the detail view that will show orders for each particular client.
First, add a new UITableViewController class with its xib file.

Make sure that your controller is based on the UITableViewController class.

The controller will take the DADataTableRow instance with the client info and then obtain and show a list of orders for the given client.

Our OrdersViewController.h will have something like the following:

@interface OrdersViewController : UITableViewController {
 
    NSArray *orderRows;
    DADataTableRow *clientRow;
}
 
@property (retain) DADataTableRow *clientRow;
@end

9. Obtaining an orders list for a given client

Now you need to hook on the moment when the client row will be set as the clientRow property and obtain the list of orders. Let’s write a custom setter method for this:

-(void)setClientRow:(DADataTableRow *)value {
 
    // set the new client row
    [value retain];
    [clientRow release];
    clientRow = value;
 
    // obtain the list of orders
    [orderRows release];
    orderRows = [[[DataAccess sharedInstance] ordersForClientID:[clientRow valueForKey:@"ClientId"]] 
retain];
 
    // reload data in the UI
    [[self tableView] reloadData];
    [self setTitle:[clientRow valueForKey:@"ClientName"]];
}

Now is a good time for adding the ordersForClientID: method inside our DataAccess class.
Its implementation is rather simple. Since the DataAccess already has the whole orders table, your task will be to extract only the orders for the given client.
Let’s use NPredicate for this:

- (NSArray *)ordersForClientID:(NSString *)clientID {
 
    NSPredicate *condition = [NSPredicate predicateWithFormat:@"Client == %@", clientID];
    return [ordersTable rowsFilteredUsingPredicate:condition];
}

Finally, let’s implement what you want to expose in the clients list.
Locate the method and implement it as shown below:

// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath  {
	// ToDo: Configure the cell. You may want to simply feed data from your table 
        // into a regular UITableViewCell
	// or provide your own customized cell imlementation to display the data in a richer format.
 
	static NSString *CellIdentifier = @"Cell";
	UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
	if (cell == nil) {
             cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault 
                                            reuseIdentifier:CellIdentifier] autorelease];
	}
 
	//#warning Implement this method to customize how your data is displayed
	DADataTableRow *row = [rows objectAtIndex:indexPath.row];
	[[cell textLabel] setText:[row valueForKey:@"ClientName"]];
 
	return cell;
}

10. Implementing the data presentation for the orders view

Now let’s form an appropriate representation of the data for each customer order with minimal effort, meaning that we avoid creating custom cells for this table and just use one of predefined styles provided by Apple.

Locate and open the tableView:cellForRowAtIndexPath: method of the OrdersViewController.m file and change it like this:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
 
    static NSString *CellIdentifier = @"OrdersCell";
 
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1
                                       reuseIdentifier:CellIdentifier] autorelease];
    }
 
    // Configure the cell...
    NSDateFormatter *df = [[[NSDateFormatter alloc] init] autorelease];
    [df setDateStyle:NSDateFormatterMediumStyle];
 
    NSNumberFormatter *cf = [[NSNumberFormatter alloc] init];
    [cf setNumberStyle:NSNumberFormatterCurrencyStyle];
 
    DADataTableRow *r = [orderRows objectAtIndex:indexPath.row];
    NSDate *orderDate = [r valueForKey:@"OrderDate"];
    NSNumber *orderSum = [r valueForKey:@"OrderSum"]; 
    [[cell textLabel] setText:[df stringFromDate:orderDate]];
    [[cell detailTextLabel] setText:[cf stringFromNumber:orderSum]];
    return cell;
}

Here I should note that we used UITableViewCellStyleValue1 as the cell style, which allows us to compose the main text label with the order date at the left side and the detail label with order total at the right label of the cell. Also please note that the OrderSum field is the calculated field you have added earlier.

I propose to improve the cell view for your root view a bit and show the client’s phone as detail information. Locate the tableView:cellForRowAtIndexPath: method in the RootViewController.m file and change it to something like this:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath  {
	// ToDo: Configure the cell. You may want to simply feed data from your table
        // into a regular UITableViewCell
	// or provide your own customized cell imlementation to display the data in a richer format.
 
	static NSString *CellIdentifier = @"Cell";
	UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
	if (cell == nil) {
		cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle 
                                               reuseIdentifier:CellIdentifier] autorelease];
	}
 
	//#warning Implement this method to customize how your data is displayed
	DADataTableRow *row = [rows objectAtIndex:indexPath.row];
	[[cell textLabel] setText:[row valueForKey:@"ClientName"]];
        [[cell detailTextLabel] setText:[row valueForKey:@"ContactPhone"]];
        return cell;
}

Let’s compile and test our application. It should show the “Our Clients” view with a list of clients, and when you tap on a particular client it will switch to the Orders view which will show orders for the tapped client.

Looks good, doesn’t it? But there’s still one thing to improve: for clients without any orders, it shows an empty orders view. But there is an easy way to change that.

11. Final adjustments.

Let’s define whether the given client has any orders, and if it does, let’s add a disclosure button for this cell, so you will know if there is an orders view to show for the given customer or not.

To do this, let’s add another calculated field to your clientsTable as shown below:

- (void)setupData {
	...    
    [[self clientsTable] addCalculatedFieldName:@"OrderCnt" 
                                      dataType:datInteger
                                        target:self
                                      selector:@selector(calculateOrdersCountForClient:)];
	...
}
 
-(id)calculateOrdersCountForClient:(DADataTableRow *)row {
 
	NSPredicate *p = [NSPredicate predicateWithFormat:@"Client == %@", [row valueForKey:@"ClientId"]];
	NSArray *rows = [[ordersTable rows] filteredArrayUsingPredicate:p];
	return [NSNumber numberWithInt:[rows count]];
 
}

Now, you can analyze this field value and make a decision whether to draw a disclosure button to the cell in the RootViewController.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
	...
    if([[row valueForKey:@"OrderCnt"] intValue] > 0)
        cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
    else
        cell.accessoryType = UITableViewCellAccessoryNone;
 
	return cell;
}

In conclusion, you will need to move the implementation of the loading orders view from
tableView:didSelectRowAtIndexPath:
to
tableView:accessoryButtonTappedForRowWithIndexPath:.

-(void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath {
    // Pass the selected object to the new view controller.
    DADataTableRow *row = [rows objectAtIndex:indexPath.row];
    [detailViewController setClientRow:row];
 
    [self.navigationController pushViewController:detailViewController animated:YES];
}

And that’s all. Compile and run the resulting application. You should have something like the following:

In my next blog post, I’ll show you how to further improve this application and add the ability to edit and save data.

Thanks

Avatar of Alex

by Alex

Changes in the Data Abstract Samples Suite

August 17, 2011 in .NET, Data Abstract, Delphi, iOS, Mac, Windows, Xcode

Before the next upcoming release, I would like to highlight several changes concerning our Data Abstract samples suite.

Provided sample domain with the Relativity Server:

With the next release, we will provide Relativity Server with a new sample domain named PCTrade Sample that will contain a single schema called PCTrade. This schema will use the PCTrade for SQLite connection. The default login and password for this domain are Data and Relativity, respectively.

With this sample domain, Relativity Server can now be used as the server part for most of our DA samples in addition to the existing DASampleServer.

For the Xcode samples, you can easily choose the server you want to connect to:

For the .NET & Delphi samples you will need to set the flag value to True in the sample code as shown in the picture below:

Please note that several .NET samples, like Business Processor Sample and Refresh Update Data Sample, do not support working with Relativity since they depend on server logic implementation.

New Login service for the DASample server:

From the beginning, our samples had a virtual login implementation used only for the identification of clients and their platforms. For the next release, we have added a new secure DataService and LoginService to the DASample server. In order to maintain compatibility, we left the existing DASampleService unsecure. So if you try to build a client using that service, you will not need a login. But we added the new DataService on the top of the DASampleService, and made it secure. Thus, if you request that service, you need to login first.