How to add a Blend Behavior in a Style Setter

43,534

Solution 1

I had the same problem and I've come up with a solution. I found this question after I solved it and I see that my solution bears a lot in common with Mark's. However, this approach is a little different.

The main problem is that behaviors and triggers associate with a specific object and so you cannot use the same instance of a behavior for multiple different associated objects. When you define your behavior inline XAML enforces this one-to-one relationship. However, when you try to set a behavior in a style, the style can be re-used for all the objects it applies to and this will throw exceptions in the base behavior classes. In fact the authors went to considerable effort to prevent us from even trying to do this, knowing that it wouldn't work.

The first problem is that we cannot even construct a behavior setter value because the constructor is internal. So we need our own behavior and trigger collection classes.

The next problem is that the behavior and trigger attached properties don't have setters and so they can only be added to with in-line XAML. This problem we solve with our own attached properties that manipulate the primary behavior and trigger properties.

The third problem is that our behavior collection is only good for a single style target. This we solve by utilizing a little-used XAML feature x:Shared="False" which creates a new copy of the resource each time it is referenced.

The final problem is that behaviors and triggers are not like other style setters; we don't want to replace the old behaviors with the new behaviors because they could do wildly different things. So if we accept that once you add a behavior you cannot take it away (and that's the way behaviors currently work), we can conclude that behaviors and triggers should be additive and this can be handled by our attached properties.

Here is a sample using this approach:

<Grid>
    <Grid.Resources>
        <sys:String x:Key="stringResource1">stringResource1</sys:String>
        <local:Triggers x:Key="debugTriggers" x:Shared="False">
            <i:EventTrigger EventName="MouseLeftButtonDown">
                <local:DebugAction Message="DataContext: {0}" MessageParameter="{Binding}"/>
                <local:DebugAction Message="ElementName: {0}" MessageParameter="{Binding Text, ElementName=textBlock2}"/>
                <local:DebugAction Message="Mentor: {0}" MessageParameter="{Binding Text, RelativeSource={RelativeSource AncestorType={x:Type FrameworkElement}}}"/>
            </i:EventTrigger>
        </local:Triggers>
        <Style x:Key="debugBehavior" TargetType="FrameworkElement">
            <Setter Property="local:SupplementaryInteraction.Triggers" Value="{StaticResource debugTriggers}"/>
        </Style>
    </Grid.Resources>
    <StackPanel DataContext="{StaticResource stringResource1}">
        <TextBlock Name="textBlock1" Text="textBlock1" Style="{StaticResource debugBehavior}"/>
        <TextBlock Name="textBlock2" Text="textBlock2" Style="{StaticResource debugBehavior}"/>
        <TextBlock Name="textBlock3" Text="textBlock3" Style="{StaticResource debugBehavior}"/>
    </StackPanel>
</Grid>

The example uses triggers but behaviors work the same way. In the example, we show:

  • the style can be applied to multiple text blocks
  • several types of data binding all work correctly
  • a debug action that generates text in the output window

Here's an example behavior, our DebugAction. More properly it is an action but through the abuse of language we call behaviors, triggers and actions "behaviors".

public class DebugAction : TriggerAction<DependencyObject>
{
    public string Message
    {
        get { return (string)GetValue(MessageProperty); }
        set { SetValue(MessageProperty, value); }
    }

    public static readonly DependencyProperty MessageProperty =
        DependencyProperty.Register("Message", typeof(string), typeof(DebugAction), new UIPropertyMetadata(""));

    public object MessageParameter
    {
        get { return (object)GetValue(MessageParameterProperty); }
        set { SetValue(MessageParameterProperty, value); }
    }

    public static readonly DependencyProperty MessageParameterProperty =
        DependencyProperty.Register("MessageParameter", typeof(object), typeof(DebugAction), new UIPropertyMetadata(null));

    protected override void Invoke(object parameter)
    {
        Debug.WriteLine(Message, MessageParameter, AssociatedObject, parameter);
    }
}

Finally, our collections and attached properties to make this all work. By analogy with Interaction.Behaviors, the property you target is called SupplementaryInteraction.Behaviors because by setting this property, you will add behaviors to Interaction.Behaviors and likewise for triggers.

public class Behaviors : List<Behavior>
{
}

