Erweiterte Programmierung


Geschäftsobjekte (Businessobjekte) sind Klassen, die während der Laufzeit bei Bedarf instanziiert (dynamisch) werden. In der Regel sind Sie Kindinstanzen von "Root\Businessobjects". Sie können aber auch Kindinstanzen jeder beliebigen ACComponent sein und auch im Anwendungsbaum als statische Instanz definiert werden.

Bei einer dynamischen Instanziierung wird ihr ACIdentifier mit einer fortlaufenden Instanznummer erweitert. Z.B. wenn es ein Businessobjekt der Klasse "BSOMaterial" gibt, dann erhält die erste Instanz die ID 1 und somit den ACIdentifier "BSOMaterial(1)". Öffnet der Benutzer eine zweite Karteikarte, dann lautet der ACIdentifier "BSOMaterial(2)" usw.

Aufgrund der dynamischen Instanzierfähigkeit dürfen Geschäftsobjekte keine persistierbare Eigenschaften besitzen. Die ACClassInfo-Attributklasse muss daher mit "Global.ACStorableTypes.NotStorable" deklariert werden.

Businessobjekte entsprechen dem Modell im MVVM-Entwurfsmuster und sind über die BSOACComponent-Eigenschaft in  VBDesign's im View-Model zugreifbar. In der Regel verweist die "FrameworkElement.DataContext"-Eigenschaft auch auf ein Businessobjekt.

Der primäre Einsatzzweck von Geschäftsobjekten ist der Umgang mit Daten aus Anwendungstabellen. Das bedeutet konkret das Anlegen, Löschen und Modifizieren von Datensätzen.

Grundsätzlich unterscheidet man 

Geschäftsobjekte:

 

Navigierbar bedeutet, dass ein Geschäftsobjekt eine ACAccessNav<T>-Eigenschaft besitzt mit der im Menüband navigiert, gesucht, gespeichert und gelöscht werden kann:

 

Navigationslos bedeutet, dass das Geschäftsobjekt nicht mit Datensätzen arbeitet. Im Menüband entfallen dann die zuvor umrahmten Icons:

 

Geschäftsobjekte starten Sie entweder über das Menü oder Favoriten-Leiste.


Ein Geschäftsobjekt benötigt einen Datenbankkontext, mit dem es arbeiten kann. Dafür stellt die Basisklasse ACBSO folgende virtuelle Eigenschaft bereit:

public virtual IACEntityObjectContext Database { get; }

Diese Eigenschaft kann in einem abgeleiteten Geschäftsobjekt überschrieben werden und es kann eine eigene Instanz eines Datenbankkontextes zurückgegeben werden. Datenbankkontexte in Geschäftsobjekten sind somit sogenannte "Langzeit-Kontexte". Für den Lebenszyklus des Datenbankkontextes ist das implementierende Geschäftsobjekt zuständig. Das bedeutet, dass ein Datenbankkontext in der überschriebenen ACDeInit()-Methode wieder geschlossen werden sollte.

Getrennter Kontext

Für jede Geschäftsobjektinstanz einen eigenen Datenbankkontext zu instanziieren hat jedoch auch Nachteile:

  1. Hoher Speicherverbrauch.
  2. Unterschiedliche Entity-Objekt-Instanzen für den gleichen Datensatz. Wenn Sie z.B. ein Geschäftsobjekt zum Verwalten von Materialstammdaten und ein anderes zum Verwalten eine Stückliste geöffnet haben, dann wird dasselbe Material das sie in dem einen Geschäftsobjekt ändern, nicht automatisch in dem anderen an der Oberfläche aktualisiert, weil es sich um unterschiedliche Objekte aus zwei verschiedenen Kontexten handelt.

Gemeinsamer Kontext

Die Verwendung eines gemeinsamen Kontextes über viele Geschäftsobjekte hinweg behebt diese Nachteile. Auf der anderen Seite hat das Teilen von Datenbankkontexten einen anderen Seiteneffekt: Der Aufruf von SaveChanges in einem Geschäftsobjekt zur Folge, dass alle Änderungen, die in den anderen Geschäftsobjekten durchgeführt worden sind, auch mit abgespeichert werden.

Getrennt oder Gemeinsam?

