Add to Faves Add to MyAOL Add to Simpy Add to Delicious Add to Live Add to Digg Add to Newsvine Add to Reddit Add to Multiply Add to Blogmarks Add to Yahoo MyWeb Add to Slashdot Add to Mister Wong Add to Spurl Add to Furl Add to Link-a-Gogo Add to Yahoo Bookmarks Add to Twitter Add to Facebook Add to Diigo Add to Mixx Add to Segnalo Add to StumbleUpon Add to Ask Add to Backflip Add to Terchnorati Add to Google Bookmarks Add to MySpace
BlogProductsOpen SourceForumsAbout
Blog > July 2010 > Attaching Behaviors from the Expression Blend SDK using Styles (Part 2)

Attaching Behaviors from the Expression Blend SDK using Styles (Part 2)

In the previous post in this series, I introduce the reader to a new attached property that gives the ability to attach a collection of behaviors to an item through a style. Since that post, I have discovered a bug in the original implementation that prevented databindings inside the behavior to work correctly.

Rather than do a walk through, here is the full, revised version of the StylizedBehaviors class:

#region
using System;
using System.Windows;
using System.Windows.Interactivity;
#endregion

namespace LivingAgile.Common.Presentation.WPF.Behaviors
{
    public class StylizedBehaviors
    {
        #region Fields (private)
        private static readonly DependencyProperty BehaviorIdProperty =
            DependencyProperty.RegisterAttached(
                @"BehaviorId"typeof ( Guid ), typeof ( StylizedBehaviors ), new UIPropertyMetadataGuid.Empty ) );
        #endregion

        #region Fields (public)
        public static readonly DependencyProperty BehaviorsProperty = DependencyProperty.RegisterAttached(
            @"Behaviors",
            typeof ( StylizedBehaviorCollection ),
            typeof ( StylizedBehaviors ),
            new FrameworkPropertyMetadatanull, OnPropertyChanged ) );
        #endregion

        #region Static Methods (public)
        public static StylizedBehaviorCollection GetBehaviors ( DependencyObject uie )
        {
            return ( StylizedBehaviorCollection )uie.GetValue( BehaviorsProperty );
        }

        public static void SetBehaviors ( DependencyObject uie, StylizedBehaviorCollection value )
        {
            uie.SetValue( BehaviorsProperty, value );
        }
        #endregion

        #region Static Methods (private)
        private static Guid GetBehaviorId ( DependencyObject obj )
        {
            return ( Guid )obj.GetValue( BehaviorIdProperty );
        }

        private static int GetIndexOf ( BehaviorCollection itemBehaviors, Behavior behavior )
        {
            int index = -1;

            Guid behaviorId = GetBehaviorId( behavior );

            for ( int i = 0; i < itemBehaviors.Count; i++ )
            {
                Behavior currentBehavior = itemBehaviors[ i ];

                if ( currentBehavior == behavior )
                {
                    index = i;
                    break;
                }

                Guid cloneId = GetBehaviorId( currentBehavior );

                if ( cloneId == behaviorId )
                {
                    index = i;
                    break;
                }
            }

            return index;
        }

        private static void OnPropertyChanged ( DependencyObject dpo, DependencyPropertyChangedEventArgs e )
        {
            var uie = dpo as UIElement;

            if ( uie == null )
            {
                return;
            }

            BehaviorCollection itemBehaviors = Interaction.GetBehaviors( uie );

            var newBehaviors = e.NewValue as StylizedBehaviorCollection;
            var oldBehaviors = e.OldValue as StylizedBehaviorCollection;

            if ( newBehaviors == oldBehaviors )
            {
                return;
            }

            if ( oldBehaviors != null )
            {
                foreach ( var behavior in oldBehaviors )
                {
                    int index = GetIndexOf( itemBehaviors, behavior );

                    if ( index >= 0 )
                    {
                        itemBehaviors.RemoveAt( index );
                    }
                }
            }

            if ( newBehaviors != null )
            {
                foreach ( var behavior in newBehaviors )
                {
                    Guid behaviorId = GetBehaviorId( behavior );

                    if ( behaviorId == Guid.Empty )
                    {
                        behaviorId = Guid.NewGuid( );
                        SetBehaviorId( behavior, behaviorId );
                    }

                    int index = GetIndexOf( itemBehaviors, behavior );

                    if ( index < 0 )
                    {
                        var clone = ( Behavior )behavior.Clone( );
                        Guid cloneId = GetBehaviorId( clone );

                        if ( behaviorId != cloneId )
                        {
                        }

                        itemBehaviors.Add( clone );
                    }
                }
            }
        }

        private static void SetBehaviorId ( DependencyObject obj, Guid value )
        {
            obj.SetValue( BehaviorIdProperty, value );
        }
        #endregion
    }
}

