BKY-Analyser

Version Anpassungen
1.0 (2025-08-11) Initiale Version
2.0 (2025-08-16) Es können auch Referenzen auf Komponenteneigenschaften ermittelt werden.

In­halts­ver­zeich­nis

Download

Motivation

Verwendung

Aufbau der HTML-Tabelle

Aufbau einer .aia-Datei

Aufbau einer .bky-Datei

Wurzel-Blöcke

Definition einer globalen Variablen

Definition einer Prozedur

Definition einer Funktion

Definition eines Event-Handlers

Referenzen

Globale Variable beschreiben

Globale Variable auslesen

Prozedur aufrufen

Funktion aufrufen

Implementierung

Übersicht

Hauptklassen

Hilfsklassen

Dokumentationskommentare

Klasse AI2Block

Klasse Reference

Klasse BKYAnalyser

Methode LoadXml

Private Methode getRootBlocks

Private Methode analyzeRootBlock

Private Methode getReferenzes

Methode GetTable

Klasse Form1

Analyse der .aia-Datei

Analyse der .bky-Datei

Download

Das ZIP-Archiv AI2-BKY-Analyser zum Download. Das Archiv enthält das Visual-Studio-Projekt und das kompilierte Binary.

Motivation

Wenn man ein größeres Projekt mit dem App Inventor entwickelt, kann man schnell die Übersicht über die Verwendung einzelner Elemente verlieren. Änderungen oder gar ein Refactoring werden dann schwierig. Mit dem hier vorgestellten Werkzeug kann man sich einen Überblick über die Verwendung der Blöcke in einem AI2-Projekt verschaffen.

Da ich eine grafische Benutzeroberfläche erstellen wollte, habe ich das Programm mit Visual Studio (kostenlose Community Edition) in C# geschrieben. Wesentlichen Elemente lassen sich aber leicht auf eine andere Plattform / Sprache portieren.

Verwendung

Screenshot BKY-Analyser

Die vom App Inventor exportierte .aia-Datei kann einfach auf das Fenster gezogen werden oder über die Schaltfläche ausgewählt werden. Danach wird eine Liste der enthalten Screen-Blöcke angezeigt (Liste mit der Überschrift "Screens"). Klickt man einen der Einträge an, wird in der Liste daneben (Überschrift "Blocks") die enthaltenen Wurzel-Blöcke angezeigt. Klickt man einen der Blöcke an, werden in der der rechten Liste (Überschrift "Is referenced by") die Blöcke angezeigt, die den selektierten referenzieren. Zur besseren Übersicht werden Blöcke, die nicht von anderen Blöcken referenziert werden, ein "---" vorangestellt (s. roter Pfeil). Diese Blöcke werden nicht verwendet und können entfernt werden.

Über die CheckBox Include Events kann ausgewählt werden, ob die Ereignishandler mit in die Liste der Blöcke aufgenommen werden sollen. Sie erscheinen dann auch in der HTML-Tabelle.

Die Schaltfläche Generate HTML Table erzeugt den Code für eine HTML-Tabelle der Blöcke und kopiert diesen in die Zwischenablage.

Die CheckBox Include links for references bestimmt, ob die referenzierenden Blöcke (rechte Liste) einen Link auf die referenzierten Blöcke (mittlere Liste) erhalten sollen. Die Namen der Blöcke werden mit in der HTML-Tabelle mit einem <span>-Tag versehen. Die TextBox Class name legt den Namen der NTML-Klasse (CSS) fest, mit der dieser Tag versehen wird.

Aufbau der HTML-Tabelle

Die Tabelle beginnt mit einer Überschriftenzeile:

<table>
  <tr>
    <th>Type</th>
    <th>Name</th>
    <th>Usage</th>
    <th>Referenced by</th>
  </tr>

Für jeden dokumentierten Block wird eine Tabellenzeile erzeugt. Beispiel:

