EDIT:  block-style method signatures have been added to the SODataStoreAsync interface, as of SDK 3.0 SP08.  You may still want to implement your own wrapper for delegate operations like Logon completion, but it is highly recommended to use the standard block interfaces with the SODataStore’s.


I can’t urge you strongly enough to give blocks a try, if you have not yet adopted them in your standard programming model.


The most important principle of using blocks, is that you can maintain your memory context in-line, for asynchronous operations. This is especially powerful in the case of network requests.  When programming with response delegates, we need to handle for the characteristics of the initial request (i.e. which collection is being queried, which method is invoked) in the delegate handler.  This results in a significant amount of application-specific code being written into what is effectively the generic communication stack.  By utilizing a callback-style asynchronous method like a block, we can eliminate unique request-specific code from the network stack, and handle responses in their original contexts.  This results in code which is much more readable, debuggable, and reusable between applications.

Edit:  A concern about the standard NSNotification API’s from Apple is their propensity to create unexpected retain cycles.  There’s a seminal blog post on this topic here.  NickLockwood has written a nice extension on the addObserver: method, named FXNotifications that does a good job of eliminating this issue.  The examples below will use the FXNotifications version of the API.  To add to your project, download the repo, and include FXNotifications.h/.m into your project.


A CREATE example

Creating a block wrapper around the delegate callbacks allows you write methods like this:


-(void)createEntity:(id) entity withCompletion:(void(^)(BOOL success))completion;

The beauty of an asychronous method with a completion block, is that you can still program against a local context in-line, instead of maintaining state at the parent level with properties and instance variables.

Consider the complexity of calling the above -createEntity: method, and handling from a delegate callback, within a full-fledged application.  Most likely, you want to refresh your UI related to the entity’s collection when it completes.  If you have multiple UI’s reading from the collection, you’ll probably want to refresh the common model, instead of just adding an entity into the local view controller’s list of objects.

So, you could add an NSNotification to the -createEntity request delegate callback, which always kicks off a refresh request for a collection when an entity is updated.  But maybe you’re also segueing between screens, so a new request is going to be sent anyway.  Or maybe you’re viewing an entity set that is filtered–which request should be sent in this context?

Having the completion block on the end of the -createEntity: method allows you to control the behavior in the specific context from which the operation was started.  See this example, where a CREATE method is invoked during a segue, but the developer is able to add a snippet to refresh the model when the request is completed in the completion block.

Example with block createEntity



- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{    
     if ([[segue identifier] isEqualToString:@"ReturnInputFromAdd"]) {
          /*
          Create a new SODataEntity, populated with values obtained from the UI
           */        
          SODataEntityDefault* entity = [[SODataEntityDefault alloc] initWithType:@"RMTSAMPLEFLIGHT.Travelagency"];        
          for (id prop in self.properties) {            
               [entity.properties setObject:prop forKey:prop.name];        
          }
                
          self.travelAgency = entity;                
          if (self.travelAgency) {                        
               [[DataController shared] createEntity:self.travelAgency withCompletion:^(BOOL success) {               
                    /*
                    When the -createEntity: request completes, this code is executed
                    */
                    [[DataController shared] fetchTravelAgencyEntitySet];            
               }];        
          }    
     }
}

Implementing the wrapper method

You can use this method to wrap any delegate interface with a completion block interface, not just -scheduleRequest:delegate:

  1. Create your new method, that will be called directly from your application.  It should have a completion block, that runs at the end of the asynchronous operation, and may have a set of input parameters from the result of the operation

  2. Inside that method’s implementation, you create a unique identifier, that should ideally be derivable from the method’s parameters (so that it could be reconstructed elsewhere in the application).  In the case of -scheduleRequest: the SODataRequestParam can be used for the unique id

  3. Add a listener to [NSNotificationCenter defaultCenter] for this unique id.  Use the block version of the -addObserver: interface, so that you can add context-specific code to be executed on the response

  4. Invoke the original delegate version of the method

  5. In the delegate callback (e.g.:  SODataRequestDelegate) methods, reconstruct the unique id for the notification from step (3) from the response object, and post a notification with the response as the payload

  6. Handle the response in the NSNotification completion block from step (3).  Typically, this means parsing the response

  7. Call the completion block for your wrapper method.  Note that the 3 parameters in the completion() match the parameters in completionHandler() in step (1)

    See example:  ScheduleRequest with completionHandler.

/*

The wrapper method, created with a completion block
*/
- (void) scheduleRequest:(id)request completionHandler:(void(^)(NSArray *entities, id<SODataRequestExecution>requestExecution>, NSError *error))completion 
      
     NSString *finishedSubscription = [NSString stringWithFormat:@"%@.%@", kRequestDelegateFinished, request];

     [[NSNotificationCenter defaultCenter] addObserver:self forName:finishedSubscription object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note, id observer) {      

     [[NSNotificationCenter defaultCenter] removeObserver:observer name:finishedSubscription object:observer];


     id<SODataRequestExecution>requestExecution = note.object;    
     id<SODataResponse>response = requestExecution.response;    
     id<SODataResponseSingle>respSingle = (id) response;    
     id<SODataPayload>p = respSingle.payload;    

     SODataEntitySetDefault *entities = (id)p;            

     // return (NSArray *entities, id<SODataRequestExecution>requestExecution, NSError *error)    
     completion(entities.entities, requestExecution, nil);
     }];

     // call the original delegate-style interface
     [self.store scheduleRequest:request delegate:self];
}

/*
The original delegate callback
*/
- (void) requestFinished:(id)requestExecution {    

     // build notification tag for this request    
     NSString *finishedSubscription = [NSString stringWithFormat:@"%@.%@", kRequestDelegateFinished, requestExecution.request];        

     // send notification for the finished request    
     [[NSNotificationCenter defaultCenter] postNotificationName:finishedSubscription object:requestExecution];
}

Profit

Ultimately, you can use this wrapper method inside any number of Collection-specific requests, so that instead of tagging requests with identifiers and metadata, and filtering them within the delegate, and/or adding NSNotification listeners all over the application to respond to Collection-specific network responses, the  response to each specific request can be handled directly in the context from which it originated!  I’ll leave you with a final example, that uses the wrapper method just created to build a clean -fetch:method:

Note how this method is called in the first example above, once the CREATE operation is completed.

fetchTravelAgenciesSampleWithCompletion


/*
Returns an array of Travelagency entities
*/
-(void)fetchTravelAgenciesSampleWithCompletion:(void(^)(NSArray *entities))completion { 
      
     NSString *resourcePath = @"TravelagencyCollection";
   
     SODataRequestParamSingleDefault *myRequest = [[SODataRequestParamSingleDefault alloc] initWithMode:mode resourcePath:resourcePath];
      
     [self scheduleRequest:myRequest completionHandler:^(NSArray *entities, id requestExecution, NSError *error) {        
          if (entities) {            
               completion(entities);        
          } else {            
               NSLog(@"did not get any entities, with error: %@", error);        
          }    
     }];
}
To report this post you need to login first.

2 Comments

You must be Logged on to comment or reply to a post.

  1. Julien NICCO

    Hi Stan,

    In the line

    SODataEntityDefault* entity = [[SODataEntityDefault alloc] initWithType:@RMTSAMPLEFLIGHT.Travelagency];

    If I understand,”Travelagency” is the name of entity right ? But what does RMTSAMPLEFLIGHT ? Name of data base ?

    Regards.

    (0) 
    1. Stan Stadelman Post author

      it is the Schema Namespace. 

      See this screenshot of the top of the $metadata document where RMTSAMPLEFLIGHT.Travelagency exists.  Note that in the EntityType “Flight”, the property “flightDetails” is of Type “RMTSAMPLEFLIGHT.FlightDetails”–a similar pattern.Screen Shot 2015-08-12 at 10.52.57 AM.png

      (0) 

Leave a Reply