Do `IsA` on a variable that is an array of a class

I can test an object to be of a particular class, even if it’s a subclass:

Var vSubClassObject As cSubClass
If vSubClassObject IsA cSubClass Then
   // Will work
End If

But I can’t check to see if it’s an Array of a subclass…

Var vSubClassArray() As cSubClass
If vSubClassArray IsA cSubClass() Then
   // Will work
End If

This generates an error at compile time.

How can I test this if my array is empty? How can I know which class an array is initialize to contain?

Regards, Antoine

What’s the scenario in which your code doesn’t already know what the type should be?

Interesting question.

I thought “let’s use Introspection”

dim xArray() as TCPSocket = Array(new TCPSocket)
dim t as Introspection.TypeInfo = Introspection.GetType(xArray)
system.DebugLog "name=" + t.Name

This fails to compile with an error

Parameter “obj” expects type Object, but this is class TCPSocket.
dim t as Introspection.TypeInfo = Introspection.GetType(xArray)

What about if we use a Variant as a go-between?

dim xArray() as TCPSocket = Array(new TCPSocket)
dim v as variant = xArray
dim t as Introspection.TypeInfo = Introspection.GetType(v)
system.DebugLog "name=" + t.Name
system.DebugLog "isArray=" + t.IsArray.ToString

This seems to work:

       : name=TCPSocket()
       : isArray=True

I think this is an edge case where both isA and Introspection don’t handle array classes properly.

Edit to add: the example using a Variant also works if the array is empty:

dim xArray() as TCPSocket  // an empty array
dim v as Variant = xArray
dim t as Introspection.TypeInfo = Introspection.GetType(v)
system.DebugLog "name=" + t.Name
system.DebugLog "isArray=" + t.IsArray.ToString
1 Like

I think the reason this is so difficult is that the use case is nearly unimaginable in strictly-typed Xojo. I honestly am struggling to conceive of a situation that would call for this.

When it’s carried in a Variant.

Regards, Antoine

Very interesting! Thanks for exploring that.

Regards, Antoine

You shouldn’t. I’m using Variant to carry all the data that comes from my API, and it can be anything, from a boolean, a string, a number, an subclass of my generic object class, that has at least a hundred subclasses.

And it only started to pose problems with empty arrays…

Regards, Antoine

I see.

Is the scenario such that a single particular API call might return any type? Or is it more that call A will return a boolean, call B will return several strings, call C will return an integer, call D will return an array of subclassed objects?

Yes. And even the API has a super class API, that is subclassed for other APIs.

No, I certainly don’t want to implement a different method for all the datatype, as it all the same « return of data ».

Regards, Antoine

Now I’m curious what this API looks like. Can you share one or two of the calls?

Thanks a lot, I guess this is the answer I’m looking for.

Regards, Antoine

Public Sub CommandeModifier(iSource As cSource, iObjet As cObjet, iObjetsValeurs() As cObjetValeur)
  If iObjet = Nil Then cApp.Erreur("cApiDepot.CommandeModifier() : iObjet = Nil", Self)
  If iObjetsValeurs = Nil Or iObjetsValeurs.Count() = 0 Then cApp.Erreur("cApiDepot.CommandeModifier() : iObjetsValeurs = Nil Or iObjetsValeurs.Count() = 0", Self)
  
  Var vDonneesEntreeJson As JsonItem = cObjetValeur.Json(iObjetsValeurs, iObjet)
  
  Var vReponseTxt As String = Self.RequeteModifier(iObjet, vDonneesEntreeJson)
  If vReponseTxt = "" Then cApp.Erreur("cApiDepot.CommandeModifier() : Modifier a échouer", Self)
  
  Var vDonneesSortieJson As JSONItem
  vDonneesSortieJson = New JSONItem(vReponseTxt)
  
  // Si on ne reçoit pas de data, ça va mal
  If Not vDonneesSortieJson.HasKey(Self.kJsonDonnees) Then cApp.Erreur("cApiDepot.CommandeModifier() : Not vDonneesSortieJson.HasKey(Self.kJsonDonnees)", Self)
  
  vDonneesSortieJson = vDonneesSortieJson.Child(Self.kJsonDonnees).ValueAt(0)
End Sub


