Seiten

Donnerstag, 7. Oktober 2010

StoryboardEventHelper Klasse für eigene Storyboard-Ereignisse (VB.NET)

In diesem Artikel stelle ich eine kleine Helferklasse vor, mit deren Hilfe man für ein Storyboard (Silverlight 4) zusätzliche Ereignisse bereitstellen kann. Die Storyboard Klasse kennt regulär nur ein einziges Ereignis, das ist das Completed-Ereignis. Dieses Ereignis feuert, wenn das Storyboard-Objekt die Wiedergabe beendet hat. Es gibt aber eine Vielzahl von denkbaren Szenarien, in denen zusätzliche Ereignisse sehr nützlich wären.

Dieser Artikel beschreibt eine Technik, wie man zwar nicht der Klasse Storyboard selbst, aber über einen kleinen Trick mittels einer helfenden Klasse zusätzliche Ereignisse für ein bestimmtes Storyboard bereitstellen kann. Diese Klasse wird zwei zusätzliche Ereignisse eines Storyboard bereitstellen, auf die anwendungsweit reagiert werden kann. Das sind beispielhaft die folgenden beiden Ereignisse:

1. OnStoryboardStarted-Ereignis
2. OnStoryboardPositionChanged-Ereignis.

Den Nutzen der fertigen Klasse demonstriere ich dann anhand eines kleinen Beispiels, bei dem der VisualState eines beliebigen UI-Elements an einer bestimmten Timeline-Position eines laufenden Storyboards geändert wird.

Grundsätzliche Funktionsweise

Die Grundidee besteht darin, drei private Delegaten vom benutzerdefinierten Typ StoryboardDelegate zu deklarieren und in einem öffentlich deklarierten Delegate des gleichen Typs zu kombinieren, so dass sie alle drei gemeinsam und zugleich durch einen einzigen Aufruf aus dem Codebehind von MainPage.Xaml ausgelöst werden können. Der StoryboardDelegate Delegat, den wir erstellen, ist ein MulticastDelegate. Er bekommt die Signatur (ByVal storyboard As Storyboard) und übernimmt damit als Parameter ein Storyboard-Objekt.

Die drei privaten Delegaten vom Typ StoryboardDelegate werden in der Klasse StoryboardEventHelper definiert. Jeder Delegate zeigt auf eine andere Routine und jede dieser Routinen hat ihre eigene Aufgabe. Die erste Routine startet einfach das übergebene Storyboard. Die zweite Routine vermerkt, dass das übergebene Storyboard gestartet wurde und macht darauf aufmerksam, indem sie das OnStoryboardStarted-Ereignis auslöst. Die dritte Routine löst schließlich, nachdem das Storyboard gestartet wurde, alle 10ms das OnStoryboardPositionChanged-Ereignis aus. Die beiden Ereignisse (OnStoryboardStarted und OnStoryboardPositionChanged) sind Ereignisse der StoryboardEventHelper-Klasse und können im Codebehind von MainPage.xaml über Ereignishandler behandelt werden. Um das zu erreichen, wird im Codebehind von MainPage.xaml eine private Variable vom Typ StoryboardEventHelper mit dem Schlüsselwort WithEvents deklariert.

Das Storyboard, dem die Ereignisse OnStoryboardStarted und OnStoryboardPositionChanged hinzugefügt werden sollen, wird nicht direkt mit der Methode Storyboard.Begin() im Codebehind von MainPage.xaml gestartet. Vielmehr wird das betreffende Storyboard im Codebehind von MainPage.xaml dem weiteren Delegaten vom Typ StoryboardDelegate übergeben, den wir in der Klasse StoryboardEventHelper als öffentlich deklariert haben. Das ist der Delegat mit dem Namen EventHelper.

Nachdem das Storyboard gestartet ist können wir dann über die WithEvents deklarierte Variable des Typ StoryboardEventHelper auf die Ereignisse reagieren.

Schritt-für-Schritt

Bevor es zu abstrakt wird, steigen wir ein und bauen Schritt-für-Schritt die Klasse StoryboardEventHelper auf.

Schritt 1 - Die Oberfläche der Anwendung

Mit Expression Blend 4 erstellen wir ein neues Silverlight 4-Projekt mit VB.NET als Programmiersprache. Der Oberfläche fügen wir ein Rectangle hinzu, dem wir den Namen R1 geben. Dann erstellen wir in Blend ein neues Storyboard mit einer Dauer von 2000ms. Im Verlauf des Storyboard bewegt sich R1 von links nach rechts und wieder zurück nach links zur Ausgangsposition. Das Storyboard nennen wir sbMove.