public class Triggers : List<TriggerBase>
{
}

public static class SupplementaryInteraction
{
    public static Behaviors GetBehaviors(DependencyObject obj)
    {
        return (Behaviors)obj.GetValue(BehaviorsProperty);
    }

    public static void SetBehaviors(DependencyObject obj, Behaviors value)
    {
        obj.SetValue(BehaviorsProperty, value);
    }

    public static readonly DependencyProperty BehaviorsProperty =
        DependencyProperty.RegisterAttached("Behaviors", typeof(Behaviors), typeof(SupplementaryInteraction), new UIPropertyMetadata(null, OnPropertyBehaviorsChanged));

    private static void OnPropertyBehaviorsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var behaviors = Interaction.GetBehaviors(d);
        foreach (var behavior in e.NewValue as Behaviors) behaviors.Add(behavior);
    }

    public static Triggers GetTriggers(DependencyObject obj)
    {
        return (Triggers)obj.GetValue(TriggersProperty);
    }

    public static void SetTriggers(DependencyObject obj, Triggers value)
    {
        obj.SetValue(TriggersProperty, value);
    }

    public static readonly DependencyProperty TriggersProperty =
        DependencyProperty.RegisterAttached("Triggers", typeof(Triggers), typeof(SupplementaryInteraction), new UIPropertyMetadata(null, OnPropertyTriggersChanged));

    private static void OnPropertyTriggersChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var triggers = Interaction.GetTriggers(d);
        foreach (var trigger in e.NewValue as Triggers) triggers.Add(trigger);
    }
}

and there you have it, fully-functional behaviors and triggers applied through styles.

Solution 2

Summing answers and this great article Blend Behaviors in Styles, I came to this generic short and convinient solution:

I made generic class, which could be inherited by any behavior.

public class AttachableForStyleBehavior<TComponent, TBehavior> : Behavior<TComponent>
        where TComponent : System.Windows.DependencyObject
        where TBehavior : AttachableForStyleBehavior<TComponent, TBehavior> , new ()
    {
        public static DependencyProperty IsEnabledForStyleProperty =
            DependencyProperty.RegisterAttached("IsEnabledForStyle", typeof(bool),
            typeof(AttachableForStyleBehavior<TComponent, TBehavior>), new FrameworkPropertyMetadata(false, OnIsEnabledForStyleChanged)); 

        public bool IsEnabledForStyle
        {
            get { return (bool)GetValue(IsEnabledForStyleProperty); }
            set { SetValue(IsEnabledForStyleProperty, value); }
        }

        private static void OnIsEnabledForStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            UIElement uie = d as UIElement;

            if (uie != null)
            {
                var behColl = Interaction.GetBehaviors(uie);
                var existingBehavior = behColl.FirstOrDefault(b => b.GetType() ==
                      typeof(TBehavior)) as TBehavior;

                if ((bool)e.NewValue == false && existingBehavior != null)
                {
                    behColl.Remove(existingBehavior);
                }

                else if ((bool)e.NewValue == true && existingBehavior == null)
                {
                    behColl.Add(new TBehavior());
                }    
            }
        }
    }

So you could simply reuse it with lot of components like this:

public class ComboBoxBehaviour : AttachableForStyleBehavior<ComboBox, ComboBoxBehaviour>
    { ... }

And in XAML enough to declare:

 <Style TargetType="ComboBox">
            <Setter Property="behaviours:ComboBoxBehaviour.IsEnabledForStyle" Value="True"/>

So basicly the AttachableForStyleBehavior class made xaml things, registering the instance of behavior for each component in style. For more details, please see the link.

Solution 3

1.Create Attached Property

public static class DataGridCellAttachedProperties
{
    //Register new attached property
    public static readonly DependencyProperty IsSingleClickEditModeProperty =
        DependencyProperty.RegisterAttached("IsSingleClickEditMode", typeof(bool), typeof(DataGridCellAttachedProperties), new UIPropertyMetadata(false, OnPropertyIsSingleClickEditModeChanged));

    private static void OnPropertyIsSingleClickEditModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var dataGridCell = d as DataGridCell;
        if (dataGridCell == null)
            return;