Diese Entscheidung ist Ihnen überlassen und kann generell nicht beantwortet werden. Es hängt im Wesentlichen von den Funktionen des Geschäftsobjektes ab. Sie können beide Varianten umsetzen und sollten dem Endanwender darüber informieren welches Verhalten Ihr Geschäftsobjekt hat. Der Umgang mit gemeinsamen Kontexten erfordert jedenfalls eine intelligente Kontextverwaltung, die beim Schließen des letzten Geschäftsobjektes den gemeinsamen Kontext ebenfalls schließt. Umgekehrt soll beim Öffnen von weiteren Geschäftsobjekten derselbe Kontext wiederverwendet werden. Darum müssen Sie sich zum Glück nicht kümmern, weil Geschäftsobjekte dies implizit bereits eingebaut haben.


Im Grunde genommen haben Sie die Kontext-Verwaltung bereits im Datenbankkapitel kennen gelernt.  Dort wurde auch die Nutzung von Langzeitkontexten per ACObjectContextManager im Programmcode erklärt. Den ACObjectContextManager sollten Sie auch für Geschäftsobjekte verwenden wie es im Beispielprojekt in der Klasse BSOMyCompNav implementiert ist:

 

Die Beispielklasse BSOMyCompNav ist eine Basisklasse für alle anderen Geschäftsobjekte, welche denselben Datenbankkontext verwenden sollen. Sie können auf dieser Art eine eigene Basisklasse bauen, die z.B. nur von ACBSO erbt, wenn Sie navigationslose Geschäftsobjekte programmieren möchten.

 

Automatische Instanziierung

Die Verwendung des gemeinsamen Kontextes ist in der Methode GetAppContextForBSO(ACComponent bso) implementiert, indem es die generische GetOrCreateContext-Methode auf dem ACObjectContextManager aufruft und den Schlüssel "BSOMyCompContext" übergibt. Falls schon eine Instanz unter diesem Schlüssel existiert wird diese zurückgegeben. Somit erhält jedes neue Geschäftsobjekt denselben Kontext, wenn auf die virtuelle Database-Eigenschaft zugegriffen wird. DatabaseApp dient nur für die abgeleiteten Klassen, damit Sie nicht ständig auf die konkrete Entity-Object-Datenbankklasse casten müssen, wenn LINQ-Abfragen programmiert werden.

 

Automatisches Schließen

Sie werden sich fragen, wo ist eigentlich die Logik programmiert, dass ein Datenbankkontext geschlossen wird, wenn alle Geschäftsobjekte beendet worden sind? 

Dies ist bereits in ACBSO implementiert. Jedes Mal, wenn ein Geschäftsobjekt geschlossen wird, wird in der ACDeInit-Methode geprüft, ob es noch weitere Geschäftsobjekte gibt, die denselben Kontext verwenden. Falls nicht, dann wird bei dem letzten Geschäftsobjekt die virtuelle Methode OnDisposeDatabase() aufgerufen die den ACObjectContextManager beauftragt den Datenbankkontext zu schliessen. Falls Sie diese Methode überschreiben, vergessen Sie auf keinem Fall den base-Aufruf zu machen. Andernfalls bleibt der Kontext geöffnet und alle Objekte verbleiben im Change-Tracker.

 

Frische Daten

Nachdem das letzte Geschäftsobjekt geschlossen wurde, ist der alte Kontext verschwunden und der Speicher wieder freigegeben. Wenn nun erneut ein neues Businessobjekt geöffnet wird, dann wird ein neuer Kontext generiert und alle Daten werden neu von der Datenbank geladen! Somit umgehen Sie Aktualisierungsprobleme, wenn Ihr Geschäftsobjekt nicht so programmiert wurde dass beim Laden alle in Beziehung stehenden Daten mit RefreshMode.StoreWins nachgeladen wurden..


Methodenaufrufreihenfolge beim Speichern

In ACBSO sind folgende virtuelle Methoden definiert, die im Zusammenhang mit dem Speichervorgang stehen: 

protected virtual bool OnSave()
protected virtual Msg OnPreSave()
protected virtual void OnPostSave()