The major change is that we now copy a cloned version of the behavior to the item that inherits the style. This seems to be necessary because the behavior gets the incorrect inheritance context since it really lives inside a freezable collection. Cloning the behavior seems to clear out the inheritance context and make things work correctly. To support that change, I had to add tracking ID's to the behaviors that are attached so they can be related during the remove operation.

Happy databinding on behaviors inside styles!

Posted: 7/31/2010 3:07:54 AM by Mark Smeltzer | with 7 comments
Filed under: .NET4.0, Behaviors, Expression, OpenSource, Styles, VS, VS2010, WPF, XAML, .NET


Comments
Avatar
Mark Smeltzer
Beej,

I received similar feedback from Antoine. Based on that, I took his advice and modified my code to use an "OriginalBehavior" property that is set only on the clones.

I will post the new code in a new blog entry.

Also, it should be noted that there may be a performance benefit to adding a line after the clone is created like "if (behavior.IsFrozen) clone.Freeze( );". I have not yet done that in my codebase as it just occurred to me as I was writing this.
10/25/2010 11:54:48 AM

Beej
In my WPF 4 project I was getting error: "Cannot set a property on object {behavior class name} because it is in a read-only state"...

on the "SetBehaviorId( behavior, behaviorId );" line of OnPropertyChanged