Dem Rectangle R1 fügen wir zwei VisualStates hinzu. Im VisualState normal hat das Rectangle einen schwarzen Hintergund. Im VisualState flipped ist der Hintergund dunkelblau und das Rectangle dreht sich um die X- und Y-Achse jeweils um 360°.

Hier ist das Xaml unserer Oberfläche:


Was wir nun erreichen wollen ist, dass R1 bei der Storyboard-Position 500ms von VisualState normal zum VisualState flipped wechselt und bei der Storyboard-Position 1500ms wieder zurück zum VisualState normal wechselt. Das Erreichen wir mithilfe der Klasse StoryboardEventHelper.

Das fertige Ergebnis sieht dann so aus:


Schritt 2 - Multicast-Delegaten in der Helferklasse deklarieren

In Visual Studio 2010 wird dem Projekt eine neue Klasse hinzugefügt, StoryboardEventHelper.vb. Der Klasse wird ein Multicast-Delegat mit der folgenden Signatur hinzugefügt:


Diese eine Zeile Quellcode definiert einen MulticastDelegate. Ein MulticastDelegate zeichnet sich dadurch aus, dass er eine Aufrufliste hat, die mehr als ein Element, hier vom Typ StoryboardDelegate, enthalten kann. Vor allem aber werden die Delegaten in der Aufrufliste eines MulticastDelegate synchron, also gleichzeitig, aufgerufen.

Schritt 3 - Die Aufrufliste des MultiCastDelegate erstellen und Programmlogik implementieren

Um die Aufrufliste des MulticastDelegate StoryboardDelegate zu erstellen, werden zunächst drei private MulticastDelegaten des Typs StoryboardDelegate mit dem Schlüsselwort New instanziert:


Jede dieser Variablen des Typs StoryboardDelegate zeigt auf eine andere Methode, in der die entsprechende Programmlogik ausgeführt werden soll.

Der Delegat _Start zeigt auf die Methode StartStoryboard. StartStoryboard hat die Aufgabe, das übergebene Storyboard zu starten:


Der Delegat _NotifyStart zeigt auf die Methode NotifyStartStoryboard. NotifyStartStoryboard hat die Aufgabe, das Ereignis OnStoryboardStarted auszulösen:


Das Ereignis OnStoryboardStarted definieren wir als öffentliches Mitglied der Klasse StoryboardEventHelper:



In der Methode NotifyStartStoryboard wird dem Ereignis OnStoryboardStarted als Parameter die aktuelle Instanz des StoryboardEventHelper Objekts übergeben, das ist Me, und als EventArgs eine neue Instanz der benutzerdefinierten StoryboardEventArgs. Die Klasse StoryboardEventArgs wird der Einfachheit halber direkt in der Klasse StoryboardEventHelper definiert:



Die Klasse StoryboardEventArgs hat nur ein einziges öffentliches Mitglied des Typs TimeSpan, das ReadOnly deklariert ist. Es ist das Mitglied StoryboardPosition. Der Wert von StoryboardPosition wird im Konstruktor hinzugefügt. Dazu wird mit der Methode GetCurrentTime() die aktuelle Timeline Position des übergebenen Storyboards abgefragt. Das passiert wie gesagt in dem Moment, in dem die Instanz der Klasse StoryboardEventArgs instanziert wird.

Es ist letztlich der Rückgabewert des Mitglieds StoryboardPosition der Klasse StoryboardEventArgs der es ermöglicht, die aktuelle Zeitposition des zu überwachenden Storyboards im Codebehind von MainPage.xaml abzufragen. Dazu wird einfach im Ereignis-Handler von OnStoryboardPositionChanged der Wert von StoryboardPosition abgerufen. Das Ereignis OnStoryboardPositionChanged wird in der Klasse StoryboardEventHelper als öffentliches Mitglied deklariert:


Das Ereignis OnStoryboardPositionChanged wird in der Methode StartStoryboardFire ausgelöst. Den Aufruf von StartStoryboardFire übernimmt der dritte private Delegat des Typs StoryboardDelegate mit dem Namen _FirePosition. Dieser Delegat zeigt auf die Methode FireStoryboardPosition. In der Methode FireStoryboardPosition erfolgt der Aufruf des Ereignisses OnStoryboardPositionChanged über einen kleinen aber wesentlichen Umweg in Gestalt eines DispatcherTimers. Es wird zunächst ein DispatcherTimer erzeugt, an dessen Tick-Ereignis die Methode StartStoryboardFire gebunden wird. Der DispatcherTimer wird zeitgleich mit dem Start des Storyboard gestartet. Jedesmal, wenn nun der DispatcherTimer tickt, wird das Ereignis OnStoryboardPositionChanged ausgelöst. Der Wert der Eigenschaft Intervall des DispatcherTimers steuert damit letztlich den zeitlichen Abstand, in dem das Ereignis OnStoryboardPositionChanged ausgelöst wird. In der Routine FireStoryboardPosition wird dem Completed Ereignis des übergebenen Storyboard schließlich noch ein Handler zugewiesen, der den DispatcherTimer stoppt, sobald das Storyboard beendet ist.