Diese können Sie überschreiben, um auf den Speichervorgang in Ihrem BSO Einfluss zu nehmen. Vergessen Sie beim Überschreiben nicht den base-Auftruf!

Mit der OnSave()-Methode wird der Speichervorgang eingeleitet. Die OnSave()-Methode führt dann eine Serie von Methodenaufrufen durch:

  1. Aufruf von protected MsgWithDetails NotifyAllPreSave():
    "NotifyAllPreSave()" iteriert über alle Geschäftsobjekte, die denselben Datenkontext besitzen und ruft dann die OnPreSave()-Methode auf. Falls diese Methode in einer Ableitung überschrieben wurde und eine Msg-Instanz anstatt eines Null-Wertes zurückgegeben wird, dann wird der Speichervorgang abgebrochen und ein Dialogfenster öffnet sich mit den Fehlermeldungen. Andernfalls geht es mit Schritt 2 weiter:
  2. Aufruf von public bool ACComponent.ACSaveChanges() :
    Diese Methode ist in ACComponent definiert. Sie ruft auf der virtuellen "IACEntityObjectContext Database"-Eigenschaft die ACSaveChanges-Methode auf. Falls der Speichervorgang nicht erfolgreich war, wird ein Dialogfenster geöffnet und die Fehlermeldungen angezeigt. Andernfalls geht es mit Schritt 3 weiter:
  3. Aufruf von protected vois NotifyAllPostSave():
    "NotifyAllPostSave()" iteriert über alle Geschäftsobjekte, die denselben Datenkontext besitzen und ruft dann die OnPostSave()-Methode auf, um alle Geschäftsobjekte über den erfolgreichen Speichervorgang zu informieren.

 

Methodenaufrufreihenfolge beim Rückgängig machen

In ACBSO sind folgende virtuelle Methoden definiert, die im Zusammenhang mit der Rückgängigmachung stehen: 

protected virtual bool OnPreUndoSave()
protected virtual Msg OnPreUndoSave()
protected virtual void OnPostUndoSave()

Diese können Sie überschreiben, um auf die Rückgängigmachung in Ihrem BSO Einfluss zu nehmen. Vergessen Sie beim Überschreiben nicht den base-Auftruf!

Mit der OnUndoSave()-Methode wird die Rückgängig-Aktion eingeleitet. Die OnUndoSave()-Methode führt dann eine Serie von Methodenaufrufen durch:

  1. Aufruf von protected MsgWithDetails NotifyAllPreUndoSave():
    "NotifyAllPreUndoSave()" iteriert über alle Geschäftsobjekte, die denselben Datenkontext besitzen und ruft dann die OnPreUndoSave()-Methode auf. Falls diese Methode in einer Ableitung überschrieben wurde und eine Msg-Instanz anstatt eines Null-Wertes zurückgegeben wird, dann wird der Rückgängigvorgang abgebrochen und ein Dialogfenster öffnet sich mit den Fehlermeldungen. Andernfalls geht es mit Schritt 2 weiter:
  2. Aufruf von public bool ACComponent.ACUndoChanges() :
    Diese Methode ist in ACComponent definiert. Sie ruft auf der virtuellen "IACEntityObjectContext Database"-Eigenschaft die ACUndoChanges-Methode auf. Falls der Rückgängigvorgang nicht erfolgreich war, wird ein Dialogfenster geöffnet und die Fehlermeldungen angezeigt. Andernfalls geht es mit Schritt 3 weiter:
  3. Aufruf von protected vois NotifyAllPostUndoSave():
    "NotifyAllPostUndoSave()" iteriert über alle Geschäftsobjekte, die denselben Datenkontext besitzen und ruft dann die OnPostUndoSave()-Methode auf, um alle Geschäftsobjekte über die erfolgreiche Rückgängigmachung zu informieren.

 

Vorabprüfung 

Im Kapitel für synchrone Methoden haben Sie das Prinzip der IsEnabled-Methoden kennengelernt. IsEnabled-Methoden dienen zur Deaktiverung von UI-Buttons per CommandBinding. Mit dieser Technik kann auch die Speicher- und Rückgängigtaste im Menüband deaktiviert werden. Diese Prüfungslogik sollte durch Überschreiben der virtuellen Methoden