        var isSingleEditMode = GetIsSingleClickEditMode(d);
        var behaviors =  Interaction.GetBehaviors(d);
        var singleClickEditBehavior = behaviors.SingleOrDefault(x => x is SingleClickEditDataGridCellBehavior);

        if (singleClickEditBehavior != null && !isSingleEditMode)
            behaviors.Remove(singleClickEditBehavior);
        else if (singleClickEditBehavior == null && isSingleEditMode)
        {
            singleClickEditBehavior = new SingleClickEditDataGridCellBehavior();
            behaviors.Add(singleClickEditBehavior);
        }
    }

    public static bool GetIsSingleClickEditMode(DependencyObject obj)
    {
        return (bool) obj.GetValue(IsSingleClickEditModeProperty);
    }

    public static void SetIsSingleClickEditMode(DependencyObject obj, bool value)
    {
        obj.SetValue(IsSingleClickEditModeProperty, value);
    }
}

2.Create a Behavior

public class SingleClickEditDataGridCellBehavior:Behavior<DataGridCell>
        {
            protected override void OnAttached()
            {
                base.OnAttached();
                AssociatedObject.PreviewMouseLeftButtonDown += DataGridCellPreviewMouseLeftButtonDown;
            }

            protected override void OnDetaching()
            {
                base.OnDetaching();
                AssociatedObject.PreviewMouseLeftButtonDown += DataGridCellPreviewMouseLeftButtonDown;
            }

            void DataGridCellPreviewMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
            {
                 DataGridCell cell = sender as DataGridCell;
                if (cell != null && !cell.IsEditing && !cell.IsReadOnly)
                {
                    if (!cell.IsFocused)
                    {
                        cell.Focus();
                    }
                    DataGrid dataGrid = LogicalTreeWalker.FindParentOfType<DataGrid>(cell); //FindVisualParent<DataGrid>(cell);
                    if (dataGrid != null)
                    {
                        if (dataGrid.SelectionUnit != DataGridSelectionUnit.FullRow)
                        {
                            if (!cell.IsSelected)
                                cell.IsSelected = true;
                        }
                        else
                        {
                            DataGridRow row =  LogicalTreeWalker.FindParentOfType<DataGridRow>(cell); //FindVisualParent<DataGridRow>(cell);
                            if (row != null && !row.IsSelected)
                            {
                                row.IsSelected = true;
                            }
                        }
                    }
                }
            }    
        }

3.Create a Style and set the attached property

        <Style TargetType="{x:Type DataGridCell}">
            <Setter Property="Behaviors:DataGridCellAttachedProperties.IsSingleClickEditMode" Value="True"/>
        </Style>

Solution 4

I have another idea, to avoid the creation of a attached property for every behavior:

  1. Behavior creator interface:

    public interface IBehaviorCreator
    {
        Behavior Create();
    }
    
  2. Small helper collection:

    public class BehaviorCreatorCollection : Collection<IBehaviorCreator> { }
    
  3. Helper class which attaches the behavior:

    public static class BehaviorInStyleAttacher
    {
        #region Attached Properties
    
        public static readonly DependencyProperty BehaviorsProperty =
            DependencyProperty.RegisterAttached(
                "Behaviors",
                typeof(BehaviorCreatorCollection),
                typeof(BehaviorInStyleAttacher),
                new UIPropertyMetadata(null, OnBehaviorsChanged));
    
        #endregion
    
        #region Getter and Setter of Attached Properties
    
        public static BehaviorCreatorCollection GetBehaviors(TreeView treeView)
        {
            return (BehaviorCreatorCollection)treeView.GetValue(BehaviorsProperty);
        }
    
        public static void SetBehaviors(
            TreeView treeView, BehaviorCreatorCollection value)
        {
            treeView.SetValue(BehaviorsProperty, value);
        }
    
        #endregion
    
        #region on property changed methods
    
        private static void OnBehaviorsChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
        {
            if (e.NewValue is BehaviorCreatorCollection == false)
                return;
    
            BehaviorCreatorCollection newBehaviorCollection = e.NewValue as BehaviorCreatorCollection;
    
            BehaviorCollection behaviorCollection = Interaction.GetBehaviors(depObj);
            behaviorCollection.Clear();
            foreach (IBehaviorCreator behavior in newBehaviorCollection)
            {
                behaviorCollection.Add(behavior.Create());
            }
        }
    
        #endregion
    }
    
  4. Now your behavior, which implements IBehaviorCreator:

    public class SingleClickEditDataGridCellBehavior:Behavior<DataGridCell>, IBehaviorCreator
    {
        //some code ...
    
        public Behavior Create()
        {
            // here of course you can also set properties if required
            return new SingleClickEditDataGridCellBehavior();
        }
    }
    
  5. And now use it in xaml:

    <Style TargetType="{x:Type DataGridCell}">
      <Setter Property="helper:BehaviorInStyleAttacher.Behaviors" >
        <Setter.Value>
          <helper:BehaviorCreatorCollection>
            <behaviors:SingleClickEditDataGridCellBehavior/>
          </helper:BehaviorCreatorCollection>
        </Setter.Value>
      </Setter>
    </Style>
    

