2

I have an attached behavior that has a single attached property of type StoryBoard. I want to set this property on every item in a ListView. The XAML looks something like this:

<Grid>
   <Grid.Resources>
      <Storyboard x:Key="TheAnimation" x:Shared="False">
         <DoubleAnimation From="0.0" To="1.0" Duration="0:0:0.20"
             Storyboard.TargetProperty="Opacity" />
      </Storyboard>
   </Grid.Resources>

   <ListView>
      <ListView.Resources>
         <Style TargetType="{x:Type ListViewItem}">
            <Setter Property="local:MyBehavior.Animation"
               Value="{StaticResource TheAnimation}" />
         </Style>
      </ListView.Resources>
   </ListView>
</Grid>

So far so good. Then the code in 'MyBehavior' tries to do this:

private static void AnimationChanged(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 an InvalidOperationException is thrown 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." If I inspect the Storyboard in the debugger, I can see that both its IsSealed and IsFrozen properties are set to true.

By contrast, if I set MyBehavior.Animation directly on the ListView so that I don't need to use a Style, the StoryBoard arrives unsealed and I am able to set the target and run it successfully. But that's not where I want it.

Why is my StoryBoard being sealed, and is there anything I can do to prevent this?

Update: I can solve my problem by adding this right after the null check:

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

But I'm still curious what's going on. Apparently something somewhere (Style? Setter?) is freezing/sealing the object in Setter.Value.

dlf
  • 9,045
  • 4
  • 32
  • 58
  • This makes sense when you stand back and think about it (I think). A xaml `Style` node presumably translates into a single `Style` object. That single object, along with the `Value` objects in its `Setter`s, is potentially shared among multiple objects. So if one of them intercepts the `Value` object in some way (a `PropertyChangedCalledback` in my case) and modifies it, it would be inadvertently affecting all the other objects that share the style. The framework developers anticipated this and sealed the `Value`s to make sure that couldn't happen. That's my guess, at least. – dlf Jul 18 '15 at 14:18
  • That train of thought led me to this [related question](http://stackoverflow.com/q/9235428/3549027) – dlf Jul 18 '15 at 14:32
  • [Also related](http://www.pcreview.co.uk/threads/wpf-using-storyboards-in-custom-control.3591259/) – dlf Jul 18 '15 at 14:40
  • _"would be inadvertently affecting all the other objects that share the style"_ -- that's exactly the point I made in my answer. – Peter Duniho Jul 18 '15 at 17:32

2 Answers2

2

I'm far from an expert in WPF, so I cannot explain the finer details of why this was the choice Microsoft made. But as I understand it, the main issue is that the object that is declared as a resource is likely to be shared with multiple other objects. As such, you are prevented from modifying it.

If you still want to go the resources route, it is possible that you can treat the resource as {DynamicResource...} instead of {StaticResource...} and that might allow you to modify the object that's been used for some other object. As I said, I'm not an expert in WPF and I admit to still being a bit cloudy on the different between DynamicResource and StaticResource, but I have a vague recollection that it addresses this scenario. :)

Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
  • 'DynamicResource` was one of the things I tried, but it didn't help. But see my comment with the original question; I may understand what's going on. – dlf Jul 18 '15 at 14:19
2

I've done some research on this, and think I've worked out most of what's going on. The short answer is that this behavior is by design. The MSDN Styling and Templating page for .net 4.0 says flat-out that "once a style has been applied, it is sealed and cannot be changed." Comments with Style.IsSealed back this up. But that's the Style itself; I'm dealing with an object contained in a Style's Setter's Value. Well, Style.Seal seals all its Setters, and Setter.Seal seals its Value. With that info in hand (head), none of what happened here is particularly shocking. But there's still no explanation for why all this sealing is being done in the first place. There are claims here and here that it is related to thread safety. That seems reasonable, but I would speculate further that, if all objects that consume a particular Style share a single Style object (and I don't know if that's the case or not), the sealing might be done for the simple reason that you don't want one consumer modifying the Style and accidentally changing everyone else.

All this seems to mean that there is no general solution to the problem, and it will need to be solved on a case-by-case basis. In my case, the solution was simply to clone the Storyboard and then operate on that clone.

Community
  • 1
  • 1
dlf
  • 9,045
  • 4
  • 32
  • 58
  • As I mentioned, the likely reason is to ensure that objects intended to be shared aren't modified (i.e. affecting many dependent objects). This could be a performance issue (too many changes propagating through the binding system) or just an API usability issue. It's not a threading issue, because WPF objects are tied to a specific thread and can't be used in multiple threads anyway. – Peter Duniho Jul 18 '15 at 17:31