Welcome to WindowsClient.net | Sign in | Join

SilverLaw

Silverlight Design and Development

Sponsors





  • advertise here

News

Flexible Surface Effect For Silverlight and WPF

Building A StoryboardEventHelper Class To Create Additional Events For A Silverlight 4 Storyboard

In this blog post I will describe a technique as a workaround on how to "add" additional events to a Silverlight 4 Storyboard. The Silverlight 4 Storyboard class only has one single event, which is the Completed event. It fires when the storyboard object has completed playing. There a numerous needs to add additional events to a storyboard but the Storyboard class is declaired NotInheritable, so that it's not possible to inherit from Stroyboard.

This article provides a workaround to "add" additional events to a Silverlight 4 storyboard. We will build a VB.NET helper class named StoryboardEventHelper. This class will provide two additional events which both fire application wide. Once we have created the class we have for example an easy way to change the VisualState of any Object at a certain timeline position of a running storyboard.

How it works in general

The idea behind this workaround is to declare and combine three different delegates of type StoryboardDelegate so that they can be triggered with one single call from the code behind of MainPage. The StoryboardDelegate is a MulticastDelegate and gets the signature of (ByVal storyboard As Storyboard) which means it takes a storyboard as parameter. The three delegates are defined in the StoryboardEventHelper class. Each delegate of type StoryboardDelegate points to a different routine and each routine has a certain task. One routine starts the passed storyboard, another routine notifies that the passed storyboard has been started by raising the public OnStoryboardStarted event and finally a third routine raises the OnStoryboardPositionChanged event each 10 ms after the passed storyboard has begun. Both events are events of the StoryboardHelperClass object and can be handled in the code behind of MainPage. Therefore we need a variable declared as WithEvents of type StoryboardEventHelper in the code behind of MainPage.XAML. To start a storyboard we do not call Storyboard.Begin in the code behind of MainPage but simply call a public delegate of type StoryboardDelegate (named EventHelper), which we define as a public member of the StoryboardEventHelper class. All we have to do to start and to take control over a storyboard is to create and instantiate a new variable of type StoryboardEventHelper, declared WithEvents, and call the EventHelper delegate to which we pass the storyboard that we want to play and take control over.  

Step By Step

Now lets take a look at how we build that step by step. I am using Expression Blend 4 and Visual Web Developer 2010 Express.

Step 1 - Build the UI

Open Blend 4 and create a new Silverlight 4 project with VB.NET as code behind language. The UI is simple as can be. We add a rectangle to LayoutRoot and create a storyboard with a timeline of 2 seconds (2000 ms) which will move the rectangle from left to right and back from right to left (to the initial position). Name the storyboard sbMove.

We name the rectangle "R1" and add two VisualStates to the rectangle. In VisualState normal the rectangle has a white colored background. In VisualState flipped the rectangle has a darkblue colored background and is flipped over the X and Y axis each with a projection of 360°.

What we want now is that the rectangle changes its VisualState while the storyboard plays from normal to flipped at timeline position 500 ms and changes its VisualState back from flipped to normal at timeline position 1500 ms. This will be the task of our StoryboardHelperClass that we build next. Before you read on you could take a look at the sample application at the Expression Gallery to visually get a better understanding of what we want to achieve.

Step 2 - Creating a MulticastDelegate in the helper class

In Visual Web Developer 2010 Express we add a new class to the project, we call this class StoryboardEventHelper.

To the code of this class we first add a delegate type named StroyboardDelegate with the following signature:

Public Class StoryboardEventHelper

 

  Delegate Sub StoryboardDelegate(ByVal storyboard As Storyboard)

 

  ' ...

 

With this basic line of code we define a MulticastDelegate class. It enables us to have more then one element in the invocation list of StoryboardDelegate and more important, the delegates in that invocation list, which is a linked list of delegates, are called synchronously.

Step 3 - Creating the invocation list of the MulitcastDelegate etc.

Next we need to create the invocation list for our MulticastDelegate StoryboardDelegate. Therefore we first instantiate three MulticastDelegates of type StoryboardDelegate by using the New keyword:

  Private _Start As New StoryboardDelegate(AddressOf StartStoryboard)

 

  Private _NotifyStart As New StoryboardDelegate(AddressOf NotifyStartStoryboard)

 

  Private _FirePosition As New StoryboardDelegate(AddressOf FireStoryboardPosition)

Each variable of type StoryboardDelegate points to a different method in which the corresponding action should take place.

The delegate _Start variable points to the method StartStoryboard. StartStoryboard does nothing more then to start the storyboard playing:

  Public Sub StartStoryboard(ByVal mySB As Storyboard)

 

    mySB.Begin()

 

  End Sub

The delegate _NotifyStart variable points to the method NotifyStartStoryboard. NotifyStartStoryboard raises the event OnStoryboardStarted:

  Public Sub NotifyStartStoryboard(ByVal mySB As Storyboard)

 

    RaiseEvent OnStoryboardStarted(Me, New StoryboardEventArgs(mySB))

 

  End Sub

The OnStoryboardStarted event we define as a public member of StoryboardEventHelper class:

  Public Event OnStoryboardStarted As EventHandler

In the method NotifyStartStoryboard we pass as parameter of the OnStoryboardStarted event the actual instance of StoryboardEventHelper object, which is Me, and as EventArgs a new instance of the custom StoryboardEventArgs. The StoryboardEventArgs class we define directly in the StoryboardEventHelper class:

  Public Class StoryboardEventArgs

    Inherits EventArgs

 

    Public ReadOnly StoryboardPosition As TimeSpan

 

    Public Sub New(ByVal storyboard As Storyboard)

      Me.StoryboardPosition = storyboard.GetCurrentTime

    End Sub

 

  End Class

This custom StoryboardEventArgs class holds a Public ReadOnly member of type TimeSpan named StoryboardPosition. The value of StoryboardPosition is set in the contructor of the class to which the storyboard is passed. In the constructor we use the Storyboard.GetCurrentTime method to get the current timeline position of the storyboard at the moment, when the StoryboardEventArgs is instantiated. The method GetCurrentTime retrieves the current time of the clock that was created for the playing Storyboard.

Using the custom StoryboardEventArgs enables us in the code behind of MainPage.XAML.VB to get the current time of the storyboard. Therefore we simply check the StoryboardEventArgs, as you will see later on. For the OnStoryboardStarted event the StoryboardEventArgs are not the very important, because when the storyboard starts the current time will always be at timeline position 0 ms. But they are essentially important for the OnStoryboardPositionChanged event.

The OnStoryboardPositionChanged event is raised in the method StartStoryboardFire that is called from the method FireStoryboardPosition to which the delegate _FirePosition variable is pointing. This is the code for the FireStoryboardPosition method and the corresponding variables and methods needed:

  Private mySB As Storyboard

 

  Private StoryboardDispatcherTimer As New System.Windows.Threading.DispatcherTimer

 

  Private HasFiredBefore As Boolean

  Public Sub FireStoryboardPosition(ByVal mySB As Storyboard)

 

    Me.mySB = mySB

 

    StoryboardDispatcherTimer.Interval = New TimeSpan(0, 0, 0, 0, 10)

    StoryboardDispatcherTimer.Start()

 

    If Not Me.HasFiredBefore Then

 

      AddHandler StoryboardDispatcherTimer.Tick, AddressOf StartStoryboardFire

      AddHandler mySB.Completed, AddressOf StopStoryboardDispatcherTimer

      Me.HasFiredBefore = True

 

    End If

 

  End Sub

 

  Public Sub StartStoryboardFire()

 

    RaiseEvent OnStoryboardPositionChanged(Me, New StoryboardEventArgs(mySB))

 

  End Sub

  Private Sub StopStoryboardDispatcherTimer()

 

    StoryboardDispatcherTimer.Stop()

 

  End Sub

This code is using a DispatcherTimer that starts when the storyboard begins playing. The DispatcherTimer ticks each 10 ms. To the Tick event of the DispatcherTimer the method StartStoryboardFire is added. And in the StartStoryboardFire method the OnStoryboardPositionChanged event is raised. As a result, OnStoryboardPositionChanged event is raised each 10 ms after the storyboard started playing. The OnStoryboardPositionChanged event itself is defined as a public member of the StoryboardEventHelper class:

  Public Event OnStoryboardPositionChanged As EventHandler