Solution 5

I couldn't find the original article but I was able to recreate the effect.

#region Attached Properties Boilerplate

    public static readonly DependencyProperty IsActiveProperty = DependencyProperty.RegisterAttached("IsActive", typeof(bool), typeof(ScrollIntoViewBehavior), new PropertyMetadata(false, OnIsActiveChanged));

    public static bool GetIsActive(FrameworkElement control)
    {
        return (bool)control.GetValue(IsActiveProperty);
    }

    public static void SetIsActive(
      FrameworkElement control, bool value)
    {
        control.SetValue(IsActiveProperty, value);
    }

    private static void OnIsActiveChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var behaviors = Interaction.GetBehaviors(d);
        var newValue = (bool)e.NewValue;

        if (newValue)
        {
            //add the behavior if we don't already have one
            if (!behaviors.OfType<ScrollIntoViewBehavior>().Any())
            {
                behaviors.Add(new ScrollIntoViewBehavior());
            }
        }
        else
        {
            //remove any instance of the behavior. (There should only be one, but just in case.)
            foreach (var item in behaviors.ToArray())
            {
                if (item is ScrollIntoViewBehavior)
                    behaviors.Remove(item);
            }
        }
    }


    #endregion
<Style TargetType="Button">
    <Setter Property="Blah:ScrollIntoViewBehavior.IsActive" Value="True" />
</Style>
Share:
43,534
Jobi Joy
Author by

Jobi Joy

VP of Engineering www.identitymine.com We build experiences on all platforms #Windows #Xbox #Windows10 #HTML #WPF #XAML #WinUI #xamarin (iOS and Android)

Updated on July 08, 2022