Public Sub CommandeCreer(iSource As cSource, iObjet As cObjet, iObjetsValeurs() As cObjetValeur)
  If iObjet = Nil Then cApp.Erreur("cApiDepot.CommandeCreer() : iObjet = Nil", Self)
  If iObjetsValeurs = Nil Or iObjetsValeurs.Count() = 0 Then cApp.Erreur("cApiDepot.CommandeCreer() : iObjetsValeurs = Nil Or iObjetsValeurs.Count() = 0", Self)
  
  Var vDonneesJson As JsonItem = cObjetValeur.Json(iObjetsValeurs, iObjet)
  If vDonneesJson.Count() = 0 Then _
    cApp.Erreur("cApiDepot.CommandeCreer() : vDonneesJson.Count() = 0", Self)
    
    Var vReponseTxt As String = Self.RequeteCreer(iObjet, vDonneesJson)
    If vReponseTxt = "" Then _
      cApp.Erreur("cApiDepot.CommandeCreer() : Creer a échoué", Self)
      
      vDonneesJson = New JsonItem(vReponseTxt)
      
      // Si on ne reçoit pas de data, ça va mal
      If Not vDonneesJson.HasKey(Self.kJsonDonnees) Then 
        cApp.Erreur("cApiDepot.CommandeCreer() : Not vDonneesJson.HasKey(Self.kJsonDonnees)", Self)
        Return
      End If
      
      vDonneesJson = vDonneesJson.Child(Self.kJsonDonnees)
      
      // Valider qu’on ne reçoit qu’1 objet de l’API
      If vDonneesJson.Count() <> 1 Then _
        cApp.Erreur("cApiDepot.CommandeCreer() : vDonneesJson.Count() <> 1", Self)
        
        Self.ExtraireObjetJson(iSource, vDonneesJson.ChildAt(0), iObjet)
End Sub


Public Sub CommandeEffacer(iSource As cSource, iObjet As cObjet)
  If iObjet = Nil Then cApp.Erreur("cApiDepot.CommandeEffacer() : iObjet = Nil", Self)
  
  Var vReponseTxt As String = Self.RequeteEffacer(iObjet)
  If vReponseTxt = "" Then cApp.Erreur("cApiDepot.CommandeEffacer() : Effacer a échouer", Self)
  
  'Var vDonneesJson As JSONItem
  'vDonneesJson = New JSONItem(vReponseTxt)
  '
  'If Not vDonneesJson.IsArray Or vDonneesJson.Count <> 0 Then App.Deboguage("cApiDepot.CommandeEffacer() : Not vDonneesJson.IsArray Or vDonneesJson.Count <> 0", Self)
End Sub


Public Sub CommandeLire(iSource As cSource, iObjet As cObjet)
  If iObjet = Nil Then cApp.Erreur("cApiDepot.CommandeLire() : iObjet = Nil", Self)
  
  'Var oObjets() As cObjet
  Var vSource As cSource = Self.SourceParClasseEnum(iObjet.Classe.Nom.ToClasseEnum())
  
  // Effectuer la requête HTTP
  #Pragma BreakOnExceptions False
  Var vReponseTxt As String = Self.RequeteLire(iObjet)
  If vReponseTxt = "" Then cApp.Erreur("cApiDepot.CommandeLire() : Lire a échouer", Self)
  #Pragma BreakOnExceptions Default
  
  Var vDonneesJson, vObjetDonneesJson As JSONItem
  vDonneesJson = New JSONItem(vReponseTxt)
  
  // Si on ne reçoit pas de data, ça va mal
  If Not vDonneesJson.HasKey(Self.kJsonDonnees) Then cApp.Erreur("cApiDepot.CommandeLire() : Not vDonneesJson.HasKey(Self.kJsonDonnees)", Self)
  
  vDonneesJson = vDonneesJson.Child(Self.kJsonDonnees)
  vObjetDonneesJson = vDonneesJson.ChildAt(0)
  
  Self.ExtraireObjetJson(vSource, vObjetDonneesJson, iObjet)
  
  ' @todo faire une erreur si l’objet n’est pas dans le bassin
  ' @todo faire une méthode, mais juste avec un ID.
End Sub


Public Function CommandeLister(iSource As cSource) As cObjet()
  If iSource = Nil Then cApp.Erreur("cApiDepot.CommandeLister() : iSource = Nil", Self)
  
  Var oObjets() As cObjet
  
  // Effectuer la requête HTTP
  Var vReponseTxt As String = Self.RequeteLister(iSource.Classe)
  If vReponseTxt = "" Then Return oObjets
  
  Var vResultatJson As JsonItem = New JsonItem(vReponseTxt)
  Var vDonneesJson As JsonItem
  
  // Si on ne reçoit pas de data, ça va mal
  If Not vResultatJson.HasKey(Self.kJsonDonnees) Then cApp.Erreur("cApiDepot.CommandeLister() : Not vDonneesJson.HasKey(Self.kJsonDonnees)", Self)
  
  vDonneesJson = vResultatJson.Child(Self.kJsonDonnees)
  
  oObjets = Self.ExtraireObjetsJson(iSource, vDonneesJson)
  
  Return oObjets
End Function


Public Function Connecter(iSession As cSession) As Boolean
  Var vObjetValeurs() As cObjetValeur
  
  // Si on n'a pas de iSession, erreur
  If iSession = Nil Then cApp.Erreur("cApiDepot.Connecter() : iSession = Nil", Self)
  
  // Si la iSession n'a pas d'usager, erreur
  If iSession.Usager = Nil Then cApp.Erreur("cApiDepot.Connecter() : iSession.Usager = Nil", Self)
  
  // Si on n’a pas les données pour, quitter avec échec
  'If Not iSession.CreationValider() Then _
  'App.Erreur("cApiDepot.Connecter() : Not iSession.CreationValider()", Self)
  
  If iSession.Usager.Nom = "" Or iSession.Usager.MotDePasse = "" Then _
    App.Erreur("cApiDepot.Connecter() : iSession.Usager.Nom = '' Or iSession.Usager.MotDePasse = ''", Self)
    
    // Si on a une SessionActuelle et que ce n'est pas celle de la SourceSession, avertir
    If Self.SessionActuelle <> Nil And Self.SessionActuelle <> iSession Then _
      App.Deboguage("cApiDepot.Deconnecter() : Self.SessionActuelle <> Nil And Self.SessionActuelle <> iSession", Self)
      
      // Changer l'etat a EnCours
      iSession.Etat = cSession.eEtat.kConnexionEnCours
      
      // Ajouter la iSession a la SourceSession et son usager a la SourceUsager et les selectionner
      Self.SourceSession.ObjetAjouter(iSession, True)
      Self.SourceUsager.ObjetAjouter(iSession.Usager, True)
      
      // Prévenir de la connexion en cours
      Self.SourceSession.Notifier(cNotification.eType.kConnexion, Self, iSession)
      
      Try
        // Tenter de lire la iSession pour la confirmer valide
        Self.JetonActuel = iSession.Jeton
        Self.CommandeLire(Self.SourceSession, iSession)
        
        // Ajuster l'etat de la connexion a Connecte et selectionne elle et son usager dans la Source de l'Api
        iSession.Etat = cSession.eEtat.kConnecte
        
        // Prévenir du succès de la connexion
        Self.SourceSession.Notifier(cNotification.eType.kConnexion, Self, iSession)
        
        Return True
        
      Catch
        Self.JetonActuel = ""
        
        iSession = cSession(Self.SourceSession.ObjetInstancier())
        Self.SourceSession.ObjetAjouter(iSession, True)
        iSession.Usager = cUsager(Self.SourceUsager.Objet)
      End Try
      
      Try
        iSession.Description = Self.ClientDescription()
        
        Var vPropriete() As String = Array("Usager_Nom", "Description")
        vObjetValeurs = iSession.ObjetValeurs(vPropriete)
        vObjetValeurs.Add(New cObjetValeur(iSession, iSession.Usager.Classe.ProprieteParNom("MotDePasse"), iSession.Usager.MotDePasse))
        
        Self.CommandeCreer(Self.SourceSession, iSession, vObjetValeurs)
        
        // On devrait avoir la même session que celle qu’on a envoyé
        If Self.SessionActuelle <> iSession Then App.Deboguage("cApiDepot.Deconnecter() : Self.SessionActuelle <> iSession", Self)
        
        iSession.Etat = cSession.eEtat.kConnecte
        Self.SourceSession.ObjetAjouter(iSession, True)
        Self.JetonActuel = iSession.Jeton
        
        // Prévenir du succès de la connexion
        Self.SourceSession.Notifier(cNotification.eType.kConnexion, Self, iSession)
        
      Catch
        App.Deboguage("cApiDepot.Deconnecter() : Catch Self.CommandeEffacer()", Self)
        App.Message(cSession.kMsgStatutConnexionEchec)
        
        iSession.Etat = cSession.eEtat.kDeconnecte
        
        // Prévenir de l’échec de la connexion
        Self.SourceSession.Notifier(cNotification.eType.kConnexion, Self, iSession)
        
        Return False
      End Try
      
      Return True
