2

I've spent the last few days looking at the DispatcherTimer and I still can't wrap my head around some stuff. Here's what I understand so far,

  1. the tick event will not occur twice at the same time link
  2. there is no need to worry about the owner thread(s) of the objects since the dispatcher timer automatically performs all the work in the UI thread
  3. the timing of the ticks may not be very accurate since the ticks are essentially executed from a queue

Now what I'm not clear about is the order of the code being executed if there is another event which runs in between a tick event. I've a test WPF application which uses a DispatcherTimer whose tick event performs 2 functions. firstStep() and secondStep() in sequence.

The firstStep()sets a variable to null while secondStep() sets it to a value that is not null. After setting the value, secondStep() will begin a storyboard which has a Completed event, which attempts to access this variable.

So my question is, is it possible for the Completed event to come in between the firstStep() and secondStep() function if we keep the timer running? I've written a test application and it seems to be that case, eventually we will reach a state where the variable is null when the Completed event gets executed. But I don't understand how that can happen, since firstStep() and secondStep() get executed in sequence, there should be no way the Completed event can be executed between the 2 functions (or I am wrong here). Does the UI thread execute the tick and the Completed event in parallel?

Can someone explain to me in detail how the UI thread executes events such as the example's storyboard completed event and dispatcherTimer's ticks in sequence? Thanks for reading, your comments are very much appreciated I'm trying very hard to get my head around this. The following is the test code I used, it will eventually throw an error after running for a while.

public partial class Window1 : Window
{
    public Window1()
    {
        InitializeComponent();

        storyBoardTest = new Storyboard();
        storyBoardTest.Completed += new EventHandler(storyBoardTest_Completed);
        DoubleAnimation animation = new DoubleAnimation(1, 0.9, new Duration(TimeSpan.FromSeconds(1)));
        Storyboard.SetTarget(animation, this);
        Storyboard.SetTargetProperty(animation, new PropertyPath(UIElement.OpacityProperty));
        storyBoardTest.Children.Add(animation);

        DispatcherTimer dt = new DispatcherTimer();
        dt.Interval = TimeSpan.FromMilliseconds(500);
        dt.Tick += new EventHandler(dt_Tick);
        dt.Start();

    }

    private Window windowTest = null;
    private Storyboard storyBoardTest = null;

    void dt_Tick(object sender, EventArgs e)
    {
        firstStep();
        secondStep();
    }

    private void firstStep()
    {
        windowTest = null;
    }

    private void secondStep()
    {
        windowTest = this;
        storyBoardTest.Stop();
        storyBoardTest.Begin(this);
    }

    void storyBoardTest_Completed(object sender, EventArgs e)
    {
        //Attempt to access object throws null error. Why?
        windowTest.Title = "test";
        windowTest = null;
    }
}

CallStack:

WpfApplication1.exe!WpfApplication1.Window1.storyBoardTest_Completed(object sender = {System.Windows.Media.Animation.ClockGroup}, System.EventArgs e = null) Line 63 C# PresentationCore.dll!System.Windows.Media.Animation.Clock.FireEvent(System.Windows.EventPrivateKey key) + 0x5b bytes
PresentationCore.dll!System.Windows.Media.Animation.Clock.RaiseAccumulatedEvents() + 0x160 bytes
PresentationCore.dll!System.Windows.Media.Animation.TimeManager.RaiseEnqueuedEvents() + 0x60 bytes
PresentationCore.dll!System.Windows.Media.Animation.TimeManager.Tick() + 0x28a bytes
PresentationCore.dll!System.Windows.Media.MediaContext.RenderMessageHandlerCore(object resizedCompositionTarget) + 0xbc bytes
PresentationCore.dll!System.Windows.Media.MediaContext.AnimatedRenderMessageHandler(object resizedCompositionTarget) + 0x9d bytes

Community
  • 1
  • 1
black eyed pea
  • 427
  • 1
  • 7
  • 16
  • If the completed event was occurring in parallel that would be disturbing. The UI thread model is supposed to prevent such things. Can you post the call stack for the completed event? Maybe it is just reentrant. – usr Feb 16 '13 at 12:40
  • yes that's what i thought too, it should all be part of a queue right? In that case it should always be `firstStep(),secondStep()` as a block, and the `Completed` event should always be able to access the value – black eyed pea Feb 16 '13 at 12:41

1 Answers1

2

Every 500 milliseconds you are starting a Storyboard that runs for one second. This will inevitably lead to two consecutive Completed events without an intermediate Tick event.

Therefore you have to check if windowTest is already null in your Completed handler :

void storyBoardTest_Completed(object sender, EventArgs e)
{
    if (windowTest != null)
    {
        windowTest.Title = "test";
        windowTest = null;
    }
}

Even if the Storyboard would run for less than 500 milliseconds there would be problem. As Storyboard.Completed events are appended to the Dispatcher queue in the same way as DispatcherTimer.Tick events and the timings of both DispatcherTimer and Storyboard are not exact, the execution order of the two event handlers is not reliable. Hence two Completed events may occur without an intermediate Tick event.


You may add some trace output to see that both handlers run in the same thread.

void dt_Tick(object sender, EventArgs e)
{
    Trace.TraceInformation("Tick: {0}", Thread.CurrentThread.ManagedThreadId);
    ...
}

void storyBoardTest_Completed(object sender, EventArgs e)
{
    Trace.TraceInformation("Completed: {0}", Thread.CurrentThread.ManagedThreadId);
    ...
} 
Clemens
  • 123,504
  • 12
  • 155
  • 268
  • thanks Clemens, I've just tested by adding a storyBoardTest.Stop(); before storyBoardTest.Begin(); the same error still occurs. Doesn't the storyBoardTest.Stop(); statement prevent multiple Completed events from taking place at the same time? – black eyed pea Feb 16 '13 at 13:44
  • I can't call `storyBoardTest.Stop()`. It says `Unable to perform action because the specified Storyboard was never applied to this object for interactive control.` – Clemens Feb 16 '13 at 16:46
  • I ran some tests on the StoryBoard Completed event just now, spent about 3 hours on it. The conclusion is, it's **not** possible to know if the storyboard completes or not. Calling StoryBoard.Stop() does not guarantee that it will not call StoryBoard_Completed event. The only way is to create a new storyboard each time. – black eyed pea Feb 16 '13 at 17:26
  • When you call Storyboard.Stop at the very moment when the Storyboard finishes, a Completed event may have already been queued to the Dispatcher, but not yet received by your application. – Clemens Feb 16 '13 at 17:41