This scope inside a callback

Hi All,

I am trying to get a ImagePicker to work. I can get an Camera to work just fine, i.e. I can take a photo and display the base64 image on the screen.

<img src="{{ base64Image }}" />

I have the following code below.

  1. I can get the alert('xbase64Img1 = ' + base64Img); to fire, but not the alert('xbase64Img2 = ' + base64Img);. I have tried it with and without the this.zone.run(() => {...});, but it still does not reach the second alert.

  2. The ImagePicker only works if I first access the Camera, and allow permission for the app to access the camera. Is there anyway of getting the ImagePicker to work alone?

Can anyone please see what I am doing incorrectly?

Thanks

import { NavController, Platform } from ā€˜ionic-angularā€™;
import { Component, NgZone } from ā€˜@angular/coreā€™;
import { Camera, ImagePicker } from ā€˜ionic-nativeā€™;
import { EmployeeService } from ā€˜ā€¦/service/EmployeeServiceā€™;
import { Employee } from ā€˜ā€¦/model/Employeeā€™;
import { UtilityService } from ā€˜ā€¦/utils/UtilityServiceā€™;
import { DummyDataService } from ā€˜ā€¦/utils/DummyDataServiceā€™;

declare var navigator: any;

@Component({
templateUrl: ā€˜build/pages/mycamera/mycamera.htmlā€™,
providers: [EmployeeService, UtilityService, DummyDataService]
})

export class MyCameraPage {

private postEmployeeData: String;
private base64Image: string;
private zone: any;
private employeeService: EmployeeService = null;
private utilityService: UtilityService = null;
private employeeModel: Employee = null;
private dummyDataService: DummyDataService = null;
constructor(employeeService: EmployeeService, utilityService: UtilityService, dummyDataService: DummyDataService) {
    this.base64Image = null;
    this.zone = new NgZone({ enableLongStackTrace: false });
    this.employeeService = employeeService;
    this.utilityService = utilityService;
    this.dummyDataService = dummyDataService;
}
cropImage() {
}
takePhoto() {
    Camera.getPicture({
        destinationType: navigator.camera.DestinationType.DATA_URL,
        sourceType: navigator.camera.PictureSourceType.CAMERA,
        allowEdit: false,
        encodingType: navigator.camera.EncodingType.JPEG,
        targetWidth: 300,
        targetHeight: 300,
        quality: 75,
        saveToPhotoAlbum: false
    }).then(imageData => {
        this.zone.run(() => {
            this.base64Image = "data:image/jpeg;base64," + imageData;
        });
    }, error => {
        console.log("ERROR -> " + JSON.stringify(error));
        alert("ERROR: " + JSON.stringify(error));
    });
}
pickImage() {
    ImagePicker.getPictures({
        maximumImagesCount: 1,
        width: 300,
        height: 300,
        quality: 75
    }).then((results) => {
        this.toDataUrl(results[0], function (base64Img) {
            alert('xbase64Img1 = ' + base64Img);
            this.zone.run(() => {
                alert('xbase64Img2 = ' + base64Img);
                this.base64Image = base64Img;
                alert('xthis.base64Image = ' + this.base64Image);
            });
        });
    }, (error) => {
        console.log("ERROR -> " + JSON.stringify(error));
        alert("ERROR: " + JSON.stringify(error));
    });
}
toDataUrl(url, callback) {
    var xhr = new XMLHttpRequest();
    xhr.responseType = 'blob';
    xhr.onload = function () {
        var reader = new FileReader();
        reader.onloadend = function () {
            callback(reader.result);
        }
        reader.readAsDataURL(xhr.response);
    };
    xhr.open('GET', url);
    xhr.send();
}
saveImage() {
    this.employeeModel = this.dummyDataService.getDummyEmployee();
    this.employeeModel.avatar = window.btoa(encodeURIComponent(this.base64Image));
    this.employeeService.saveEmployee(this.employeeModel);
}

}

