如何在样式设置器中添加混合行为(Blend Behavior)

98

我已为按钮创建了一个混合行为。如何将其设置为应用程序中的所有按钮。

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

但是,当我尝试:

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

我遇到了错误

属性"Behaviors"没有可访问的setter。

10个回答

83

我曾经遇到同样的问题并找到了解决办法。在我解决问题之后,我发现了这个问题,并且我看到我的解决方案与Mark的解决方案有很多相似之处。不过,这种方法有些不同。

主要问题是行为和触发器与特定对象相关联,因此您不能将相同实例的行为用于多个不同的相关对象。当您在inline XAML中定义行为时,XAML强制执行此一对一关系。但是,当您尝试在样式中设置行为时,该样式可以重复使用于其应用的所有对象,这将在基本行为类中引发异常。事实上,作者花了很多力气防止我们甚至尝试这样做,因为他们知道这样做是行不通的。

第一个问题是我们甚至无法构造行为设置器值,因为构造函数是内部的。因此,我们需要自己的行为和触发器集合类。

下一个问题是行为和触发器附加属性没有setter,因此它们只能在内联XAML中添加。我们通过自己的附加属性来解决此问题,以操纵主要的行为和触发器属性。

第三个问题是我们的行为集合仅适用于单个样式目标。我们通过利用一个不常用的XAML特性来解决这个问题,它在每次引用时都创建一个新的资源副本。

最后一个问题是行为和触发器不像其他样式设置器;我们不想用新行为替换旧行为,因为它们可能做完全不同的事情。因此,如果我们接受一旦添加了行为就不能将其删除(这就是行为当前的工作方式),我们可以得出行为和触发器应该是可添加的结论,而这可以通过我们的附加属性来处理。

以下是使用此方法的示例:

<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>

这个示例使用触发器,但是行为的工作方式相同。在示例中,我们展示了:

  • 样式可以应用于多个文本块
  • 多种类型的数据绑定都可以正常工作
  • 可以在输出窗口生成文本的调试操作

下面是一个行为示例,我们的DebugAction。更准确地说,它是一个动作,但是通过滥用语言,我们将行为、触发器和动作称为"行为"。

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);
    }
}
最后,我们需要使用集合和附加属性来使所有这些工作。类比于 Interaction.Behaviors,您要定位的属性称为 SupplementaryInteraction.Behaviors,因为通过设置此属性,您将向 Interaction.Behaviors 添加行为,并对触发器进行同样的操作。
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);
    }
}

这就是全部,通过样式应用的完全功能行为和触发器。


1
很好的解决方案,但不幸的是它在WinRT上不起作用,因为这个平台上不存在x:Shared... - Thomas Levesque
2
我可以确认这个解决方案有效。非常感谢您的分享。不过,我还没有尝试用隐式样式来实现它。 - Golvellius
@Jason Frank,我认为我将其用作非隐式,可以正常工作而不会出现循环引用异常。您是否使用了BasedOn,这可能会创建循环引用?... 这是我的用法:<Style TargetType="Line"><Setter Property="behavior:SupplementaryInteraction.Behaviors" Value="{StaticResource LineBehaviors}"/> - Eric Ouellet
2
@Jason Frank,谢谢。只是为了其他人的参考...我让它在隐式和显式两种情况下都能工作。实际上,我问了一个问题,本来想把我找到的所有代码放在那里帮助其他人,但有人认为我的问题是重复的。我不能回答自己的问题并提供我发现的一切。我觉得我发现了很好的东西。 :-( ...我希望这种情况不会经常发生,因为这种行为会剥夺其他用户有用的信息。 - Eric Ouellet
1
尽管使用了 x:Shared="False",但我仍然遇到了异常 An instance of a Behavior cannot be attached to more than one object at a time。原来在“嵌套”的字典中不支持共享属性,而且 ResourceDictionary.MergedDictionaries 也算在内。然而,当资源定义在一个单独的 XAML 文件中,该文件只包含一个 ResourceDictionary 时,我还是让它正常工作了。希望这能帮助到某些人... - Tim Sylvester
显示剩余4条评论

32
通过总结答案和这篇很棒的文章混合样式中的行为,我得出了这个通用、简洁且方便的解决方案:
我创建了一个通用类,可以被任何行为继承。
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());
                }    
            }
        }
    }

