Base solution for your next web application
Open Closed

abp.event.on and abp.event.off: How to get it right #10344


User avatar
0
winstat created

This is not about an issue. It is to help any users struggling with events in their components, specifically

  • how to unsubscribe successfully
  • how to access the component's variables within an event handler

If you have a component that subscribes to abp events, you must unsubscribe when the component is destroyed. Otherwise, when the component subscribes again in its next instantiation, the handler will be called twice. However, getting this right is not easy. There have been some threads about this matter, but I haven’t seen any solutions that give an example of putting it all together correctly.

There are two main problems.

PROBLEM 1: abp.event.on and abp.event.off need to refer to the same instance of the handler. In the abp documentation, there is only this example of an event handler:


abp.event.on('itemAddedToBasket', function (item) {
    console.log(item.name + ' was added to basket!');
});

Unfortunately: If you use this format for abp.event.on, there is no way to ever unsubscribe using abp.event.off. The reason is that the handler is embedded as a lambda and has no reference that can be used later for unsubscribing. The documentation does mention this problem:

You can use the abp.event.off method to unregister from an event. Note that the same function should be provided so it can be unregistered. So for the example above, you must set the callback function to a variable, then use both the on and off methods.

But there is no example given. And there is no mention of the fact that, if you do define a named method for the event handler, this method is called within the scope of the EVENT instance, not within the scope of the COMPONENT instance, no matter where the method is defined. Which brings us to:

PROBLEM 2: It is quite likely that, if you subscribe to an event within a component, you will want to interact with the component in your event handler. Maybe you’ll need to set a member variable depending on the event data. But if you put a breakpoint in your event handler, you will see that “this” refers to the event class (with the methods “on” and “off”) and not to the component. Any attempt to reference, say, “this.myVariable” will fail. So the first thing the event handler must do is change the scope to that of the component. The following example shows how to put it all together.


interface AbpSubscription {
    name: string;
    handler: (...args: any[]) => void;
}

@Component({
    selector: 'my-component',
    templateUrl: './my-component.component.html'
})
export class MyComponent extends AppComponentBase implements OnInit, OnDestroy {

    subscriptions: Array<AbpSubscription> = [];
    eventContents: string;

    constructor(
        injector: Injector,
        public _zone: NgZone
    ) {
        super(injector);
    }

    ngOnInit() {
        // register all subscriptions
        this.registerSubscriptions();
    }

    ngOnDestroy() {
        // unregister all registered subscriptions
        this.unRegisterSubscriptions();
    }

    // register all subscriptions
    registerSubscriptions() {

        const self = this;

        // register handler for 'EventA' which emits one argument
        this.registerSubscription('EventA', eventAHandler);
        // this is the registered handler
        function eventAHandler(arg1: string)
        {
            // switch to the component scope
            self._zone.run(() => {
                // call the component's 'real' event handler
                self.onEventA(arg1);
            });
        }

        // register handler for 'EventB' which emits two arguments
        this.registerSubscription('EventB', eventBHandler);
        // this is the registered handler
        function eventBHandler(arg1: string, arg2: string)
        {
            // switch to the component scope
            self._zone.run(() => {
                // call the component's 'real' event handler
                self.onEventB(arg1, arg2);
            });
        }

    }

    // register a single subscription
    registerSubscription(name: string, handler: (...args: any[]) => void) {
        // activate the subscription
        abp.event.on(name, handler);
        // remember this subscription for the unRegister method
        this.subscriptions.push({
            name: name,
            handler: handler
        });
    }

    // unregister all registered subscriptions
    unRegisterSubscriptions() {
        // call abp.event.off on each subscription
        this.subscriptions.forEach(s => abp.event.off(s.name, s.handler));
        // clear the list of subscriptions
        this.subscriptions = [];
    }

    // the component's event handler for 'EventA'
    onEventA(arg1: string) {
        // we have access to the component's variables
        this.eventContents = arg1;
    }

    // the component's event handler for 'EventB'
    onEventB(arg1: string, arg2: string) {
        // we have access to the component's variables
        this.eventContents = arg1 + arg2;
    }

}


1 Answer(s)
  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi,

    Thank you for your detailed explaination. Actually, abp events are not designed to be used in Angular apps at first but we will make enhancements about this in the following versions. I have created an issue about this https://github.com/aspnetzero/aspnet-zero-core/issues/3929