<tr id="refBlockName">
  <td>Global</td>
  <td><span class="symbolNoWrap">BlockName</span></td>
  <td></td>
  <td>Get: Procedure: <a href="#refOtherBlockName"><span class="symbolNoWrap">OtherBlockName"</span></a><br>
      Set: Procedure: <a href="#refFutherBlockName"><span class="symbolNoWrap">FutherBlockName"</span></a>
  </td>
</tr>

Das <tr>-Tag erhält als ID den Blocknamen mit vorangestelltem "ref". Damit kann auf diesen Block per Link verwiesen werden. Die erste Spalte erhält die Bezeichnung des Typs:

Global  Globale Variable Deklaration einer globalen Variablen
Function   Funktion, eine Methode, die einen Wert zurück liefert   Deklaration einer Funktion
Procedure   Prozedur, eine Methode, die keinen Wert zurück liefert Deklaration einer Prozedur
Event Eventhandler Deklaration eines Eventhandlers

In der zweiten Spalte wird der Name des Blocks angegeben. Der Name wird mit einem <span>-Tag versehen. Der Klassenname für dieses Tag kann in der Bildschirmmaske festgelegt werden. Im Beispiel ist dies symbolNoWrap. Auf meinen Seiten ist dafür dieser CSS-Code hinterlegt:

.symbolNoWrap {
  font-family: 'Times New Roman';
  font-weight: bold;
  font-style: italic;
  white-space: nowrap;
  hyphens: none;
}

Die dritte Spalte ist leer. Sie ist für Kommentierungen vorgesehen.

Die vierte Spalte listet die Blöcke, die den dokumentierten Block referenzieren. Zunächst wird die Art der Referenz angegeben:

Es folgt der Typ des referenzierenden Blocks: Function, Procedure oder Event, gefolgt von dessen Namen. Der Name ist wieder mit einem <span>-Tag versehen und, wenn angefordert, mit einem Link zu der Zeile in der er dokumentiert wird.

Mit einigen zusätzlichen Formatierungsanweisungen für die Tabelle sieht das dann so aus:

Beispiel für eine erzeugte Tabelle

Aufbau einer .aia-Datei

App Inventor bietet die Möglichkeit eine Datei mit der Projektdefinition zu exportieren und wieder zu importieren. Die exportierte Datei hat die Endung .aia ist ein ZIP-Archiv. Man kann sie also mit einem entsprechenden Programm öffnen.

Im Verzeichnis src\appinventor\<user>\<projektname> (z.B. src\appinventor\ai_bienonline\ADFC_EIN_Code_SH_HH) findet man den Source-Code für das Projekt. Im Beispiel:

Sourcecode eines AI2-Projekts

Für jeden Screen gibt es eine Datei mit der Endung .scm, eine Datei im JSON-Format, die Daten für das Designer-Fenster enthält, und eine mit der Endung .bky mit XML-Daten für das Blocks-Fenster. Die .scm-Datei beschreibt also die Oberfläche der App, die .bky-Datei den Code.

Aufbau einer .bky-Datei

Wurzel-Blöcke

Ein App-Inventor-Projekt ist aus vier verschiedenen Wurzel-Blöcken aufgebaut. Die .bky-Datei ist im Wesentlichen eine Liste von XML-Knoten (XmlNode) mit einem Eintrag für jeden dieser Blöcke mit dem Namen <block>. Jeder dieser XML-Knoten hat ein Attribut mit dem Namen type, dass den Typ des Blocks angibt.

<block type="global_declaration" id=")J*K{.OM)m3_hM]z4!xB" x="-970" y="-2190">

Definition einer globalen Variablen

Die XML-Nodes für die Definition von Prozeduren haben das type-Attribut global_declaration.

<block type="global_declaration" id=")J*K{.OM)m3_hM]z4!xB" x="-970" y="-2190">
   <field name="NAME">PrintCount</field>
...

Jeder dieser Block-Definitionen hat ein Child-Node <field> mit dem Attribut name und dem Wert NAME, das den Namen der globalen Variablen enthält (hier PrintCount).