所以你可以简单地重复使用它,像这样有很多组件:
public class ComboBoxBehaviour : AttachableForStyleBehavior<ComboBox, ComboBoxBehaviour>
    { ... }

在XAML中只需声明:

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

基本上,AttachableForStyleBehavior类是用来处理XAML的事情的,它为每个样式中的组件注册了行为实例。更多详细信息,请参阅链接。

运行得非常好!通过我的滚动行为组合,我摆脱了内部RowDetailsTemplate-Datagrids无法滚动父Datagrids的问题。 - Philipp Michalski
很高兴能帮助你,享受吧=) - Roma Borodov
1
在行为中使用依赖属性进行数据绑定怎么样? - JobaDiniz
我不知道如何与用户联系或以负面反馈拒绝编辑。所以亲爱的@Der_Meister和其他编辑,请在尝试编辑代码之前仔细阅读它。这可能会影响其他用户和我的声誉。在这种情况下,通过删除IsEnabledForStyle属性并坚持使用静态方法进行替换,您破坏了在xaml中绑定它的可能性,这是这个问题的主要点。所以看起来你没有完全阅读代码。可悲的是我不能以极大的负面评价拒绝你的编辑,所以请在将来小心。 - Roma Borodov
1
@RomaBorodov,XAML中一切正常。这是定义附加属性的正确方式(与依赖属性不同)。请参阅文档:https://learn.microsoft.com/en-us/dotnet/framework/wpf/advanced/attached-properties-overview#custom-attached-properties - Der_Meister
显示剩余2条评论

19

1.创建附加属性

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. 创建一个行为

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.创建一个样式并设置附加属性

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

当我尝试从样式中访问DependencyProperty时,它会显示“IsSingleClickEditMode未被识别或不可访问”。 - Igor Meszaros
抱歉我的错。我一评论就意识到GetIsSingleClickEditMode应该与您传递给DependencyProperty.RegisterAttached的字符串匹配。 - Igor Meszaros
OnDetaching 添加了另一个事件处理程序,这应该被修复(在编辑帖子时不能修改单个字符...) - BalintPogatsa

11

我有一个新想法,可以避免为每个行为创建附加属性:

  1. 行为创建者接口:

public interface IBehaviorCreator
{
    Behavior Create();
}
  • 小助手合集:

    public class BehaviorCreatorCollection : Collection<IBehaviorCreator> { }
    
  • 帮助类,用于附加行为:

  • 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
    }
    
  • 现在你的行为,它实现了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();
        }
    }
    
  • 现在在XAML中使用它:

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

    5

    我找不到原始文章,但我能够重新创建这个效果。

    #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>
    

    然而,针对每个行为编写这样的代码有点麻烦。 - Stephen Drew

    5

    根据这个答案,我做了一个更简单的解决方案,只需要一个类就可以了,不需要在你的行为中实现其他东西。

    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
    }
    

    样例用法如下:

    <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>
    

    不要忘记添加这个xmlns以使用ArrayList:
    xmlns:collections="clr-namespace:System.Collections;assembly=mscorlib"
    

    不错的解决方案!对行为本身的代码影响非常小。需要注意的一点是,在调用OnAttached之后,AssociatedObject可能会变成null,我猜测这是由于克隆造成的。我通过在行为中存储一个私有属性来解决这个问题,并引用它。 - Ernie S

    0

    将个别行为/触发器声明为资源:

    <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>
    

    将它们插入集合中:
    <Button x:Name="Btn1" Content="Button">
    
            <i:Interaction.Triggers>
                 <StaticResourceExtension ResourceKey="ET1"/>
            </i:Interaction.Triggers>
    
    </Button>
    

    5
    它是如何回答原帖的?触发器并未通过你的回答中的样式添加。 - Kryptos

    0

    WPF中的附加行为简介一文仅使用样式实现了附加行为,可能也与本文相关或有帮助。

    “附加行为简介”文章中的技术完全避免了交互标记,仅使用样式。我不知道这是否只是一种更古老的技术,或者在某些情况下仍然具有一些优点,应该在某些情景下优先选择它。


    2
    这不是一个混合行为,而是通过一个简单的附加属性实现的“行为”。 - Stephen Drew

    0

    0

    行为代码期望一个可视化对象,因此我们只能将其添加到可视化对象上。所以我唯一能想到的选择是将其添加到ControlTemplate内的一个元素上,以便将行为添加到样式中并影响特定控件的所有实例。


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接