WPF: Using Storyboards in Custom Control

  • Thread starter Thread starter Wonko the Sane
  • Start date Start date
W

Wonko the Sane

Hello All,

I have defined a custom control (call it CustomButton).

One thing that I would like to put into this custom control is a color
animation (in its ContentTemplate). However, I would like to be able to
specify the Color in this animation via binding.

Poking around, I found this post
(http://forums.msdn.microsoft.com/en-US/wpf/thread/9336022f-badb-4b40-a86c-a50ab1a64ba5/)
that seems to say that it cannot be done (I do get the exception thrown as
described in the article). Does anybody have a workaround that allows for a
bindable property?

Thanks,
WtS
 
Hi Wonko,

Unfortunately, this is a by-design "feature" of WPF, Styles and Templates
could be used in multiple referencing points (for instance, when you define
a Button style, these styles will be used by multiple Button instances
residing across the visual tree), and those referencing points will
potentially be created by different threads. So in order to make them
thread safe when used across threading boundaries, Styles and Templates
will be sealed, if you use reflector to examine the Style and
FrameworkTemplate class definition, you will find that both Style and
FrameworkTemplate classes all implement ISealable internal interface, this
interface imposes the sealable contract, which means that when a Style or
Template is sealed, it will be made read-only, read-only objects could be
used safely across threading boundaries, and any Freezable objects within
the Style and Template definition will also be frozen (because Freezable
objects take freezing semantic), this is expected, since when you want to
make an object read-only, you need to make sure that any objects which this
object has reference to should also be made read-only. The caveat here is
that Freezable objects when have data binding or dynamic resource
referencing (via DynamicResourceExtension) set on it cannot be frozen,
because data binding or dynamic resource referencing relies on the specific
contextual information to make it work (thinking of RelativeSource and
ElementName data binding), and if WPF allows you to set data binding on
frozen Freezable objects, those Freezable objects could be used and
attached to multiple parenting points (to make the matter worse, those
parenting points could reside in different threads), which could confuse
the RelativeSource and ElementName data binding, and potentially cause some
nasty threading issues.

So in order to work around this limitation, you could choose not to
introduce any Freezable objects inside Styles or Templates; you could place
the Storyboard/TimelineAnimation inside the theme resource dictionary as
follows:

<SolidColorBrush x:Key="brush" Color="Green"/>
<!--Note that in order to refer to theme resources, you need to use
ComponentResourceKey.
And you also need to specify the x:Shared attribute to false to
prevent resource sharing, this could effectively workaround
the limiation that Freezable resources will be frozen by the theme
dictionary loading code-->

<ColorAnimation
x:Key="{ComponentResourceKey ResourceId=ColorAnimation,
TypeInTargetAssembly={x:Type local:CustomButton}}"
x:Shared="False"
To="{Binding Path=Color, Source={StaticResource brush}}"
Duration="0:0:2" RepeatBehavior="Forever"/>
<Style TargetType="{x:Type local:CustomButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<TextBlock Text="CustomButton" Tag="Green" x:Name="textBlock">
<TextBlock.Background>
<SolidColorBrush x:Name="back" Color="Red"/>
</TextBlock.Background>
</TextBlock>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

And inside the CustomButton control implementation, you could override the
OnApplyTemplate() protected method as follows:

public override void OnApplyTemplate()
{
base.OnApplyTemplate();
//Presumably you could use FrameworkElement.FindResource() or
FrameworkElement.TryFindResource() to look up the "ColorAnimation"
resource,
//unforunately those two APIs will freeze resource before returning it
for your //disposal. That's why I use the SetResourceReference hack here.
this.SetResourceReference(Button.TagProperty, new
ComponentResourceKey(typeof(CustomButton), "ColorAnimation"));

SolidColorBrush solidColorBrush = this.GetTemplateChild("back") as
SolidColorBrush;
this.MouseEnter += delegate
{
ColorAnimation colorAnimation = this.Tag as ColorAnimation;
if (colorAnimation != null && solidColorBrush != null)
{
Console.WriteLine(colorAnimation.IsFrozen);
solidColorBrush.BeginAnimation(SolidColorBrush.ColorProperty,
colorAnimation);
}
};
}

The above method looks like a little bit like a hackery, so I recommend you
to construct Animations and Storyboards completely at the code behind
(presumably inside the instance constructor of CustomButton class (note
that per instance Freezable objects are thread safe because WPF imposes the
dispatcher threading model for Freezable objects), then you could hook up
to the any events such as MouseEnter/MouseLeave to start and stop those
Animations and Storyboards.

--------------------------------------------------
Best regards,
Macro Zhou ([email protected], remove 'online.')
Microsoft Online Community Support

Delighting our customers is our #1 priority. We welcome your comments and
suggestions about how we can improve the support we provide to you. Please
feel free to let my manager know what you think of the level of service
provided. You can send feedback directly to my manager at:
(e-mail address removed).

This posting is provided "AS IS" with no warranties, and confers no rights.
 
Back
Top