Durchzug verhindern

Schöneres Debugging unter macOS bei penetrantem GetOpenFolderItem-Dialogfenster

Es ist ein kleines Ärgernis für Mac-Entwickler, das sich irgendwann in einem der letzten Systemupdates so um Sierra herum eingeschlichen hat: Benutzt man einen GetOpenFolderItem-Dialog und setzt dann einen Breakpoint, um den gewählten Dateipfad im Debugger zu betrachten, bleibt das Dialogfenster mit unschöner Penetranz noch geöffnet und liegt mit größter Wahrscheinlichkeit über genau dem Bereich des Bildschirms, den man eigentlich inspizieren wollte.

Im Programmablauf kann das Fenster noch auf geraume Zeit geöffnet bleiben, sofern sich eine längere Aktion mit der erhaltenen Datei anschließt. Verunsichernd für den Anwender.

GetOpenFolderItemHang.png

Der nun wirklich nicht gravierende, aber doch gerne Zeit kostende Fehler ist nicht bei Xojo zu suchen, sondern liegt an Veränderungen in Apples Grafikengines: Das System wird erst beim nächsten Bildschirmupdate Zeit finden, das Fenster zu schließen, und dieses findet erst statt, wenn das aktive Programm Prozessor-Zeit freigibt – was bei einem Break direkt nach GetOpenFolderItem bzw. einer sich anschließenden Methode noch nicht der Fall war.

Hier also ein Mini-Projekt, um schöneres Debuggen und schnelleres GUI-Verhalten zu ermöglichen. Und damit gleich ein kleiner Exkurs in relativ selten benutzte Xojo-Features wie Delegates.

Dem System Zeit für Updates frei zu schaufeln, ist eine häufiger wiederkehrende Angelegenheit, wenn man länger dauernde Prozesse durchläuft. Liegen diese ganz normal in einer Xojo-Methode, krallt sich das Programm den Hauptthread der CPU, bis die Methode beendet ist. Das führt dann zu hässlichen Nebenerscheinungen wie kreiselnden Rädern und der Befürchtung des Anwenders, das Programm könne ihm die Gunst ganz entzogen haben und würde nur noch so tun, als wäre es für ihn da.

In einen Thread ausgelagert, gibt es die Möglichkeit, über Timer-Aktionen etwa auf den Mainthread zu hüpfen und dort einen Fortschrittsbalken neu zu stellen, ggf. mit einem Invalidate zum baldigen Neumalen markiert.

Ein Thread verbietet sich in diesem Fall. Einerseits haben wir gar keine lang andauernde Methode vor uns, sondern nur einen einzigen Befehl:

Dim F As FolderItem = GetOpenFolderItem (Filter As String) As FolderItem

– und dieser manipuliert außerdem die GUI und würde aus einem Thread heraus zur ThreadAccessingGUIException führen.

Das einzige, was wir bräuchten, wäre eine minimale Verzögerung zwischen Anwahl der Datei und Auslieferung dieser an den Programmierer. Ein klassischer Fall für einen Timer bzw. wieder einmal

Xojo.Core.Timer.Calllater (msec As Integer, Del As Delegate)

Klassenarbeit

Es gäbe verschiedene Lösungsansätze für dieses Problem. Ein Modul wäre denkbar, wer in C-Dialekten fit ist, könnte ein Plugin programmieren, oder aber man nimmt eine Klasse. Dafür entscheide ich mich:

  • Ich lege eine neue Klasse „GUISavvyGetOpenFolderItem“ an.
  • und sie bekommt gleich eine Methode „Constructor“ mit den Eingabeparametern Filter As String, Del As GotItemDelegate.

Der Filter entspricht dem üblichen Dateiartenfilter eines FolderItems. Aber was ist GotItemDelegate?

Um noch einmal eine Grundlage in Erinnerung zu rufen:

Beim Programmieren definieren und manipulieren wir immer nur Speicherbereiche (und lesen diese aus). Bei (Xojo-)Datentypen entspricht der Speicherbereich immer dem Wert an sich – der Wert eines Integers ergibt sich aus der Addition der Bitwerte der 4 oder 8 Bytes, an dem er im Speicher angelegt ist –, bei (Xojo-)Objekten werden diese durch Pointer auf den Speicherbereich repräsentiert, an dem dieses Objekt residiert.

