Since the introduction of GeneXus X Evolution 3, the External Objects for Native Mobile Applications can trigger GeneXus events.
Also, for an External Object to be called from a locally generated object in an Offline application, it must provide another implementation. This is because of the way the Flexible Client calls the External Object in a user event is different from the way the External Object is called when it is used in offline generated code.
This guide explains how to create an External Object that can be called from offline code and can trigger a GeneXus Event.
If you didn't already did so, please read the External Objects for iOS Devices document first.
You must provide the user-event-callable implementation allong with the offline implementation, so start there first.
As an example, we will implement a timer, that is, an external object with a method that receives a number of seconds, a message, and calls a GeneXus event when the time has elapsed, passing the message to the event.
By "online external object", we mean an external object that can be called from a user event, whether the object that calls it is online or offline.
This is the external object definition:
Note that we already added the event definition, you can ignore it by now.
Make sure you mark the method and the event as "static" in the external object definition's property.
As described in the External Objects for iOS Devices document, you need to provide a "mapper" class and the implementation. Here's the code:
@interface GXCustomExternalObjectsMapper : NSObject
- (NSString *)externalObjectClassNameForObjectName:(NSString *)name;
@end
#import "GXCustomExternalObjectsMapper.h"
@implementation GXCustomExternalObjectsMapper
- (NSString *)externalObjectClassNameForObjectName:(NSString *)name {
if ([name isEqualToString:@"TimerEO"])
return @"TimerEOActionHandler";
return nil;
}
@end
#import <GXFlexibleClient/GXFlexibleClient.h>
@interface TimerEOActionHandler : GXActionExternalObjectHandler
@end
Note: the following implementation adds the action handler, but won't call the actual timer yet, because we didn't implement the GeneXus event trigger yet. See the //TODO comment in the -handleStartTimerAction method implementation. We'll come back to that later.
#import "TimerEOActionHandler.h"
@implementation TimerEOActionHandler
#pragma mark - Overrides
+ (BOOL)canHandleAction:(id )actionExObjDesc {
if (actionExObjDesc && [@"TimerEO" isEqualToString:[actionExObjDesc actionCallObjectName]]) {
NSString *exObjMethod = [actionExObjDesc actionExternalObjectMethod];
if ([exObjMethod isEqualToString:@"Start"]) {
return YES;
}
}
return NO;
}
- (void)actionExecuteWithContext:(id)contextEntityData delegate:(id)delegate {
[super actionExecuteWithContext:contextEntityData delegate:delegate];
NSString *exObjMethod = [self.actionExObjDesc actionExternalObjectMethod];
if ([exObjMethod isEqualToString:@"Start"]) {
[self handleStartTimerAction];
}
else {
[self onFinishedExecutingWithDefaultError]; // Unknown method
}
}
#pragma mark - Handlers
- (void)handleStartTimerAction {
NSArray *actionDescriptors = [[self.actionDesc actionParametersDescriptor] actionParametersDescriptors];
if ([actionDescriptors count] != 2) {
[self onFinishedExecutingWithError:[NSError defaultGXErrorWithDeveloperDescription:@"Wrong number of parameters"]];
return;
}
NSInteger seconds = [self readIntParameter:(id<GXActionParameterDescriptor>)[actionDescriptors objectAtIndex:0] fromEntityData:self.contextEntityData];
NSString *message = [self readStringParameter:(id<GXActionParameterDescriptor>)[actionDescriptors objectAtIndex:1] fromEntityData:self.contextEntityData];
// TODO: Start the timer and trigger the user event
[self onFinishedExecutingWithSuccess];
}
@end
Let's call "offline External Object" or "offline EO" an External Object that can be called from generated code.
Before starting with the implementation, we need to provide some information in the EO definition.
In the External Object's root node, we need to add the iOS External Name and Header File Name properties:
In the Start method properties, we need to provide the iOS External Name property:
Now that everything is in place, we can provide the actual implmenetation.
First of all, create the TimerEO class as follows:
#import <GXFlexibleClient/GXFlexibleClient.h>
@interface TimerEO : GXExternalObjectBase
- (void)startTimerWithSeconds:(NSInteger)seconds message:(NSString *)message;
@end
@interface TimerEO (OfflineGenerator)
- (void)startTimer:(NSArray *)params;
@end
Note that:
- The base class is GXExternalObjectBase, this is required to trigger the GeneXus event from the external object
- The external object declares a method with the actual parameters, and then the method as will be called from the generated code in a category, where the parameters are passed in an array. This is not required, but is a good practice.
#import "TimerEO.h"
@implementation TimerEO
#pragma mark - Overrides
- (NSString *)externalObjectName {
return @"TimerEO";
}
#pragma mark - Offline Generator
- (void)startTimer:(NSArray *)params {
if (params.count >= 2) {
NSNumber *sec = params[0];
NSString *msg = params[1];
[self startTimerWithSeconds:[sec integerValue] message:msg];
}
}
#pragma mark - Public methods
- (void)startTimerWithSeconds:(NSInteger)seconds message:(NSString *)message {
gx_dispatch_sync_on_main_queue(^{
[NSTimer scheduledTimerWithTimeInterval:seconds
target:self
selector:@selector(timerDidFinish:)
userInfo:@{ @"message": message}
repeats:NO
];
});
}
#pragma mark - Notifications
- (void)timerDidFinish:(NSTimer *)timer {
NSString *msg = [[timer userInfo] objectForKey:@"message"];
[self dispatchExteralObjectEvent:@"Completed"
withParameters:@[msg]
];
}
@end
A few things to note:
- To simplify the code, there is no error handling. The type of the parameters is not checked in -startTimer:, and there is no way to now if the timer was scheduled correctly. You should add error cheching in your EO's actual implementation.
- The -startTimerWithSeconds:message: method creates the timer in the main thread. This is beacuse it may be called on a background thread that finishes before the time has been elapsed, and in that case the event won't be triggered.
When implementing your own external object, you may need to do some things on the main thread, but avoid doing this if it is not required.
- The -externalObjectName is required if the external object will trigger some GeneXus events, and should return the name of the EO as defined in GeneXus
- To trigger the GeneXus event, use the method -dispatchExteralObjectEvent:withParameters: defined in the base class.
Now that we have the actual implementation, we can go back to the action handler.
First, we need to import the implementation's header file
#import "TimerEO.h"
Then replace the //TODO coment with:
TimerEO *eo = [TimerEO new];
[eo startTimerWithSeconds:seconds message:message];
To test the implementation, we created the following objects:
Main program: True
Connectivity Support: Offline
Event TimerEO.Completed(&message)
msg(&message)
EndEvent
Event 'StartTimer'
TimerEO.Start(5, "TimerEO from user code")
Endevent
Event 'StartTimerProc'
StartTimerProc()
EndEvent
TimerEO.Start(5, "TimerEO from offline code")
You can download the source code from this sample here.
Note: you'll need a SVN client to donwload the source code, or you can browse it online in the link above.
You need to update your GeneXus installation, the "iOS\Templates\iOS_Genexus\Classes" folder must be updated with the classes detailed above.
The \iOS\iOS.impl template file must add extra steps to copy the new files to the target generation folder and transfer them to the MAC.
Following the example, the Bootstrapping section added the following items in the Templates section:
<!-- Extra files to support TimerEO -->
<Template Id="iOS_Genexus\Classes\GXCustomExternalObjectsMapper.h" Output="iOS\Genexus\Classes\GXCustomExternalObjectsMapper.h"/>
<Template Id="iOS_Genexus\Classes\GXCustomExternalObjectsMapper.m" Output="iOS\Genexus\Classes\GXCustomExternalObjectsMapper.m"/>
<Template Id="iOS_Genexus\Classes\TimerEO.h" Output="iOS\Genexus\Classes\TimerEO.h"/>
<Template Id="iOS_Genexus\Classes\TimerEO.m" Output="iOS\Genexus\Classes\TimerEO.m"/>
<Template Id="iOS_Genexus\Classes\TimerEOActionHandler.h" Output="iOS\Genexus\Classes\TimerEOActionHandler.h"/>
<Template Id="iOS_Genexus\Classes\TimerEOActionHandler.m" Output="iOS\Genexus\Classes\TimerEOActionHandler.m"/>