I have also tried the following with no success (placing the this.zone.run(() before I format the image to base64). It calls the first alert, but not the second:

pickImage() {
    ImagePicker.getPictures({
        maximumImagesCount: 1,
        width: 300,
        height: 300,
        quality: 75
    }).then((results) => {
        this.zone.run(() => {
            this.toDataUrl(results[0], function (base64Img) {
                alert('base64Img1 = ' + base64Img);
                this.base64Image = base64Img;
                alert('this.base64Image = ' + this.base64Image);
            });
        });
    }, (error) => {
        console.log("ERROR -> " + JSON.stringify(error));
        alert("ERROR: " + JSON.stringify(error));
    });
}

From what I have been reading, I think the callback does not have the this scope, and I need to convert the callback to rather use an arrow function () =>.

I am struggling with the syntax. Can anyone help please?

        this.toBase64(results[0], function (base64Img) {

ā€¦
});

should be:

        this.toBase64({results[0]}).then((base64Img) => {?????????????

ā€¦
});

Callback

toBase64(url, callback) {
    var xhr = new XMLHttpRequest();
    xhr.responseType = 'blob';
    xhr.onload = function () {
        var reader = new FileReader();
        reader.onloadend = function () {
            callback(reader.result);
        }
        reader.readAsDataURL(xhr.response);
    };
    xhr.open('GET', url);
    xhr.send();
}

SOLVED

Using this and that

pickImage() {
    ImagePicker.getPictures({
        maximumImagesCount: 1,
        width: 400,
        height: 400,
        quality: 75
    }).then((results) => {
        this.toBase64(this, results[0], function (that, base64Img) {
            that.zone.run(() => {
                that.base64Image = base64Img;
            });
        });
    }, (error) => {
        console.log("ERROR -> " + JSON.stringify(error));
        alert("ERROR: " + JSON.stringify(error));
    });
}

and

    toBase64(that, url, callback) {
        var xhr = new XMLHttpRequest();
        xhr.responseType = 'blob';
        xhr.onload = function () {
            var reader = new FileReader();
            reader.onloadend = function () {
                callback(that, reader.result);
            }
            reader.readAsDataURL(xhr.response);
        };
        xhr.open('GET', url);
        xhr.send();
    }

I think a better way to handle this is with promises. Callbacks lead to ugly code, and passing this around explicitly is too tightly coupled. Have toBase64 take only the url and return a Promise<string>. That will also remove the need for you to be messing with zones manually, as the promise will be zone-aware.

Hi rapropos, thank you for your advise. I am pretty new at javascript . Would you mind giving me an example please?

SOLVED

pickImage() {
    ImagePicker.getPictures({
        maximumImagesCount: 1,
        width: 400,
        height: 400,
        quality: 75
    }).then((results) => {
        this.toBase64(results[0]).then((base64Img) => {
            this.base64Image = base64Img;
        });
    }, (error) => {
        console.log("ERROR -> " + JSON.stringify(error));
        alert("ERROR: " + JSON.stringify(error));
    });
}

and

toBase64(url:string) {
    var promise:Promise<string> = new Promise<string>(function (resolve) {
        var xhr = new XMLHttpRequest();
        xhr.responseType = 'blob';
        xhr.onload = function () {
            var reader = new FileReader();
            reader.onloadend = function () {
                resolve(reader.result);
            }
            reader.readAsDataURL(xhr.response);
        };
        xhr.open('GET', url);
        xhr.send();
    });
    return promise;
}
1 Like

Why is it that when i json.stringyfy the object base64 image gets removed from the result but i can still see it in the object.

imageToBase64(avatar).then((base64Img) => {
            this.user.avatar = base64Img;
        });
        this.service.update(this.user);

Once again an old thread. Iā€™m trying to achieve pretty basic stuff, but I get stuck onā€¦ basic stuffā€¦ Iā€™m trying to setup a simulation of client/server network comms. I have a networkProvider, a clientProvider and a serverProvider and a HomePage to display the activities in each. At the moment the client/server providers are pretty simple but they are deemed to evolve with xState more complex FSM, thatā€™s why they are in separate files like that. The client and server join the network and whenever the client sends something, the network forwards it to its members using a callback provided on the ā€˜joinā€™. The callback gets called fine but JS complains about reading property ā€˜macAddrā€™ of undefined. How can I get this scope to follow ? Thereā€™s a live code example at https://i3-xs-net.stackblitz.io. Clicking the ā€˜startNetCommsā€™ button will trigger the events leading to this error. Any help appreciated.

Unfortunately, it doesnā€™t seem to have source maps (or Iā€™m having trouble loading them), so all I see is transpiled JavaScript, which is sort of hard to read for me. So, Iā€™ll have to get generic here.

I used to write X11 applications. This experience made me, shall we say, less than enthusiastic about the concept of ā€œcallbacksā€ in general.

OK, I hate them with the brilliance of a thousand white-hot suns. Fortunately for both of us, you are working in an environment that has a rich library (RxJS) that completely eliminates the need for you to ever even momentarily think about another callback-based design. So letā€™s do that.

How I would design this would be something like so:

interface Address {
  // name? mac? ip? whatever uniquely identifies a host
}

interface Packet {
  dest?: Address; // using undefined for broadcast, could be explicit
  payload: string;
}

class Network {
  private backbone$ = new Subject<Packet>();
 
  join(addr: Address): Observable<Packet> {
    return this.backbone$.pipe(
      filter(pkt => _.isUndefined(pkt.addr) || _.isEqual(pkt.addr, addr)));
  }

  send(pkt: Packet): void {
    this.backbone$.next(pkt);
  }
}  

Now all hosts (servers and clients) get a custom Observable that delivers packets addressed either to them or to the broadcast address. They subscribe to it like any other Observable, unsubscribe when theyā€™re done.

1 Like

Unfortunately, it doesnā€™t seem to have source maps (or Iā€™m having trouble loading them),

Maybe this link is better

That link is better, but I still would seriously consider using RxJS aggressively, because it seems like a really perfect tool for the job to me.

If you insist on going down the road to callback hell, however, you have two major options:

  • allow clients to pass a context parameter that gets passed back to them with every callback invocation - this is what every C++ widget toolkit Iā€™ve worked with does, and clients use it to stash their this pointer
  • leave clients to manage their own execution context, which can most easily be done by wrapping things in fat arrow functions:
world.callMeBack(this.callback); // no
world.callMeBack(() => this.callback()); // yes

No I really donā€™t insist on using callbacks, itā€™s just a reflexā€¦ Iā€™ll definitely try to convert my simulation scenario with your above design suggestion. Tx a lot for your input.

I managed to convert my simulation with Observables instead of Callbacksā€¦ really niceā€¦ much simplerā€¦ tx againā€¦ I know this is just a simulation but itā€™s becoming very helpful. Iā€™d like to simulate multiple clients. I know could duplicate my client-provider to have a second one and call them client1-provider and client2-provider but thereā€™s gotta be a better way. The clients need to implement the same behavior/protocols but need to be independant in their actions. So if I may push my luck a bit, how would you modify the client-provider to allow for a second one, a third one, etc. When all of this structure is in place, I can start implementing xState machines for the server and the client.

The most important task is to clearly define what properties attach to what. My brain works in analogies (in case this has not become eye-bleedingly obvious to this point), so hereā€™s another one.

Cars have license plates and maintenance issues.
Mechanics have schedules to work on particular issues involving certain cars at certain times.

Obviously, any design where a mechanic has a license plate is a problem, but I fear thatā€™s what we currently have with ClientProvider.

So, I would take anything that can vary by what you are calling a ā€œclientā€ and Iā€™m calling a Host (because the way I would simulate a network doesnā€™t distinguish between clients and servers) and throw it into the Host interface. That would include address, name, whatever state stuff youā€™re talking about.

An outstanding question is where to put the packet Observables and subscriptions to them. This might end up being one situation where I bend my rule about dumb PODOs and smart services, and put subscription code that receives packets and modifies state into the Host itself. This doesnā€™t necessarily mean giving up on Host being an interface and turning it into a class, because you could use the Factory pattern in the NetworkProvider to spawn well-formed Hosts.

Sidebar: if you havenā€™t read the Gang of Four book, I canā€™t recommend it highly enough. Thatā€™s where I first encountered Factory and friends, and if I was stranded on the proverbial desert island and could take only three programming-related books with me, it would make the cut (along with some incarnation of Sedgewickā€™s Algorithms and the dragon book)

WOW, this is a dense answer!!! Iā€™m not familiar with Factory but I looked it up and it seemed just like a class implementing an interface, so I implemented it and now the only dependency of the Home component is the NetworkProvider, which is nice. So far I have the following

network-provider.ts

export interface Host {
    addr: string;
    start(): void ;
    send(dest:string, msg:string): void;
    receive: Function;
    getActivities():Observable<string>;
}
...
class HostFactory implements Host {
  public addr:string;
  public receive: Function = null;
  public net: NetworkProvider;  //network that created the host 
  constructor(addr:string) { this.addr=addr; }
  start(){
    this.net.join(this.addr).subscribe((p:Packet) => {
      if (p) {
        this._activitie$.next("R-"+p.sa+":"+p.da+":"+p.payload);  //log activities for curious folks
        if (this.receive) {
          this.receive(p);                                        //call host receive handler
        }
      }
    });
...
@Injectable()
export class NetworkProvider {
  public createHost(addr:string): Host {
    let h = new HostFactory(addr);
    h.net = this; 
    return h;
  }

home.ts

import { NetworkProvider, BroadcastAddress, Host, Packet } from '../../providers/network-provider';
@Component({...})
export class HomePage {
  clientA:Host;
  server:Host;
  constructor(public net:NetworkProvider...) { 
    this.clientA = net.createHost('AA');
    this.clientA.receive = (p:Packet) => { this.clientReceiveMachine(this.clientA, p); }
    this.server = net.createHost('55');
    this.server.receive = (p:Packet) => { this.serverReceiveMachine(this.server, p); }
  }
  clientReceiveMachine(h:Host, p:Packet) {
    console.log('Rcvd.hAddr:'+h.addr+' sa:'+p.sa+' da:'+p.da+' p:'+p.payload);
  }
  serverReceiveMachine(h:Host, p:Packet) {
    console.log('Rcvd.hAddr:'+h.addr+' sa:'+p.sa+' da:'+p.da+' p:'+p.payload);
      if (p.payload==='Hello') {
        h.send(p.sa, "HelloBack");
      }
  }

Even though it works fine, thereā€™s a few things leaving me uncomfortable:

  • in order for the host to send and receive packets, it needs a reference to the network, so I set it with this in createHost --> doesnā€™t feel right
  • clients and servers need a different logic when receiving packets so I use a receive:Function property on the interface --> can be seen as a handler but too similar to callbacks to feel comfortable

So once again, Iā€™m sure there is a better way.
Complete code can be found at https://stackblitz.com/edit/i3-xs-net

No, itā€™s a singleton that spawns things.

Iā€™m assuming for now that there is only a single Network. To deal with multihomed Hosts, you would have to convert addr into a list of addr/network pairs.

// this does not deal with multihomed hosts atm
export interface Host {
  addr: string;
  packets$: Observable<Packet>;
  activities$: Observable<string>;
}

@Injectable()
class HostFactory {
  constructor(private net: NetworkProvider) {}
  
  createHost(addr: string): Host {
    let rv = {addr} as Host;
    rv.packets$ = this.net.join(addr);
    rv.activities$ = rv.packets$.pipe(
        map(pkt => `R-${p.sa}:${p.da}:${p.payload}`));
    return rv;
  }

  // if needed, but i would try to make it not needed
  disposeHost(host: Host): void {
    this.net.unjoin(addr); 
  }
}

@UntilDestroy()
class HomePage {
  clientA: Host;
  server: Host;
  
  constructor(private hoster: HostProvider, private net: NetworkProvider) {
    this.clientA = hoster.createHost("AA");
    this.clientA.packets$.pipe(untilDestroyed(this))
      .subscribe(pkt => this.clientReceiveMachine(this.clientA, pkt));
    this.server = hoster.createHost("55");
    this.server.packets$.pipe(untilDestroyed(this))
      .subscribe(pkt => this.serverReceiveMachine(this.server, pkt));
  }

  serverReceiveMachine(h: Host, p: Packet) {
    console.log('Rcvd.hAddr:'+h.addr+' sa:'+p.sa+' da:'+p.da+' p:'+p.payload);
      if (p.payload==='Hello') {
        this.net.send({sa: h.addr, da: p.sa, payload: "HelloBack"});
      }
  }
}

Solved by not having hosts send and receive packets. The network does that.

Not something Host should concern itself with. Done as shown above by changing what you do when you subscribe to the packet stream coming into that Host (or derivative thereof such as activities$). All Hosts do is effectively filter all the packets on the network down to the ones where da matches that Hostā€™s addr.

Thanks again for your lightsā€¦ Iā€™m still trying to catch all the inner-workings with mods. I added a send function to the Host interface since my client app will have the host.send something and no spoofing is allowed so this function makes sure that packets are sent to the network with their own address. That part works fine so far.

The thing Iā€™m wondering is the proper way to implement functions on Factory objects like these. I can implement a send property as a function, but it lacks all parms definition and types. An alternative would be to have a sendFromThisHost method in HostFactory, but this seems weird since OOP seems more natural.

export interface Host {
  ...
  send: Function;
}
@Injectable()
export class HostFactory implements Host {
  ...
  send: Function;
  constructor(private net: NetworkProvider) {}
  createHost(addr: string): Host {
    let rv = {addr} as Host;
    rv.send = (dest, msg) => {
      rv.activities$.next("S-"+addr+":"+dest+":"+msg);
      this.net.send({sa:rv.addr, da:dest, payload:msg});
    };
  sendFromThisHost(h:Host, dest:string, msg:string):void {
    h.activities$.next("S-"+h.addr+":"+dest+":"+msg);
    this.net.send({sa:h.addr, da:dest, payload:msg});
  }

So in Home.ts I can do send on the object or sendFromThisHost on the hoster factory.

  constructor(...public hoster: HostFactory, ...) {
     ...
    this.clientA.send("FF", "Hello");
    this.hoster.sendFromThisHost(this.clientB, "FF", "Hello");
  }

Is this a matter of coding taste or is there more to it ?
tx

Iā€™m not sure I entirely understand your concern, but what I do in a somewhat similar situation is to provide a second-level Observable.

packetses$$ = new BehaviorSubject<Observable<Packet> | null>(null);

I expect you could achieve a similar effect by using the fact that Subjects can themselves subscribe to things, so you could just change which keg is feeding the single beer tap, as it were.

Sorry I fixed my original issue of ā€˜delayedā€™ join and I was editing my post while you were responding to it I guess.