November 16, 2010

The WPF marquee text ordeal or watch your DispatchTimer usage!

Last Friday I set myself a simple enough task.  Modify my asset browser dialog so that it would scroll the text if it was too wide.  This turned out to be simple enough in the end, but it certainly took me long enough to figure out what was going wrong so I thought I’d put a quick blog up about it.  For those that can’t be bothered reading the whole thing, check the work you’re doing in any DispatchTimer tick handlers!

Here is a screenshot of the end result.  Basically it’s allowing the user to browse for video clips on a device (K2 Solo) using thumbnails of the clips.  It’s used in a library I wrote and in the application I spend most of my life developing called Uppercut.

Of course I started by Googling for various solutions out there.  The two I worked from were:
http://msdnbangladesh.net/blogs/razan/archive/2009/08/02/creating-marquee-scrolling-text-in-wpf.aspx
and
http://jobijoy.blogspot.com/2008/08/wpf-custom-controls-marquee-control.html

My first mistake was to try doing this in XAML rather than code, but on the bright side I learnt a lot in the process.  I managed to get a simple example working in Kaxaml and proceeded to wire things up to be far more complicated than they needed to be.

I setup a DataConverter to set the To parameter on the animation from the ActualWidth of the TextBlock as it needed to Negate it.  Interestingly enough I did find a handy NegateConverter that handles a lot of types you might want to negate.  This worked, but I had two problems.  The animations were jerky, and everything was animating, not just the text that was too wide to fit.

So I set out to dynamically trigger the animation.  I ended up creating a MultiConverter to compare the width of the TextBlock and the Canvas surrounding it and used the result to trigger the animation via a DataTrigger in a DataTemplate.  The problem was that as soon as I ran my code I got an error I’d never seen before… "Cannot freeze this Storyboard timeline tree for use across threads”.  Some more time with Google and it turns out that if I’m going to dynamically trigger the storyboard this way then it’s arguments (the From/To) have to be static so I couldn’t use my DataBinding.  If I hard coded the widths this approach would work, but the dialog is a useable with different sized buttons/images so that wasn’t an option.

So I finally admitted defeat and reverted to using code behind after all, like in the original solutions linked above.  I did make things a bit more complex with two DoubleAnimations but that’s just because I’m picky about things like that.  The resulting code is here:

private void MarqueeTextBlock_OnLoaded( object sender, RoutedEventArgs e )

{

    var textBlock = sender as TextBlock;

 

    if ( textBlock != null && textBlock.ActualWidth > ((Canvas)textBlock.Parent).Width )

    {

        var anim1 = new DoubleAnimation( 0, -textBlock.ActualWidth, TimeSpan.FromSeconds( 6 ) );

        var anim2 = new DoubleAnimation( textBlock.ActualWidth, 0, TimeSpan.FromSeconds( 3 ) )

        {BeginTime = TimeSpan.FromSeconds( 6 )};

 

        var sb = new Storyboard {RepeatBehavior = RepeatBehavior.Forever};               

        sb.Children.Add(anim1);

        sb.Children.Add(anim2);

        Storyboard.SetTarget(anim1, textBlock);

        Storyboard.SetTargetProperty( anim1, new PropertyPath( "(Canvas.Left)" ) );

        Storyboard.SetTarget(anim2, textBlock);

        Storyboard.SetTargetProperty(anim2, new PropertyPath("(Canvas.Left)"));

        sb.Begin();

    }

}

And that worked nicely, except the animation was still jerky. So I went back to Googling, and end up downloading and trying out the WPF Performance Toolkit.  Sadly things seemed just fine there.  My storyboards were now freezeable, the regions I’d expect were being redrawn, software rendering wasn’t being used, and barely any of my CPU was in use.  I’d now been at this for almost 2 days and was starting to think I was insane for trying it.  Though again, I’d now learnt about the toolkit which I’m sure I’ll use again in the future.

Then I had a breakthrough.  The VPN connection I was using timed out whilst I was running the application and my test app threw an exception.  Where it happened to throw it was in a timer routine that receives updates from the K2 Solo.  I’d used a DispatcherTimer for that to keep my life simple.  After all, who wants the headache of cross threading if you can avoid it?   Of course, it turns out this was the culprit of my jerky animations.  It was firing every 300ms and sending a request, via the VPN, from Melbourne (Australia) to Los Angeles for any updates.  Needless to say this was taking longer than it would on a LAN, something I’m finding is a great way to test my network code, and therefore preventing the UI rendering.

So in the end the solution for the jerky animation was simple enough.  Disable the timer while the dialog was in use, and enable it again afterwards.  I also reduced the timer to fire every 1 second and lowered it’s priority for good measure.

The end result was a lot more time spent, a lot of learning, and thankfully a reusable dialog that does just what I wanted.

0 comments: