Saturday, July 18, 2015

WPF: Using an object as the Value in a Style Setter will seal and freeze it

The title says it all. I discovered this during an attempt to animate the addition of a ListViewItem to a ListView. I decided to do this via an attached behavior that involved an attached property of type Storyboard. The XAML looked something like this:

<Window>
   <Window.Resources>
      <Storyboard x:Key="TheAnimation" x:Shared="False">
         <DoubleAnimation From="0.0" To="1.0" Duration="0:0:0.20"
            Storyboard.TargetProperty="Opacity" />
      </Storyboard>
   </Window.Resources>
    
   <Grid>
      <ListView>
         <ListView.Resources>
            <Style TargetType="{x:Type ListViewItem}">
               <Setter Property="local:MyBehavior.Storyboard"
                  Value="{StaticResource TheAnimation}" />
            </Style>
         </ListView.Resources>
      </ListView>
   </Grid>
</Window>

As you can see, because the ListViewItems are not directly accessibly in XAML, I used a Style to set their MyBehavior.Storyboard attached properties. Then, in the C# code for the behavior, I tried to do this:

private static void StoryboardChanged(DependencyObject d,
   DependencyPropertyChangedEventArgs e)
{
   var listViewItem = d as ListViewItem;
   if (d == null)
      return;

   var sb = e.NewValue as Storyboard;
   if (sb == null)
      return;

   Storyboard.SetTarget(sb, listViewItem);
   sb.Begin();
}

But this blew up with an InvalidOperationException on the call to Storyboard.SetTarget():
Cannot set a property on object 'System.Windows.Media.Animation.Storyboard' because it is in a read-only state
And indeed, inspecting sb in the debugger showed that its IsSealed and IsFrozen properties were both set to true.

Searching around the web turned up evidence that Styles are indeed sealed by design the first time they're used. For example, this page at MSDN ("Note that once a style has been applied, it is sealed and cannot be changed. If you want to dynamically change a style that has already been applied, you must create a new style to replace the existing one"). Examining the source code confirms this, and also demonstrates that sealing a Style seals its Setters, and sealing a Setter seals its Values. So there's no mystery about what happened, but it's not at all clear why all of this sealing is being done in the first place. There are a few claims that this has to do with thread safety, for example, here and here. That seems plausible, but I wonder if there isn't another reason toothe Style node in the XAML presumably translates into a single Style object that is shared among all the objects that consume it. If one of them were to modify it (or its contents) in some way, the change would affect everyone, which is probably not what you want. This is just a guess; I haven't been able to prove it, but it makes sense. Assuming, of course, that I am correct in presuming that only one Style object is created.

In any case, in this case, I was able to solve my problem simply by cloning the Storyboard and working with the clone:

if(sb.IsSealed || sb.IsFrozen)
   sb = sb.Clone();

A more general solution might be to make the property type XyzFactory instead of Xyz, though that would obviously limit your ability to put things together in markup.

No comments:

Post a Comment