Definition einer Prozedur

Die XML-Nodes für die Definition von Prozeduren haben das type-Attribut procedures_defnoreturn.

<block type="procedures_defnoreturn" id="JJ/t,[I9T:0r}S]F_%Nv" collapsed="true" x="710" y="-2010">
   <mutation xmlns="http://www.w3.org/1999/xhtml">
      <arg name="State"/>
   </mutation>
   <field name="NAME">SetDialogElements</field>
...

Auch jeder dieser Block-Definitionen hat ein Child-Node <field> mit dem Attribut name und dem Wert NAME, das den Namen der Prozedur enthält (hier SetDialogElements).

Definition einer Funktion

Die XML-Nodes für die Definition von Funktionen haben das type-Attribut procedures_defreturn.

<block type="procedures_defreturn" id="iW)k^-]sRQp.7!(gu)@d" collapsed="true" x="-270" y="-1330">
   <field name="NAME">getCode</field>
...

Auch jeder dieser Block-Definitionen hat ein Child-Node <field> mit dem Attribut name und dem Wert NAME, das den Namen der Funktion enthält (hier getCode).

Definition eines Event-Handlers

Die XML-Nodes für die Definition von Event-Handlern haben das type-Attribut component_event.

<block type="component_event" id="A:`O78$dOIF1![_A{rdj" collapsed="true" x="-630" y="-1690">
   <mutation xmlns="http://www.w3.org/1999/xhtml" component_type="Form" is_generic="false"
             instance_name="Screen1" event_name="OtherScreenClosed"/>
   <field name="COMPONENT_SELECTOR">Screen1</field>

...

Jeder dieser Block-Definitionen hat ein Child-Node <field> mit dem Attribut name und dem Wert NAME, das den Namen der Componente enthält, zu der dieser Event-Handler gehört (hier Screen1). Der Name des Ereignisses in dem Attribut event_name der Child-Node <mutation> codiert (hier OtherScreenClosed).

Referenzen

Referenzen werden ebenfalls durch ein XML-Node mit den Tag <block> dargestellt. Das type-Attribut gibt die Art der Referenz an.

<block type="lexical_variable_set" id="rr6#O08t7p-lB9`@2jOc">

Globale Variable beschreiben

Die XML-Nodes für Blöcke, die globale Variablen beschreiben haben das type-Attribut lexical_variable_set. Sie sind einem Wurzel-Block untergeordnet.

...
   <block type="lexical_variable_set" id="rr6#O08t7p-lB9`@2jOc">
      <field name="VAR">global PrintCount</field>
...

Jeder dieser Block-Definitionen hat ein Child-Node <field> mit dem Attribut name und dem Wert VAR, das den Namen der globalen Variablen enthält (hier global PrintCount).

Globale Variable auslesen

Die XML-Nodes für Blöcke, die globale Variablen beschreiben haben das type-Attribut lexical_variable_get. Sie sind einem Wurzel-Block untergeordnet.

...
   <block type="lexical_variable_get" id="2dGj+$lE/@6s)^!dc.L!">
      <field name="VAR">global Today</field>
...

Jeder dieser Block-Definitionen hat ein Child-Node <field> mit dem Attribut name und dem Wert VAR, das den Namen der globalen Variablen enthält (hier global Today).

Prozedur aufrufen

Die XML-Nodes für Blöcke, die globale Variablen beschreiben haben das type-Attribut procedures_callnoreturn. Sie sind einem Wurzel-Block untergeordnet.

...
   <block type="procedures_callnoreturn" id="*9F:y6cE4~l9wWr?)(v`" inline="true">
      <mutation xmlns="http://www.w3.org/1999/xhtml" name="SetDialogElements">
         <arg name="State"/>
      </mutation>
   <field name="PROCNAME">SetDialogElements</field>
...

Jeder dieser Block-Definitionen hat ein Child-Node <field> mit dem Attribut name und dem Wert PROCNAME, das den Namen der globalen Variablen enthält (hier SetDialogElements).