End Function


Public Function Deconnecter() As Boolean
  Var vSession As cSession = Self.SessionActuelle
  
  If vSession = Nil Then App.Deboguage("cApiDepot.Deconnecter() : vSession = Nil", Self)
  
  Try
    Self.CommandeEffacer(Self.SourceSession, vSession)
    
    App.Message(cSession.kMsgStatutDeconnecte)
    vSession.Etat = cSession.eEtat.kDeconnecte
    
  Catch
    App.Deboguage("cApiDepot.Deconnecter() : Catch Self.CommandeEffacer()", Self)
    
    Return False
  End Try
  
  // Prévenir de la déconnexion
  Self.SourceSession.Notifier(cNotification.eType.kConnexion, Self, vSession)
  vSession.Etat = cSession.eEtat.kDeconnecte
  Self.SourceSession.Objet = Nil
  
  Return True
End Function

This is my code for CRUD, sorry, the comments are in French, like me. :wink:

Regards, Antoine

Hmm. Your API appears to return discrete types like Boolean. I guess I’m not sure where the empty array issue is happening.

In CommandeLire(), this line acts like a deserializer:
Self.ExtraireObjetJson(vSource, vObjetDonneesJson, iObjet)

There is no clear place where a Variant is returned from the API, because the code basically either creates the objects, or updates existing objects that are in a « pool » of all the objects…

Maybe there’s not a few simple methods I can share. Sorry.

Regards, Antoine

Got it, thanks.

It sounds like you’ve found a solution that works for you, so I won’t push too hard on this; but my sense of it is that there might be a more object oriented way to represent these calls and the data itself. Often when I find myself in a corner with variants and frustrated with code to handle type edge cases, and especially if I find I have to use the Introspection system, I take some time to rethink about how I’ve structured everything and I move towards a way that better fits into Xojo’s type system, and I’m always happy I did so.

Obviously I haven’t seen all of your code and I know very little about your project, so take that with a grain of salt. It’s just a hunch; what we used to call “code smell” or “design smell”, a sense that there might be foundational difficulties in the system. :slight_smile:

1 Like

Well, up to now, it was working well. My only problems are:

  1. IsA doesn’t work well with arrays of object
  2. Variant can’t convert arrays of object.

For the first, I’ll write a IsA_Enhanced() and for the second, I’ll write an ObjectArray class.
But I will keep my object oriented structure.

Xojo is great, but has a few shortcomings. If they were totally documented (like the 2 cases previously stated), I would see them ahead.

Regards, Antoine

1 Like

Please excuse if this is a dumb question, but why even test while the array is empty?
And if it’s not empty, why don’t do a:

Var vSubClassArray() As cSubClass
If vSubClassArray(0) IsA cSubClass Then
  // Will (really) work
End If

Because it also need to work when the array is empty. Your suggestion would be good if my array was always filled with at least one item.

Regards, Antoine

1 Like

Just to add to this thread, MBS seams to have the solution for me!

Regards, Antoine

1 Like

Yes, you can:

Dim xArray() As TCPSocket = Array(New TCPSocket)
Dim v As Variant = xArray
Dim t As Introspection.TypeInfo = Introspection.GetType(v)
System.DebugLog "name=" + t.Name
System.DebugLog "isArray=" + t.IsArray.ToString
System.DebugLog "is subclass: " +t.GetElementType.IsSubclassOf(Introspection.GetType(New TCPSocket)).ToString
1 Like