This post (http://www.codeproject.com/Messages/3113532/Re-Cannot-set-a-property-on-object-Identity-becaus.aspx) told me that it's because of the whole frozen thing you're coding around but for some reason I'm hitting it on the initial brand new behavior instance where you don't appear to have had that problem.

Anyway, I twiddled your OnPropertyChanged code to postpone the behaviorId setter to always hit the clone like below and it's working for me now... skimming your GetIndexOf() logic I think this is ok because you look at both primary and cloned instances for the lookup so i don't think i'm creating too many instances???

Thanks for the cool trick!

private static void OnPropertyChanged(DependencyObject dpo, DependencyPropertyChangedEventArgs e)
{

[...]

if (newBehaviors != null)
{
foreach (var behavior in newBehaviors)
{
Guid behaviorId = GetBehaviorId(behavior);
int index = GetIndexOf(itemBehaviors, behavior);

if (index < 0)
{
var clone = (Behavior)behavior.Clone();
if (GetBehaviorId(clone) == Guid.Empty)
{
SetBehaviorId(clone, Guid.NewGuid());
}

itemBehaviors.Add(clone);
}
}
}
}
10/25/2010 6:28:40 AM

Saheer
Hi Mark,
Will this work in Silverlight? I think FreezableCollection is not available in SL. Is there any alternate class for this/workaround for this to get working in SL?

Thanks in Advance..
10/25/2010 12:59:13 AM

Avatar
Mark Smeltzer
When you say that you are getting errors, are they compilation errors, runtime exceptions, or logged messages in the debug output window?

If the "error" is noise in the debug log output, the most likely explanation is that the binding on the original collection is interpreted as being incorrect (and indeed it is), but since that binding is just used as a template, the log "error" can be safely ignored. On that note, I wish there was a way to squelch the log output in cases where we are doing "incorrect things" on purpose, like in this case.

Thank you for your notes about the problems with implementing this for triggers. I don't currently use triggers myself, which is why I have not tried implementing style-based setters for triggers. Sadly, not all Freezable classes are created equal: if the derived class does not store *ALL* of its runtime state using dependency properties, the built in cloning procedure for Freezables will not properly clone all of the state information. This may be the case for the "Actions" property on triggers.

If all else fails and you really need this to work for triggers, you might have to go the route that I almost went, but thankfully was able to avoid, which was to use XAML serialization/deserialization as a cloning method. The problem with this method is that, unless I was doing something wrong, it did not automatically preserve property bindings -- if there is a way to do that, please let me know!

Assuming that XAML serialization does not preserve bindings, I was going to have to loop through the entire object graph (once) and cache all of the bindings and for the clone operation, I would clone the graph, then clone all of the bindings (using XAML cloning), and then apply the bindings to the clone using BindingOperations.SetBinding(). That is definitely more work, and I will probably add it as an alternative cloning method at some point in the future (controlled by an attached property I would add called "UseXamlCloning") once I run into having to support a behavior that does not properly support Freezable-based cloning.
8/24/2010 10:18:24 AM

Antoine
Thanks for your quick reply!

I renamed the class to Interaction (to reflect Microsoft's naming) and am using it like this:
<Setter
Property="my:Interaction.Behaviors">
<Setter.Value>
<my:BehaviorCollection>
<my:SomeBehavior
IsActive="{Binding IsActive}" />
</my:BehaviorCollection>
</Setter.Value>
</Setter>

The error I mentioned happens for each behavior (i.e. 2 behaviors = twice the error), but the binding ends up being correctly set and the behaviors work. I suspect that the binding tries to evaluate, but that the inheritance context is not appropriate.

Regarding triggers, this code cannot be transposed as it is.
1) The Ids cannot be set on the original objects, because they are already frozen at that point. I dropped Ids altogether (including for behaviors) and used an attached property named 'Original' and of type 'object'. Instead of setting an Id on both the original and the clone, I just set the 'Original' attached property to reference the original freezable. GetIndexOf then just compares the references.
2) The trigger actions (in the TriggerBase.Actions property) seem to be discarded by the cloning operation. I don't even know what's going on, the CreateInstanceCore() method is not called for TriggerCollection, whereas it is called for BehaviorCollection.

I'm not very optimistic for triggers.
8/24/2010 9:20:02 AM

Avatar
Mark Smeltzer
I have not had any problems with that. Could you post your XAML?
8/23/2010 10:05:05 AM

Antoine
Hi,

Thanks a lot for this, I found it interesting and tried to use it.
However I keep getting these errors when binding a behavior property:
System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element.

Do you also get that?
8/23/2010 5:31:54 AM

Recent Comments

10/25/2010
Mark Smeltzer
Beej, I received similar feedback from Antoine. Based on...

10/25/2010
Beej
In my WPF 4 project I was getting error: "Cannot set a...

10/25/2010
Saheer
Hi Mark, Will this work in Silverlight? I think Freezable...

8/24/2010
Mark Smeltzer
When you say that you are getting errors, are they compilat...

8/24/2010
Antoine
Thanks for your quick reply! I renamed the class to Inte...

8/23/2010
Mark Smeltzer
I have not had any problems with that. Could you post your ...

8/23/2010
Antoine
Hi, Thanks a lot for this, I found it interesting and tr...

7/31/2010
Blog - Attaching Behaviors from the Expression Blend SDK using Styles (Part 2)
In the previous post in this series, I introduce the reader...

6/17/2010
Mark Smeltzer
Werner, The latest release, 0.9.7.4, should fix the prob...

5/31/2010
Mark Smeltzer
Werner, Are you using AnkhSVN? I have created a patch...

|< <  1   2   > >| Results 1 - 10 of 11

Syndication

RSS
This web site uses Kentico CMS, the content management system for ASP.NET developers.