Auch eine Methode wird durch den Speicherbereich definiert, an dem ihr ausführbarer Binärcode liegt. Zusätzlich braucht der Compiler zur sinnvollen Arbeit aber auch Informationen über Ein- und Ausgabeparameter dieser Methode, um sinnvoll damit arbeiten zu können.

Das ist das, was einen Delegate ausmacht, den man in Xojo zu einer Klasse hinzufügt:

Ein Delegate verhält sich zu einer Methode wie eine Klasse zur Instanz. Er ist der „äußere Bauplan“ für eine Methode. Was individuell darin stattfindet, wird an anderer Stelle definiert.

Die Klasse soll mit Verzögerung ein FolderItem übergeben:

  • Durch eine der üblichen Methoden (Menüklick auf Insert, Klick aufs Insert-Icon in der Werkzeugleiste oder Rechtsklick auf die neue Klasse) erhält GUISavvyGetOpenFolderItem einen Delegate:
Private Sub GotItemDelegate(f as FolderItem)

GotItemDelegate.png

Damit ist definiert, dass der zweite Parameter des Constructors ein Delegate einer Methode sein muss, die ein FolderItem als EingabeParameter und keine Rückgabeparameter besitzt.

Moment? Ein Delegate?

Begriffsverwirrungsentwirrungsversuch

Ein Delegate ist in Xojo zugleich ein Objekt, das eine bestimmte Methode repräsentiert.

Das ist ein bisschen so, wie zu behaupten, eine Klasse wäre zugleich eine Instanz. Vielleicht wäre es besser gewesen, das, was man im Navigator anlegt, ein DelegateScheme zu nennen oder so, damit das Ergebnis eines (Weak)AddressOf dann eindeutig etwas anderes wäre, nämlich die Repräsentation einer individuellen Methode (die natürlich ihrerseits einem bestimmten Schema folgt, selbst wenn dieses nicht explizit angelegt wurde).

Aber es ist, wie es ist: Beides heißt in Xojo Delegate.

Wie weisen wir einen Delegate einem Delegate zu?

  • Window1 bekommt eine Methode verpasst:
Private Sub Processitem(f as FolderItem)
 break
End Sub

Mehr Code brauchen wir nicht; wir wollen ja sehen, ob der Dialog ordentlich zugemacht wird, bevor wir hier landen. Dies ist also die Methode, die als Delegate dem Constructor der neuen Klasse übergeben werden soll, und sie folgt dem Delegate(Scheme) GotItemDelegate – Ein- und Ausgabeparameter stimmen überein: ein FolderItem als Ein- und kein Rückgabewert.

  • Also bekommt Window1 einen OpenEvent-Handler:
Sub Open() Handles Open
 gui As New GUISavvyGetOpenFolderitem("", WeakAddressOf ProcessItem)
End Sub
  • … und GUISavvyGetOpenFolderItem erhält eine Property, um den Delegate zu speichern:
Private Property Del as GotItemDelegate
  • Der Constructor besteht aus diesem Code:
Public Sub Constructor(Filter as String, del as gotitemdelegate)
 Me.Del = del
 Dim f As FolderItem = GetOpenFolderItem(Filter)
 xojo.core.timer.CallLater 0, WeakAddressOf RaiseGotItem, f
End Sub

Die interne Property Del wird also mit dem übergebenen Methoden-Delegate belegt, GetOpenFolderItem aufgerufen und mit minimaler Verzögerung an eine andere Methode weitergereicht – RaiseGotItem.

  • Und dann braucht die Klasse noch diese Methode – die Verzögerung von 0 sec reicht, um den nötigen Refresh des Bildschirms auszuführen:
Private Sub RaiseGotItem(f as auto)
 del.Invoke(f)
End Sub

Hier wird also die „gespeicherte Methode“ Processitem mit Invoke aufgerufen und ihr das erhaltene Folderitem übergeben.

In Bildern:

Mit WeakAddressOf(Window1.ProcessItem) wird ein Delegate erzeugt, der der Property gui.Del zugewiesen wird.

 

Damit wird gui.Del bis auf weiteres zu Window1.ProcessItem, und Del.Invoke bewirkt den Aufruf der Methode des Fensters.

 

Crash Tests

Sieht gut aus, oder? Zeit für einen Probelauf. Drücken Sie einmal auf „Run“ und genießen Sie die NilObjectException.

Warum will es nicht so, wie es soll?

Die Lösung liegt wieder einmal in der Lebenszeit – dem Scope – begründet. Der Open-Eventhandler erstellt eine temporäre Instanz gui der Klasse GUISavvyGetOpenFolderItem, die am Ende des Event-Handlers aus dem Scope gerät.

Während die Instanz versucht, via Calllater ihre Methode RaiseGotItem aufzurufen, hört sie selbst auf zu existieren. Das geht naturgemäß nicht gut.

Ja, man könnte dem Fenster eine Property dieser Klasse geben, damit sie am Leben bleibt. Aber mal ehrlich: Für so etwas einfaches wie ein Dialogfenster? Bisschen mit Kanonen auf Kleinvögel geschossen, oder? Viel eleganter ist doch die

Selbstreferenzierung

– also beim Programmieren, und auch nur wenn beabsichtigt. Andernorts genießt sie keinen so guten Ruf.

Nochmal Grundlagen-Auffrischung:

Beim ARC, der automatischen Referenz-Zählung und damit dem Speicherverwaltungsmodell von Xojo (nebst vielen anderen), bleibt ein Objekt zu lange am Leben – also im Speicher und valide benutzbar –, wie mindestens ein Verweis auf es existiert.

Im Falle der im Open-Eventhandler angelegten Instanz ist es die Variable gui, die auf die Instanz verweist – und die am Ende des Events aufhört zu existieren, womit ihr Verweis gelöscht wird und die Instanz aus dem Speicher entfernt.

Wir brauchen also etwas, das länger als die Instanz lebt. Zum Beispiel eine Klassen- oder gesharte Property. Diese leben von der Erstellung bis zum Ende des Programms.

Und da ja theoretisch bei vielen Klassen mehrere Instanzen zeitgleich existieren könnten (hier eigentlich weniger, da ja der Anwender normalerweise nur einen Dateidialog zur Zeit öffnen kann), sollte die Property idealerweise etwas sein, das ein oder mehrere Objekte verwalten kann. Das könnte ein Array sein, oder einfach ein Dictionary:

  • GUISavvyGetOpenFolderItem bekommt eine neue gesharte Computed Property:
Private Shared Property InstanceDict as Dictionary
 Get
  Static mInstanceDict As Dictionary
  If mInstanceDict = Nil Then mInstanceDict = New Dictionary
  return mInstanceDict
 End Get
 Set
 End Set
End Property

Der Setter bleibt also leer, und die Initialisierung erfolgt lazy genau dann, wenn die Property das erste Mal benötigt wird. Ich mag faule Initialisierungen viel mehr als lange Initialisierungs-Methoden, die dies zentral erledigen: Letztere verlangsamen den Programmablauf, da oft Werte gesetzt werden, die zu diesem Zeitpunkt noch gar nicht benötigt werden. Und man muss länger suchen, wenn es einmal etwas zu ändern gilt.

mInstanceDict könnte natürlich auch eine private shared Property sein statt eines Statics. Ich mag es, mir den Debugger mit wenig Umständen freier zu halten.

  • Nun noch zwei Anpassungen:
Public Sub Constructor(Filter as String, del as gotitemdelegate)
 Me.Del = del
 InstanceDict.Value(self) = self
 Dim f As FolderItem = GetOpenFolderItem(Filter)
 xojo.core.timer.CallLater 0, WeakAddressOf RaiseGotItem, f
End Sub
Private Sub RaiseGotItem(f as auto)
 del.Invoke(f)
 InstanceDict.Remove(self)
End Sub

Damit wird im Constructor ein Verweis auf die Instanz gesetzt – als Key dient ebenfalls die Instanz. Und damit sie nicht nach Gebrauch im Speicher rumgammelt, wird sie aus dem Dictionary wieder entfernt.

Fazit

Eigentlich relativ viel Aufwand für ein kleines Problem. Aber ideal, um mal genauer in Delegates reinzuschnuppern – die nun wirklich nicht allzu kompliziert sind, oder?

Die fertige Klasse zum Download: GUISavvyGetOpenFolderItem