protected virtual bool OnIsEnabledSave()
protected virtual bool OnIsEnabledUndoSave()

implementiert werden. Vergessen Sie beim Überschreiben nicht den base-Auftruf!

Mit dem Aufruf der Methode protected bool NotifyAllIsEnabledSave() und protected bool NotifyAllIsEnabledUndoSave() können Sie alle Geschäftsobjekte, die mit demselben Datenbankkontext arbeiten, auf einmal abfragen. Bei der Iteration wird die Methode OnIsEnabledSave() bzw. OnIsEnabledUndoSave() auf jedem Geschäftsobjekt aufgerufen. Ist der Rückgabewert falsch, dann erlaubt ein Geschäftsobjekt die Speicherung bzw. Rückgängigmachung nicht.

 

Benutzerabfrage forcieren

Falls ein Datenbankkontext geändert wurde und Sie möchten, dass der Benutzer aufgefordert wird zu bestätigen ob der die Änderungen übernehmen oder rückgängig machen will, rufen Sie die Methode public virtual bool ACSaveOrUndoChanges() auf. Der Rückgabewert der Methode ist wahr, wenn der Benutzer die Speicherung oder das Rückgängigmachen bestätigt hat.


Nun stellt sich die Frage wie wird eigentlich der Speichervorgang von der Oberfläche eingeleitet?

Sie haben im Design-Kapitel gelernt, wenn man eine Methode auf die Designoberfläche zieht, dann wird automatisch eine Taste (VBButton) generiert und der VBContent mit dem Methodennamen gesetzt. Dies funktioniert allerdings nur, wenn die Methode per ACMethodInfo dem iPlus-Typsystem bekannt gemacht worden ist. Die OnSave-Methoden sind jedenfalls - aus gutem Grund - nicht damit versehen.

Sie müssen diese Speichermethode daher explizit in ihrem Geschäftsobjekt deklarieren (oder evtl. in einer ihrer eigenen Basisklassen die Ableitungen von ACBSO sind). Innerhalb dieser Methode rufen Sie dann die OnSave()-Methode auf.

Da die meisten Geschäftsobjekte immer dieselben Grundfunktionen haben, werden standardmäßig alle Geschäftsobjekte mit dem Steuerelement "gip.core.layoutengine.VBRibbon" angezeigt. Das Menüband (VBRibbon) selbst hat einige Buttons, die bereits mit einem vordefinierten VBContent versehen sind, damit sie automatisch an die Methoden des Geschäftsobjektes gebunden werden. Ist eine Methode nicht in ihrem Geschäftsobjekt definiert, dann wird auch die entsprechende Taste im Menüband ausgeblendet.

 

Speicherefehle im Menüband
IconMethode

Diese Taste dient zum Speichern. Der VBContent ist "!Save". Folglich muss eine Methode mit dem Namen "Save()" im Geschäftsobjekt deklariert werden:

[ACMethodCommand(InOrder.ClassName, "en{'Save'}de{'Speichern'}", 
(short)MISort.Save, false, Global.ACKinds.MSMethodPrePost)]

public void Save()
{
OnSave();
}
public bool IsEnabledSave()
{
return OnIsEnabledSave();
}

Hinweis: Der Code ist aus dem Beispielprojekt entnommen. Für Ihr eigenes Businessobjekt müssen Sie die ACMethodCommand-Deklaration und die innere Logik entsprechend Ihrem Anwendungsfall anpassen. Das gilt auch für die folgenden Codebeispiele in den Unterkapiteln.

Vergessen Sie nicht eine IsEnabled-Methode zu definieren, damit die Taste im Menüband deaktiviert wird. Im obigen Beispiel wird durch den Aufruf von OnIsEnabledSave() geprüft, ob der Datenbankkontext geändert wurde und eine Speicherung notwendig ist.

Diese Taste dient zum Verwerfen von Änderungen. Der VBContent ist "!UndoSave".

[ACMethodCommand(InOrder.ClassName, "en{'Undo'}de{'Nicht speichern'}", 
(short)MISort.UndoSave, false, Global.ACKinds.MSMethodPrePost)]

public void UndoSave()
{
OnUndoSave();
}
public bool IsEnabledUndoSave()
{
return OnIsEnabledUndoSave();
}