HowTo: create an External Object which triggers GeneXus events in iOS (X Evolution 3)

Official Content

Introduction

Since the introduction of GeneXus X Evolution 3, the External Objects for Smart Devices 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.

Getting started

If you didn't already did so, please read the External Object for iOS Devices document first.

You must provide the user-event-callable implementation allong with the offline implementation, so start there first.

Example

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.

Online External Object

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:

HowTo_OfflineEOiOS_EODefinition

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 Object for iOS Devices document, you need to provide a "mapper" class and the implementation. Here's the code:

GXCustomExternalObjectsMapper.h

@interface GXCustomExternalObjectsMapper : NSObject
- (NSString *)externalObjectClassNameForObjectName:(NSString *)name;
@end

GXCustomExternalObjectsMapper.m

#import "GXCustomExternalObjectsMapper.h"
@implementation GXCustomExternalObjectsMapper
- (NSString *)externalObjectClassNameForObjectName:(NSString *)name {
    if ([name isEqualToString:@"TimerEO"])
        return @"TimerEOActionHandler";
    return nil;
}
@end

TimerEOActionHandler.h

#import <GXFlexibleClient/GXFlexibleClient.h>

@interface TimerEOActionHandler : GXActionExternalObjectHandler

@end

TimerEOActionHandler.m

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

Offline External Object

Let's call "offline External Object" or "offline EO" an External Object that can be called from generated code.

Definition

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:

HowTo_OfflineEOiOS_EOProperties

In the Start method properties, we need to provide the iOS External Name property:

HowTo_OfflineEOiOS_MethodProperties

Implementation

Now that everything is in place, we can provide the actual implmenetation.

First of all, create the TimerEO class as follows:

TimerEO.h

#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:

  1. The base class is GXExternalObjectBase, this is required to trigger the GeneXus event from the external object
  2. 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.

TimerEO.m

#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:

  1. 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.
  2. 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.
  3. 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
  4. To trigger the GeneXus event, use the method -dispatchExteralObjectEvent:withParameters: defined in the base class.

TimerEOActionHandler.m

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];

GeneXus test objects

To test the implementation, we created the following objects:

MainTimer (Dashboard)

Properties

Main program: True

Connectivity Support: Offline

Events

Event TimerEO.Completed(&message)
    msg(&message)
EndEvent

Event 'StartTimer'
    TimerEO.Start(5, "TimerEO from user code")    
Endevent

Event 'StartTimerProc'
    StartTimerProc()
EndEvent

StartTimerProc (Procedure)

Source

TimerEO.Start(5, "TimerEO from offline code")

Source 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.

Installation

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"/>

Was this page helpful?
What Is This?
Your feedback about this content is important. Let us know what you think.