6 Gedanken zu “Durchzug verhindern

  1. Hallo
    es ist ja viel Programmieraufwand nötig
    ich habe in solchen Fällen die MSGBOX eingesetzt um den Pfad, Dateinamen oder Variable zu sehen
    einen Breakpoint setze ich selten.

    vielen Dank für die Delegate Erklärungen ich versuche mich da langsam einzuarbeiten
    und habe sie noch nicht ganz Begriffen.
    Ich habe seit 20 Jahren Realbasic / Xojo noch nie mit Klassen und Systemaufrufen programmiert
    und bin gut damit zurechtgekommen.

    Ereignis Entscheidung
    Methodenaufruf
    Übergabe mit globalen Variablen

    meine Frage hierzu: warum wird so selten auf die Grund Funkionen von Yojo eingegangen
    Diese reichen, meiner Ansicht nach, für fast alle Fälle vollkommen aus.

    Gruss Harry

    Gefällt mir

    1. Ja, es stimmt schon, man kann Xojo auch für völlig lineare Programmierkonzepte verwenden.
      Und im Grunde sind Objekte nur „Simulationen“ – es wurde Code um Kernfunktionen der Systeme gestrickt, damit diese sich wie Objekte verhalten. Unter der Haube ist alles nach wie vor nur Manipulation von Speicheradressen. Peak and Poke, aber ohne die Adressen selbst berechnen zu müssen.

      OOP selbst ist nur eine Philosophie. Es geht auch ohne.
      Ich selbst hatte meinen Erstkontakt mit Xojo zu RealBasic-Zeiten, aber damals fehlte mir die Muße zur Einarbeitung. Leider.
      Trotzdem habe ich damals mit meinen alten Pascal- und Basic-Kenntnissen ein Tool für meine Gestaltungsarbeit gestrickt, das mir bis vor wenigen Jahren bei einer jährlichen Katalogproduktion half. Völlig linear und OOP missachtend, bis auf die notwendigen GUI-Interaktionen. Dafür hat’s gereicht.

      Mittlerweile sind meine Projekte sehr viel anspruchsvoller und umfangreicher als die kleine CSV-Überarbeitung, um die es damals ging. Und natürlich nach bestem Wissen und Gewissen (darf man das heute noch sagen?) OOP-Prinzipien berücksichtigend.

      Hätte ich versucht, sie mit dem damaligen Programmierstil aufzubauen: Ich bin sicher, es hätte mich ein Vielfaches der Zeit gekostet. Und Änderungen am Code wären enorm komplexer durch die zwangsläufig auftretende Verzahnung von Programmcode und zahlreichen globalen Variablen gegenüber der klaren Abkapselung der OOP-Klassen.

      Mir sind auf Treffen und Kongressen immer wieder Programmierer begegnet – oft auch alte Hasen, die ehrfurchtgebietende Projekte pflegten –, die mir sagten, dass sie OOP gar nicht so ganz begriffen hätten. Der Umstieg ist gedanklich auch problematisch, und es gibt m.E. wenig Literatur, die einem dabei hilft. Da Xojo nun einmal eine objektorientierte Sprache ist, versuche ich, mit meinen spärlichen Beiträgen ein wenig zu helfen, diese dann doch gar nicht einmal so komplizierten Features zu verstehen. Die Arbeit mit Xojo wird damit garantiert schneller und der Code eleganter und viel besser wartbar. Andernfalls wäre es ja so, als besäße man ein unheimlich geländegängiges ATV und würde damit immer nur zum Einkaufen fahren. Na gut, schlechtes Beispiel.

      Die Grundfunktionen sind m.E. gut dokumentiert; nach einer allgemeinen Akklimatisierung sollte man mit vielen davon klarkommen. Wenn aber nicht, bin ich für Anregungen dankbar. Ebenso, wenn ein Artikel noch zu suboptimalen Epiphanien führt.

      Gefällt mir

    1. 2018r1 hatte Probleme mit Breakpoints, die aber – zumindest zum Großteil – mit 2018r1.1 korrigiert wurden. Bei mir funktioniert ein Breakpoint sowohl am Kopf als auch innerhalb einer For … Next-Schleife.

      Gefällt mir

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden /  Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden /  Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden /  Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden /  Ändern )

w

Verbinde mit %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.