The last step in the code behind of StoryboardEventHelper class is to create a public member of type StoryboardDelegate, named EventHelper, and to combine the three delegate StoryboardDelegate variables in the invocation list of that public member, which is a MulticastDelegate. The combining takes place in the constructor of the class:

  Public EventHelper As StoryboardDelegate

 

  Sub New()

 

    EventHelper = CType([Delegate].Combine(EventHelper,

                                           _Start), StoryboardDelegate)

    EventHelper = CType([Delegate].Combine(EventHelper,

                                           _NotifyStart), StoryboardDelegate)

    EventHelper = CType([Delegate].Combine(EventHelper,

                                           _FirePosition), StoryboardDelegate)

 

  End Sub

As the final step we switch to MainPage.XAML.VB and implement the code so that we can react on the two events.

Final Step 4 - MainPage.XAML.VB

In MainPage.XAML.VB we create a StoryboardEventHelper object, declared WithEvents. In the MouseLeftButtonUp event handler of rectangle R1 we call the (MulticastDelegate) delegate EventHelper of StoryboardEventHelper and pass the storyboard sbMove as parameter. Here is the code behind snippet of MainPage.XAML.VB:

  Private WithEvents _StoryboardEventHelper As New StoryboardEventHelper

 

  Private Sub R1_MouseLeftButtonUp _

    (

    ByVal sender As Object,

    ByVal e As System.Windows.Input.MouseButtonEventArgs

    ) _

  Handles R1.MouseLeftButtonUp

    _StoryboardEventHelper.EventHelper(sbMove)

  End Sub

 

  Private Sub _StoryboardEventHelper_OnStoryboardPositionChanged _

    (

    ByVal sender As Object,

    ByVal e As System.EventArgs

    ) _

  Handles _StoryboardEventHelper.OnStoryboardPositionChanged

    Dim CurrentPosition As TimeSpan =

      CType _

      (

        e,

        StoryboardEventHelper.StoryboardEventArgs

      ).StoryboardPosition

    If CurrentPosition.TotalMilliseconds >= 500 _

      And CurrentPosition.TotalMilliseconds <= 515 Then

      VisualStateManager.GoToState(Me, "flipped", True)

    ElseIf CurrentPosition.TotalMilliseconds >= 1500 _

      And CurrentPosition.TotalMilliseconds <= 1515 Then

      VisualStateManager.GoToState(Me, "normal", True)

    End If

 

  End Sub

Because of the _StoryboardEventHandler object is declared WithEvents, we can build an event handler for the OnStoryboardPositionChanged event of that object. In the _StoryboardEventHelper_OnStoryboardPositionChanged event handler we check the value of e, which is of type EventArgs. Because of it is of type EventArgs we have to convert it using CType to custom type StoryboardEventArgs. We take the value of StoryboardPosition and assign it to the variable CurrentPosition which is of type TimeSpan.

As we saw, the OnStoryboardPositionChanged event fires each 10 ms and each time the event fires, we check in the If clause the TotalMilliseconds value of CurrentPosition. As you can see, we do not check for the exact value of 500 and 1500 respectively. The reason for that is, that the clock of our storyboard, exposed by the GetCurrentTime method, which we placed in the constructor of our custom StoryboardEventArgs, only gives us a value of type Double which is not the exact current timeline position but a value that is within a range of 1 to 16 around the value of 500 and 1500 respectivley. I don't know the reason for that and if you have any idea on that it would be nice if you would contact me. This is a small limitation but it visually doesn't affect the functionality of our events. A delay of maximum 16 ms isn't visible for the human eye.

That's it. The OnStoryboardStarted event and the OnStoryboardPositionChanged event fire application wide. And, while the storyboard plays, (nearly) at the timeline positions 500 ms and 1500 ms the VisualState of R1 changes from normal to flipped and from flipped to normal respectively. The implementation of the OnStoryboardStarted event works similar.

You can view an example and get the full sourcecode at the Expression Gallery, where I contributed a small sample application.

Hope this helps.

Best regards,

Martin

 

 

Comments

No Comments

Leave a Comment

(required) 

(required) 

(optional)

(required) 

Page view counter