Funktion aufrufen

Die XML-Nodes für Blöcke, die globale Variablen beschreiben haben das type-Attribut procedures_callreturn. Sie sind einem Wurzel-Block untergeordnet.

...
   <block type="procedures_callreturn" id="]XYGJ5b|gQ:}):z%3(f~" inline="false">
      <mutation xmlns="http://www.w3.org/1999/xhtml" name="CodeIsNotComplete"/>
      <field name="PROCNAME">CodeIsNotComplete</field>
...

Jeder dieser Block-Definitionen hat ein Child-Node <field> mit dem Attribut name und dem Wert PROCNAME, das den Namen der globalen Variablen enthält (hier CodeIsNotComplete).

Implementierung

Übersicht

Hauptklassen

Hauptklassen des Analyse-Programms

Die Klasse BKY-Analyser stellt die Methoden zur Analyse einer .aia-Datei bereit. In der Klasse AI2Block werden die notwendigen Informationen zu den gefundenen Wurzel-Blöcken abgelegt. In der Klasse Reference werden die Daten zu den referenzierenden Blöcken gespeichert. Hinzu kommen Enum-Klassen zur Typ-Sicherheit und Listen-Klassen.

Hilfsklassen

Hilfklassen

BKYException dient zum Erkennen von projektspezifischen Ausnahmen. Die Klasse EmptyNodeList erleichtert die null-Abfrage, wenn XML-Node-Listen abgerufen werden.

Dokumentationskommentare

Der Code ist mit umfangreichen XML-Dokumentationskommentaren versehen. Daraus wurde eine Help-Datei erstellt (Link zur Help-Datei).

Klasse AI2Block

Die Klasse AI2Block speichert die Daten eines Wurzelblocks. Dies sind

Die Methode ToString ist überschrieben als Kombination der Felder Type und Name, z.B. "Global: PrintCount". Objekte dieser Klasse können demnach direkt an eine ListBox übergeben und werden sinnvoll angezeigt.

Die Gleichheitsoperatoren == und != sind überschrieben. Dadurch können Listen von Objekten dieser Klasse einfach durchsucht werden. Verglichen werden die Felder Type und Name.

Implementiert ist die Schnittstelle IComparable<T>. Sie erlaubt das einfache sortieren einer Liste von Objekten dieser Klasse. Verglichen werden die von ToString zurück gegebenen Daten.

Klasse Reference

Die Klasse Reference speichert Daten zu Blöcken, die andere Blöcke referenzieren. Diese sind:

Eine Liste mit Objekten dieser Klasse wir in der Klasse AI2Block benutzt.

Die Methode ToString ist überschrieben als Kombination der Felder Type und AI2Block.ToString, z.B. "Get: Procedure: ComputeCode". Objekte dieser Klasse können demnach direkt an eine ListBox übergeben und werden sinnvoll angezeigt.

Die Gleichheitsoperatoren == und != sind überschrieben. Dadurch können Listen von Objekten dieser Klasse einfach durchsucht werden. Verglichen werden die Felder Type und ReferencedFrom.

Implementiert ist die Schnittstelle IComparable<T>. Sie erlaubt das einfache sortieren einer Liste von Objekten dieser Klasse. Verglichen werden die von ToString zurück gegebenen Daten.

Klasse BKYAnalyser

Die Klasse BKYAnalyser stellt Methoden zur Analyse von .bky-Dateien bereit. Die Methode LoadXml analysiert die XML-Struktur der Datei und erstellt eine Liste von Wurzel-Blöcken (Feld RootBlocks). Diese Liste kann zur Anzeige der Inhalte in einer ListBox verwendet werden. LoadXml liefert ein Objekt dieser Klasse zurück, über das auf die Blockliste zugegriffen werden kann. Die Methode GetTable liefert eine HTML-codierte Tabelle der Blöcke und Referenzen.

Methode LoadXml

