In the previous posts in this series (Attaching Behaviors from the Expression Blend SDK using Styles and Attaching Behaviors from the Expression Blend SDK using Styles (Part 2)), I introduced the reader to a new attached property that gives the ability to attach a collection of behaviors to an item through a style.
I received feedback from two developers regarding a bug in the code that was in Part 2, and as a result I wanted to post a corrected version of the code.
Rather than do a walk through, here is the full, revised version of the StylizedBehaviors class:
using System.Windows;
using System.Windows.Interactivity;
#endregion
namespace LivingAgile.Common.Presentation.WPF.Behaviors
{
public class StylizedBehaviors
{
#region Fields (private)
private static readonly DependencyProperty OriginalBehaviorProperty =
DependencyProperty.RegisterAttached(
@"OriginalBehaviorInternal", typeof (Behavior), typeof (StylizedBehaviors), new UIPropertyMetadata(null));
#endregion
#region Fields (public)
public static readonly DependencyProperty BehaviorsProperty = DependencyProperty.RegisterAttached(
@"Behaviors",
typeof (StylizedBehaviorCollection),
typeof (StylizedBehaviors),
new FrameworkPropertyMetadata(null, 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 Behavior GetOriginalBehavior(DependencyObject obj)
{
return obj.GetValue(OriginalBehaviorProperty) as Behavior;
}
private static int GetIndexOf(BehaviorCollection itemBehaviors, Behavior behavior)
{
int index = -1;
Behavior orignalBehavior = GetOriginalBehavior(behavior);
for (int i = 0; i < itemBehaviors.Count; i++)
{
Behavior currentBehavior = itemBehaviors[i];
if (currentBehavior == behavior
|| currentBehavior == orignalBehavior)
{
index = i;
break;
}
Behavior currentOrignalBehavior = GetOriginalBehavior(currentBehavior);
if (currentOrignalBehavior == behavior
|| currentOrignalBehavior == orignalBehavior)
{
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)
{
int index = GetIndexOf(itemBehaviors, behavior);
if (index < 0)
{
var clone = (Behavior) behavior.Clone();
SetOriginalBehavior(clone, behavior);
itemBehaviors.Add(clone);
}
}
}
}
private static void SetOriginalBehavior(DependencyObject obj, Behavior value)
{
obj.SetValue(OriginalBehaviorProperty, value);
}
#endregion
}
}
The interested reader can refer to the comments on the previous posts to see what motivated the changes.
If you are an avid user of Resharper, you may want to give this Type Members Layout template a try to improve the formatting of your code when using the Code Cleanup command:
<?xml version="1.0" encoding="utf-8" ?>
<!--
I. Overall
I.1 Each pattern can have <Match>....</Match> element. For the given type declaration, the pattern with the match, evaluated to 'true' with the largest weight, will be used
I.2 Each pattern consists of the sequence of <Entry>...</Entry> elements. Type member declarations are distributed between entries
I.3 If pattern has RemoveAllRegions="true" attribute, then all regions will be cleared prior to reordering. Otherwise, only auto-generated regions will be cleared
I.4 The contents of each entry is sorted by given keys (First key is primary, next key is secondary, etc). Then the declarations are grouped and en-regioned by given property
II. Available match operands
Each operand may have Weight="..." attribute. This weight will be added to the match weight if the operand is evaluated to 'true'.
The default weight is 1
II.1 Boolean functions:
II.1.1 <And>....</And>
II.1.2 <Or>....</Or>
II.1.3 <Not>....</Not>
II.2 Operands
II.2.1 <Kind Is="..."/>. Kinds are: class, struct, interface, enum, delegate, type, constructor, destructor, property, indexer, method, operator, field, constant, event, member
II.2.2 <Name Is="..." [IgnoreCase="true/false"] />. The 'Is' attribute contains regular expression
II.2.3 <HasAttribute CLRName="..." [Inherit="true/false"] />. The 'CLRName' attribute contains regular expression
II.2.4 <Access Is="..."/>. The 'Is' values are: public, protected, internal, protected internal, private
II.2.5 <Static/>
II.2.6 <Abstract/>
II.2.7 <Virtual/>
II.2.8 <Override/>
II.2.9 <Sealed/>
II.2.10 <Readonly/>
II.2.11 <ImplementsInterface CLRName="..."/>. The 'CLRName' attribute contains regular expression
II.2.12 <HandlesEvent />
-->
<Patterns xmlns="urn:shemas-jetbrains-com:member-reordering-patterns">
<!--Do not reorder COM interfaces or structs with StructLayout attribute-->
<Pattern>
<Match>
<Or Weight="100">
<And>
<Kind Is="interface"/>
<HasAttribute CLRName="System.Runtime.InteropServices.InterfaceTypeAttribute"/>
</And>
<HasAttribute CLRName="System.Runtime.InteropServices.StructLayoutAttribute"/>
</Or>
</Match>
</Pattern>
<!--Special formatting of NUnit test fixture-->
<Pattern RemoveAllRegions="true">
<Match>
<And Weight="100">
<Kind Is="class"/>
<HasAttribute CLRName="NUnit.Framework.TestFixtureAttribute" Inherit="true"/>
</And>
</Match>
<!--Setup/Teardow-->
<Entry>
<Match>
<And>
<Kind Is="method"/>
<Or>
<HasAttribute CLRName="NUnit.Framework.SetUpAttribute" Inherit="true"/>
<HasAttribute CLRName="NUnit.Framework.TearDownAttribute" Inherit="true"/>
<HasAttribute CLRName="NUnit.Framework.FixtureSetUpAttribute" Inherit="true"/>
<HasAttribute CLRName="NUnit.Framework.FixtureTearDownAttribute" Inherit="true"/>
</Or>
</And>
</Match>
<Group Region="Setup/Teardown"/>
</Entry>
<!--All other members-->
<Entry/>
<!--Test methods-->
<Entry>
<Match>
<And Weight="100">
<Kind Is="method"/>
<HasAttribute CLRName="NUnit.Framework.TestAttribute" Inherit="false"/>
</And>
</Match>
<Sort>
<Name/>
</Sort>
</Entry>
</Pattern>
<!--Default pattern-->
<Pattern RemoveAllRegions="true">
<!--public delegate-->
<Entry>
<Match>
<And Weight="100">
<Access Is="public"/>
<Kind Is="delegate"/>
</And>
</Match>
<Sort>
<Access Order="public" />
<Name/>
</Sort>
<Group Region="Delegates"/>
</Entry>
<!--public enum-->
<Entry>
<Match>
<Kind Is="enum"/>
</Match>
<Sort>
<Access Order="public" />
<Name/>
</Sort>
<Group>
<Name Region="Enums"/>
</Group>
</Entry>
<!--constants-->
<Entry>
<Match>
<Kind Is="constant"/>
</Match>
<Sort>
<Kind Order="constant"/>
<Access Order="public" />
<Static/>
<Readonly/>
<Name/>
</Sort>
<Group>
<Access Region="Constants"/>
</Group>
</Entry>
<!--static fields-->
<Entry>
<Match>
<And>
<Static />
<Kind Is="field"/>
</And>
</Match>
<Sort>
<Kind Order="constant"/>
<Access Order="public" />
<Static/>
<Readonly/>
<Name/>
</Sort>
<Group>
<Access Region="Static Fields (${Access})"/>
</Group>
</Entry>
<!--fields-->
<Entry>
<Match>
<Kind Is="field"/>
</Match>
<Sort>
<Kind Order="constant"/>
<Access Order="public" />
<Static/>
<Readonly/>
<Name/>
</Sort>
<Group>
<Access Region="Fields (${Access})"/>
</Group>
</Entry>
<!-- Events-->
<Entry>
<Match>
<Kind Is="event"/>
</Match>
<Sort>
<Access Order="public" />
<Name/>
</Sort>
<Group>
<Access Region="Events (${Access})"/>
</Group>
</Entry>
<!--Constructors. Place static one first-->
<Entry>
<Match>
<Kind Is="constructor"/>
</Match>
<Sort>
<Static/>
</Sort>
<Group Region="Constructors"/>
</Entry>
<!--Deconstructor-->
<Entry>
<Match>
<Kind Is="destructor"/>
</Match>
<Sort>
<Static/>
</Sort>
<Group Region="Deconstructor"/>
</Entry>
<!--properties, indexers-->
<Entry>
<Match>
<Or>
<Kind Is="property"/>
<Kind Is="indexer"/>
</Or>
</Match>
<Sort>
<Kind Order="property"/>
<Access Order="public" />
<Name/>
</Sort>
<Group>
<Access Region="Properties (${Access})"/>
</Group>
</Entry>
<!--static properties, indexers-->
<Entry>
<Match>
<And>
<Static />
<Or>
<Kind Is="property"/>
<Kind Is="indexer"/>
</Or>
</And>
</Match>
<Sort>
<Kind Order="property"/>
<Access Order="public" />
<Name/>
</Sort>
<Group>
<Access Region="Static Properties (${Access})"/>
</Group>
</Entry>
<!--interface implementations-->
<Entry>
<Match>
<And Weight="100">
<Kind Is="event"/>
<ImplementsInterface/>
</And>
</Match>
<Sort>
<ImplementsInterface Immediate="true"/>
<Access Order="public" />
<Name/>
</Sort>
<Group>
<ImplementsInterface Immediate="true" Region="${ImplementsInterface} Events"/>
</Group>
</Entry>
<!--interface implementations-->
<Entry>
<Match>
<And Weight="100">
<Kind Is="member"/>
<ImplementsInterface/>
<Or>
<Kind Is="property"/>
<Kind Is="indexer"/>
</Or>
</And>
</Match>
<Sort>
<ImplementsInterface Immediate="true"/>
<Kind Order="property"/>
<Name/>
</Sort>
<Group>
<ImplementsInterface Immediate="true" Region="${ImplementsInterface} Properties"/>
</Group>
</Entry>
<!--interface implementations-->
<Entry>
<Match>
<And Weight="100">
<Kind Is="member"/>
<ImplementsInterface/>
</And>
</Match>
<Sort>
<ImplementsInterface Immediate="true"/>
<Access Order="public" />
<Kind Order="property"/>
<Name/>
</Sort>
<Group>
<ImplementsInterface Immediate="true" Region="${ImplementsInterface} Methods"/>
</Group>
</Entry>
<Entry>
<Match>
<HandlesEvent />
</Match>
<Sort>
<Access Order="public" />
<Name/>
</Sort>
<Group>
<Access Region="Event Handlers (${Access})"/>
</Group>
</Entry>
<!-- Console's Main Entry Point Static Method -->
<Entry>
<Match>
<And Weight="100">
<Kind Is="method"/>
<Static />
<Name Is="Main"/>
</And>
</Match>
<Sort>
<Access Order="public" />
<Name />
</Sort>
<Group>
<Access Region="Main Static Methods (${Access})"/>
</Group>
</Entry>
<!-- Static Methods -->
<Entry>
<Match>
<And Weight="100">
<Kind Is="method"/>
<Static />
</And>
</Match>
<Sort>
<Access Order="public" />
<Name />
</Sort>
<Group>
<Access Region="Static Methods (${Access})"/>
</Group>
</Entry>
<!-- Abstract Methods -->
<Entry>
<Match>
<And Weight="100">
<Kind Is="method"/>
<Abstract/>
</And>
</Match>
<Sort>
<Access Order="public" />
<Name/>
</Sort>
<Group Region="Abstract Methods"/>
</Entry>
<!-- Methods -->
<Entry>
<Match>
<Kind Is="method"/>
</Match>
<Sort>
<Access Order="public" />
<Name/>
</Sort>
<Group>
<Access Region="Methods (${Access})"/>
</Group>
</Entry>
<!--all other members-->
<Entry/>
<!--nested types-->
<Entry>
<Match>
<Kind Is="type"/>
</Match>
<Sort>
<Access Order="public" />
<Name/>
</Sort>
<Group>
<Name Region="Nested type: ${Name}"/>
</Group>
</Entry>
</Pattern>
</Patterns>
Enjoy!
I am proud to announce that I have released a new version of the Assembly Version Manager for Visual Studio 2010. Version 0.9.8.7 adds major new features to the build set identification capabilities. Download it now! If you like it, be sure to give it a positive rating on the Visual Studio Gallery.
I will be updating the documentation on the website to reflect the new features and changes over the coming week.
New Features
- Added new features to the build set identification process:
- Added a Platform capture group to allow capturing platform information from the assemblies' path
- Added a Default Platform setting to specify the platform that is associated with build sets by default (the default setting is simply ".NET")
- Added a setting called "Additional Search Levels" that allows the provider to correctly handle nested file hierarchies for complex component distributions such as the complex hierarchy in the log4net distribution
- Added a "Preview" panel in the provider configuration editor that allows you to select a reference in the solution and get a real-time preview of the build sets that are found using the current settings in the editor -- if you make a change, the preview will update accordingly!
Bug Fixes
- Fixed a bug that prevented the Refresh and Configuration buttons from becoming visible even after a solution was open (which was actually caused by problems with other Visual Studio plugins running on the same machine!)
UI Improvements
- Fixed a visual glitch on the styling of non-filterable column headers in the "Windows Classic" visual theme
- Updated the in-program documentation for the configuration editor
- Made the configuration editor's documentation "pop down" three times larger by default
- Made the configuration editor window twice as large by default
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 UIPropertyMetadata( Guid.Empty ) );
#endregion
#region Fields (public)
public static readonly DependencyProperty BehaviorsProperty = DependencyProperty.RegisterAttached(
@"Behaviors",
typeof ( StylizedBehaviorCollection ),
typeof ( StylizedBehaviors ),
new FrameworkPropertyMetadata( null, 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!
The Expression SDK comes with a great feature called "Behaviors" which allows the designer to attach a unit of functionality (like drag-and-drop) to a visual element. Expression comes bundled with a set of general purpose behaviors, and developers can add more to give the designers functionality that is missing.
Behaviors are awesome, and it sure would be nice to be able to define a style that can be applied to any particular button -- for example, maybe your designer wants certain buttons to "pop" or "wiggle" when the mouse hovers over them.
The best way do that would be to define a style (perhaps in a shared resource dictionary) that looks something like this:
<Style TargetType="{x:Type Button}">
<Setter Property="[some property here]">
<Setter.Value>
<MyBehaviors:PopOnHoverBehavior />
</Setter.Value>
</Setter>
</Style>
The problem is that there isn't an attached property included in the Expression SDK to put in the "[some property here]" blank... But not to fear! It is easy to create an attached property to give us the ability to make this style work.
First, we'll define a new class to contain the new attached property:
#region using System.Windows;
using System.Windows.Interactivity;
#endregion
namespace LivingAgile.Common.WPF
{
public class StylizedBehaviors
{
}
}
Next we'll add the definition for the new attached property:
#region Fields (public)
public static readonly DependencyProperty BehaviorsProperty = DependencyProperty.RegisterAttached(
@"Behaviors",
typeof ( StylizedBehaviorCollection ),
typeof ( StylizedBehaviors ),
new FrameworkPropertyMetadata( null, 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
Then we will add the "OnPropertyChanged" method:
#region Static Methods (private)
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 = itemBehaviors.IndexOf( behavior );
if ( index >= 0 )
{
itemBehaviors.RemoveAt( index );
}
}
}
if ( newBehaviors != null )
{
foreach ( var behavior in newBehaviors )
{
int index = itemBehaviors.IndexOf( behavior );
if ( index < 0 )
{
itemBehaviors.Add( behavior );
}
}
}
}
#endregion
This method does the bulk of the work. When a new collection of behaviors is assigned to the attached property, this method will remove any old behaviors that are no longer required from the visual element (though it is doubtful whether this is useful), and it will add any new behaviors to the visual element.
The last thing we need is to define the StylizedBehaviorCollection class:
#region
using System.Windows;
using System.Windows.Interactivity;
#endregion
namespace LivingAgile.Common.WPF
{
public class StylizedBehaviorCollection : FreezableCollection<Behavior>
{
#region Methods (protected)
protected override Freezable CreateInstanceCore ( )
{
return new StylizedBehaviorCollection( );
}
#endregion
}
}
Now we are ready to go! The style from above can now be written as:
<Style TargetType="{x:Type Button}">
<Setter Property="WPF:StylizedBehaviors.Behaviors">
<Setter.Value>
<WPF:StylizedBehaviorCollection>
<MyBehaviors:PopOnHoverBehavior />
</WPF:StylizedBehaviorCollection>
</Setter.Value>
</Setter>
</Style>
For this example, assuming you use the same namespaces that I did, you would need to define the WPF namespace in your XAML file as:
xmlns:WPF="clr-namespace:LivingAgile.Common.WPF"
And that is all there is to it! Enjoy.
What about Blend's Attached Triggers?
The Expression Blend SDK also includes a concept of attached triggers, and you may want to extend this design to add another attached property for assigning these triggers from styles. I leave this as an exercise for the reader. The code above can be used as a template and it would be very easy to add this feature!
Why it works...
You might be thinking that this solution won't work if the same style is applied to several visual elements since an instance of a behavior can only be attached to one visual element at a time. The answer is that anything that derives from Freezable, and stores all of its state using dependency properties, can be cloned (i.e. deep copied).
When WPF applies a style to a visual element, it inspects the setter's values to see whether they are freezables and whether they are frozen. If they are frozen, the same instance will be shared across all visual elements that implement the style, which is fine because frozen means it cannot change or vary. This really only applies to things like visual brushes that don't have any internal state that will vary based on the inheritance context, etc.
When WPF sees that the setter's value derives from Freezable and isn't frozen, it clones the instance defined in the style for each visual element that implements the style and assigns the cloned values to the visual elements. That is why it is critical that the collection class derives from FreezableCollection<>. If the collection class was just an ObservableCollection<>, we would not get this cloning behavior and the solution would break.