Comments

  • Jobi Joy
    Jobi Joy almost 2 years

    I have crated a Blend behavior for Button. How can I set that to all of my Buttons in the app.

    <Button ...>
      <i:Interaction.Behaviors>
        <local:MyBehavior />
      </i:Interaction.Behaviors>
    </Button>
    

    However, when I try:

    <Style>
      <Setter Property="i:Interaction.Behaviors">
        <Setter.Value>
          <local:MyBehavior />
        </Setter.Value>
      </Setter>
    </Style>
    

    I get the error

    The property "Behaviors" does not have an accessible setter.

  • MetalMikester
    MetalMikester over 12 years
    Great stuff, this works beautifully. I noticed that if you put the style, for example, in the UserControl resources, then e.NewValue may be null at first (might depend on the control used - I'm using this on the XamDataTreeNodeControl in an Infragistics XamDataTree). So I added a little sanity check in OnPropertyTriggersChanged: if (e.NewValue != null)
  • Stephen Drew
    Stephen Drew almost 12 years
    This isn't a Blend behavior, it's a "behavior" through a simple attached property.
  • Stephen Drew
    Stephen Drew almost 12 years
    Having to write this for each behavior is a bit of a PITA though.
  • Jason Frank
    Jason Frank about 11 years
    Has anybody had a problem with this approach when applying the Setter in an implicit Style? I've gotten it to work fine with a non-implicit style (one with a Key), but I get a cyclic reference exception if its in an implicit style.
  • Thomas Levesque
    Thomas Levesque almost 11 years
    Nice solution, but unfortunately it doesn't work in WinRT, because x:Shared doesn't exist on this platform...
  • Golvellius
    Golvellius almost 11 years
    I can confirm that this solution works. Thank you very much for sharing it. I have not yet tried it with an implicit style, though.
  • Philipp Michalski
    Philipp Michalski almost 9 years
    Works like a charm! With my Scrollingbehavior combined i got rid of Inner RowDetailsTemplate-Datagrids not scrolling the parent Datagrids.
  • Roma Borodov
    Roma Borodov over 8 years
    Glad to help, enjoy=)
  • Eric Ouellet
    Eric Ouellet over 8 years
    @Jason Frank, I think I use it as a non implicit and it works fine without cyclic reference exception. Do you use BasedOn which could create the cyclic reference? ... This is how I use it: <Style TargetType="Line"> <Setter Property="behavior:SupplementaryInteraction.Behaviors" Value="{StaticResource LineBehaviors}"/>
  • Jason Frank
    Jason Frank over 8 years
    @EricOuellet Thanks for replying. I'm not able to answer your follow-up question as I am not working in this tech right now. But just in case others follow-up, I mentioned in my comment that I did get it to work with non-implicit style, and that I only got the cyclic reference exception if it is in an implicit style. So my question was specifically for the implicit style case.
  • Eric Ouellet
    Eric Ouellet over 8 years
    @Jason Frank, Thanks, Just as references for others... I made it works in both cases: Implicit and explicit. In fact I ask a question where I would have put all of my code to help others but somebody estimate that my question was a duplicate. I cannot answer my own question giving everything I have found. I think I discover pretty nice things. :-( ...I hope it does not happen too often because that behavior deprive other users from useful information.
  • JobaDiniz
    JobaDiniz over 7 years
    what about data binding with dependency properties in the Behavior?
  • Igor Meszaros
    Igor Meszaros over 7 years
    When I try to access the DependencyProperty from the style it says IsSingleClickEditMode is not recognized or no accessible?
  • Igor Meszaros
    Igor Meszaros over 7 years
    Sorry my bad.. as soon as I commented I realised GetIsSingleClickEditMode should match the string you pass in to the DependencyProperty.RegisterAttached
  • Kryptos
    Kryptos almost 7 years
    How does it answer the OP? The trigger is not added through a style in your answer.
  • Roma Borodov
    Roma Borodov about 6 years
    I don't know how to contact user or decline edit with negative feedback personally. So dear @Der_Meister and other editors, please read code carefully before you trying to edit it. It could affect other users and my reputation too. In this case, by removing IsEnabledForStyle property and insistently replacing it with static methods, you destroying the possibility of binding to it in xaml, which is the main point of this question. So looks like you didn't read code till the end. Saddly I can't reject your editing with great minus, so just please be careful in future.
  • Der_Meister
    Der_Meister about 6 years
    @RomaBorodov, everything works in XAML. It's a correct way to define attached property (which is different from dependency property). See documentation: docs.microsoft.com/en-us/dotnet/framework/wpf/advanced/…
  • Tim Sylvester
    Tim Sylvester over 5 years
    I got an exception An instance of a Behavior cannot be attached to more than one object at a time despite x:Shared="False". Turns out the shared property isn't supported in "nested" dictionaries and ResourceDictionary.MergedDictionaries apparently counts. However, I got it to work anyway when the resource was defined in a separate XAML file consisting of just a ResourceDictionary. Hope that helps someone...
  • Luishg
    Luishg almost 5 years
    Is there any way I can access the property directly in the control xaml? Example: <TextBox Text="asd" behaviours:TextBoxBehavior.IsEnabledForStyle="False">
  • Keith
    Keith almost 5 years
    This looks great, except I have a behavior which needs its own argument passed in (in my case, a Control as a property called PlacementTarget). Is this extendable to allow other things to be bound?
  • BalintPogatsa
    BalintPogatsa over 4 years
    OnDetaching adds another event handler, this should be fixed (cannot modify a single character when editing a post...)
  • stoj
    stoj about 2 years
    Further to @TimSylvester's comment, the x:Shared=False behavior is documented here .. docs.microsoft.com/en-us/previous-versions/dotnet/…. However, it can also be defined in the same file as well, but it has to be at the 'top level', i.e. not within another control's resources section if that control is already part of a resource dictionary.