如何在样式设置器中添加混合行为

我为 Button 创建了一个 Blend 行为。我怎样才能在应用程序中设置所有的按钮。

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

然而,当我尝试:

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

我知道错了

属性“ Behavior”没有可访问的 setter。

45302 次浏览

Behavior code expects a Visual, so we can add it only on a visual. So the only option I could see is to add to one of the element inside the ControlTemplate so as to get the behavior added to the Style and affect on all the instance of a particular control.

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.

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>

The article Introduction to Attached Behaviors in WPF implements an attached behavior using Style only, and may also be related or helpful.

The technique in the "Introduction to Attached Behaviors" article avoids the Interactivity tags altogether, using on Style. I don't know if this is just because it is a more dated technique, or, if that still confers some benefits where one should prefer it in some scenarios.

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>

I like the approach shown by the answers by Roman Dvoskin and Jonathan Allen in this thread. When I was first learning that technique though, I benefited from this blog post which provides more explanation about the technique. And to see everything in context, here is the entire source code for the class that the author talks about in his blog post.

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>
    

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.

Declare individual behavior/trigger as Resources :

<Window.Resources>


<i:EventTrigger x:Key="ET1" EventName="Click">
<ei:ChangePropertyAction PropertyName="Background">
<ei:ChangePropertyAction.Value>
<SolidColorBrush Color="#FFDAD32D"/>
</ei:ChangePropertyAction.Value>
</ei:ChangePropertyAction>
</i:EventTrigger>


</Window.Resources>

Insert them in the collection :

<Button x:Name="Btn1" Content="Button">


<i:Interaction.Triggers>
<StaticResourceExtension ResourceKey="ET1"/>
</i:Interaction.Triggers>


</Button>

Based on this answer I made a simpler solution, with just one class needed and there is no need to implement something else in your behaviors.

public static class BehaviorInStyleAttacher
{
#region Attached Properties


public static readonly DependencyProperty BehaviorsProperty =
DependencyProperty.RegisterAttached(
"Behaviors",
typeof(IEnumerable),
typeof(BehaviorInStyleAttacher),
new UIPropertyMetadata(null, OnBehaviorsChanged));


#endregion


#region Getter and Setter of Attached Properties


public static IEnumerable GetBehaviors(DependencyObject dependencyObject)
{
return (IEnumerable)dependencyObject.GetValue(BehaviorsProperty);
}


public static void SetBehaviors(
DependencyObject dependencyObject, IEnumerable value)
{
dependencyObject.SetValue(BehaviorsProperty, value);
}


#endregion


#region on property changed methods


private static void OnBehaviorsChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue is IEnumerable == false)
return;


var newBehaviorCollection = e.NewValue as IEnumerable;


BehaviorCollection behaviorCollection = Interaction.GetBehaviors(depObj);
behaviorCollection.Clear();
foreach (Behavior behavior in newBehaviorCollection)
{
// you need to make a copy of behavior in order to attach it to several controls
var copy = behavior.Clone() as Behavior;
behaviorCollection.Add(copy);
}
}


#endregion
}

and the sample usage is

<Style TargetType="telerik:RadComboBox" x:Key="MultiPeriodSelectableRadComboBox">
<Setter Property="AllowMultipleSelection" Value="True" />
<Setter Property="behaviors:BehaviorInStyleAttacher.Behaviors">
<Setter.Value>
<collections:ArrayList>
<behaviors:MultiSelectRadComboBoxBehavior
SelectedItems="{Binding SelectedPeriods}"
DelayUpdateUntilDropDownClosed="True"
SortSelection="True"
ReverseSort="True" />
</collections:ArrayList>
</Setter.Value>
</Setter>
</Style>

Don't forget to add this xmlns to use ArrayList:

xmlns:collections="clr-namespace:System.Collections;assembly=mscorlib"