Das Analysieren der XML-Datei erfolgt in mehreren Phasen. Die Analyse erfolgt mit Hilfe der XmlDocument-Klasse. Um Elemente in dem XML-Dokument zu selektieren werden Xpath-Abfragen verwendet.

Das in der .bky-Datei enthalte XML-Dokument verwendet Namensräume. Dies muss bei den Xpath-Abfragen berücksichtigt werden. Am einfachsten geht dies, wenn man einen XmlNamespaceManager verwendet. Diesem werden die Bezeichnungen der Namensräume (URI) und ein Kürzel für die spätere Verwendung übergegeben.

// Das .bky-XML-Dokument besitzt einen Namespace (xmlns="https://developers.google.com/blockly/xml")
// Zur Selektion muss deshalb mit einem Namespace-Manager gearbeitet werden.
XmlNamespaceManager nsman = new(bky.NameTable);
nsman.AddNamespace("a", docElement.NamespaceURI);
nsman.AddNamespace("b", "http://www.w3.org/1999/xhtml");

Mit dem XmlNamespaceManager und dem Stamm-XmlElement (XmlDocument.DocumentElement Eigenschaft) wird eine neue Instanz der BKYAnalyser-Klasse erzeugt. Anschließen werden die Wurzel-Blöcke aus dem XML-Dokument und anschließend die Referenzen extrahiert. Die BKYAnalyser-Instanz wird zur weiteren Verwendung zurück geliefert.

var bkyAnalyser = new BKYAnalyser(nsman, docElement);

bkyAnalyser.getRootBlocks();
bkyAnalyser.getReferenzes();

return bkyAnalyser;

Private Methode getRootBlocks

getRootBlocks benutzt einen XPath-Ausdruck um alle XML-Wurzelknoten mit dem Namen block zu finden. Diese werden dann in analyzeRootBlock weiter untersucht.

/// <summary>Blöcke auf der obersten Ebene selektieren und analysieren.</summary>
/// <exception cref="BKYException">Das XML-Dokument ist kein wohlformatiertes .bky-Dokument.</exception>
private void getRootBlocks() {
   var blockNodes = docElement.SelectNodes("a:block", nsman);
   if (blockNodes == null) {
      throw new BKYException("invalid structure: Select Blocks");
   }

   foreach (XmlNode blockNode in blockNodes) {
      RootBlocks.Add(analyzeRootBlock(blockNode));
   }

   RootBlocks.Sort();
}

Private Methode analyzeRootBlock

analyzeRootBlock ermittelt die Typ-Bezeichnung des Blocks ...

// Root-Blocks haben ein Attribut 'type', das angibt, um welchen Typ Block es sich handelt.
string ai2BlockTypeName = ""; // AI2-Bezeichnung des BlockTyps

XmlNode? typeAttr = blockNode.SelectSingleNode("./@type", nsman); 
if (typeAttr != null) {
   ai2BlockTypeName = typeAttr.InnerText;
}

... den Namen des AI2-Blocks bzw. den Namen der Komponente bei Eventhandlern ...

// Die Root-Block-Nodes haben eine Child-Node mit dem Namen (Tag) 'field' mit dem Attribut 'NAME'
// Dieses Attribut enthält den Namen der Variable, Funktion, Prozedur oder Ereignis.
XmlNode? nodeField = blockNode.SelectSingleNode("child::a:field", nsman);

string blockName = "unknown";
if (nodeField != null) {
   blockName = nodeField.InnerText;
}

AI2Block ai2Block = new(blockName, ai2BlockTypeName, blockNode);

... und im Falle eines Eventhandlers den Namen des Ereignisses:

// Ereignisname ermitteln
if (ai2Block.Type == BlockTypes.Event) {
   string eventName = "unknown";

   XmlNode? nodeMutation = blockNode.SelectSingleNode("child::b:mutation/@event_name", nsman);
   if (nodeMutation != null) {
      eventName = nodeMutation.InnerText;
   }
   ai2Block.Name += "." + eventName;

Private Methode getReferenzes

Das suchen der Referenzen erfolgt in vier Schritten. In den gespeicherten XML-Nodes der Wurzelblöcke wird nacheinander nach Knoten mit dem Namen block und den type-Attributen lexical_variable_set, lexical_variable_get, procedures_callnoreturn und procedures_callreturn gesucht. Hier als Beispiel die Suche nach lexical_variable_set. Alle Wurzelblöcke werden durchsucht. Solche, die eine globale Variable repräsentieren, können ignoriert werden. Diese können keine anderen Blöcke referenzieren:

// Prüfen, welche Blöcke der obersten Ebene von welchen Blöcken referenziert werden
// Welcher Root-Block setzt welche globaler Variable
// --------------------------------------------------
foreach (AI2Block rootBlock in RootBlocks) {
   if (rootBlock.Type == BlockTypes.Global) // Globale Variablen können keine anderen beschreiben
      continue;

Die in der AI2Block-Instanz gespeicherte XML-Node wird per Xpath durchsucht:

// Liste der Nodes in diesem Root-Block, in denen eine globale Variable belegt wird.
   var setBlocks = rootBlock.XmlNode.SelectNodes("descendant::a:block[attribute::type='lexical_variable_set']", nsman);

Bei allen gefunden Nodes wird der Name der referenzierten Variablen ermittelt (Innertext der Child-Node field mit dem Attribut name='Var'). Dazu muss geprüft werden, ob es sich um eine globale oder eine lokale Variable handelt.

   // Diese Liste abarbeiten
   foreach (XmlNode setBlock in setBlocks ?? new EmptyNodeList()) {

      // Der Name ist im Child 'field' mit Attribut name = 'VAR'
      var referencedVarNode = setBlock.SelectSingleNode("child::a:field[attribute::name='VAR']", nsman);

      if (referencedVarNode == null) // Kein solches Feld: Problem!
         throw new BKYException();

      String variableName = referencedVarNode.InnerText; // Der Name der Variablen
      if (!variableName.StartsWith("global")) // Andernfalls lokale Variable
         continue;

Zum Schluss muss der referenzierte Block herausgesucht werden, und die Referenz notiert werden:

 // Den Root-Block für die globale Variable mit diesem Namen heraussuchen
      var globalVariableBlock = RootBlocks.GetBlock(variableName[7..], BlockTypes.Global);

      // Dieser Root-Block referenziert diese Variable
      globalVariableBlock?.AddReference(new Reference(ReferenceTypes.Set, rootBlock));
   }
}

Methode GetTable

Diese Methode liefert den HTML-Code für eine Tabelle mit den Wurzelblöcken und den zugehörigen Referenzen. Hier sei auf den Abschnitt Aufbau der HTML-Tabelle und die Code-Dokumentation verweiesen.

Klasse Form1

Analyse der .aia-Datei

Die Entschlüsselung der .aia-Datei ist mit der ZipFile-Klasse recht einfach. Wenn man den Dateinamen über den OpenFileDialog oder per Drag-and-Drop erhält, kann man diese öffnen und alle enthaltenen Dateinamen auslesen:

aia = ZipFile.OpenRead(openFileDialog.FileName);

foreach (ZipArchiveEntry entry in aia.Entries) {
   if (entry.Name.EndsWith(".bky"))
      lbScreens.Items.Add(entry.Name);
}

Analyse der .bky-Datei

Hat der Anwender eine .bky-Datei aus der Liste ausgewählt, wird diese in dem ZIP-Archiv gesucht, geöffnet und in eine String-Variable eingelesen. Dieser String wird anschließend durch die Methode BKYAnalyser.LoadXml analysiert.

string screenName = (string)lbScreens.SelectedItem;

foreach (ZipArchiveEntry entry in aia.Entries) {
   if (entry.Name == screenName) {
      var stream = entry.Open();
      StreamReader reader = new StreamReader(stream, System.Text.Encoding.UTF8);
      var text = reader.ReadToEnd();
      bkyAnalyser = BKYAnalyser.LoadXml(text);