Hier ist der betreffende Quellcode-Ausschnitt:


Schließlich wird in der Klasse StoryboardEventHelper noch ein öffentliches Mitglied des Typs StoryboardDelegate gebildet. Das ist der Delegat mit dem Namen EventHelper. Dieser Delegat wird im Codebehind von MainPage.xaml aufgerufen, wo ihm das zu steuernde Storyboard übergeben wird. Im Konstruktor der Klasse StoryboardEventHelper werden dann die drei privaten Delegaten, also _Start, _NotifyStart und _FirePosition, mit der Methode Combine() der Aufrufliste des öffentlichen Delegaten EventHelper hinzugefügt. Hier ist der entsprechende Quellcode-Ausschnitt:


Der Aufruf des öffentlichen Delegaten EventHelper erfolgt dann im nächsten Schritt im Coddebehind von MainPage.xaml.

Schritt 4 - Aufruf des EventHelper und Einbau der Ereignisbehandlungen in MainPage.xaml.vb

In MainPage.xaml.vb wird zunächst eine neue Instanz der Klasse StoryboardEventHelper mit dem Schlüsselwort WithEvents erzeugt.


Zu Demonstrationszwecken soll das Storyboard durch einen Klick auf das Rectangle R1 gestartet werden. In der Ereignisbehandlung von MouseLeftButtonUp wird dazu der öffentliche Delegat EventHelper der erzeugten Instanz der Klasse StoryboardEventHelper ausgerufen und diesem das Storyboard übergeben:


Schließlich erstellen wir noch die Ereignisbehandlung für das Ereignis OnStoryboardPositionChanged der Instanz der Klasse StoryboardEventHelper und fragen darin durch Auswertung von StoryboardEventArgs.StoryboardPosition die aktuelle Laufzeitposition des Storyboard ab. Um den Wechsel des VisualStates des Rectangles R1 herbeizuführen, wird bei jedem Tick die Laufzeitposition abgefragt und bei der richtigen Position der VisualState verändert. Hier ist der entsprechende Quellcodeausschnitt:


Beim obigen Quellcodeausschnitt fällt sofort auf, dass nicht eine exakte Zeitposition überprüft wird, sondern eine Zeitspanne. Das ist einmal eine Zeitspanne zwischen 500ms und 515ms und ferner eine Zeitspanne zwischen 1500ms und 1515ms. Das liegt daran, dass der Rückgabewert von Storyboard.GetCurrentTime(), der benutzt wird, um der Eigenschaft StoryboardEventArgs.StoryboardPosition ihren Wert zuzuweisen, nicht die exakte Zeitposition des laufenden Storyboard zurückgibt. GetCurrentTime() gibt also nicht die exakte Millisekundenposition, sondern einen Wert zurück, der innerhalb einer Zeitspanne zwischen 1ms und maximal 16ms um die Werte 500 bzw. 1500 herum liegt. Woran das liegt, habe ich bislang nicht herausgefunden. Diese kleine Ungenauigkeit kann jedoch vernachlässigt werden. Denn visuell ist für den Betrachter des Storyboard ein Zeitunterschied von bis zu 15ms nicht wahrnehmbar. Deswegen schadet es im Ergebnis nicht, wenn der VisualState von R1 nicht exakt bei 500ms, sondern etwa bei 512ms wechselt. Es fällt einfach nicht auf.

Das war's. Der Quellcode für das Beispielprojekt kann in der Expression Gallery heruntergeladen werden.

Die Ereignisse OnStoryboardStarted und OnStoryboardPositionChanged feuern jetzt während des Laufs des übergebenen Storyboard anwendungsweit. Die anwendungsweite Fähigkeit auf die Ereignisse reagieren zu können hat u.a. den Vorteil, dass z.B. zu einer bestimmten Laufzeitposition des einen Storybard ein oder mehrere andere Storyboards gestartet oder gestoppt werden können.

Die dargestellte Vorgehensweise kann natürlich auch genutzt werden, um einem Storybaord weitere benutzerdefinierte Ereignisse hinzuzufügen.

Viel Spaß also mit ereignisfreudigen Storyboards.

Keine Kommentare:

Kommentar veröffentlichen