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


88

我已经为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>

我得到错误

属性“行为”没有可访问的设置器。

Answers:


76

我遇到了同样的问题,并且提出了解决方案。解决后,我发现了这个问题,并且发现我的解决方案与Mark的解决方案有很多共同点。但是,这种方法有些不同。

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

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

下一个问题是行为和触发器附加属性没有设置器,因此只能将它们添加到内联XAML中。我们使用自己的附加属性来解决此问题,该属性可操纵主要行为和触发器属性。

第三个问题是我们的行为收集仅适用于单个样式目标。我们通过利用很少使用的XAML功能x:Shared="False"来解决此问题,该功能在每次引用资源时都会创建该资源的新副本。

最终的问题是行为和触发器与其他样式设置器不同。我们不想用新的行为替换旧的行为,因为它们可能会做截然不同的事情。因此,如果我们接受一旦添加了行为就无法消除它(这就是当前行为的方式),那么我们可以得出结论,行为和触发器应该是可加的,并且可以通过我们附加的属性来处理。

这是使用此方法的示例:

<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);
    }
}

在那里,您可以通过样式应用全功能的行为和触发器。


很棒的东西,这很漂亮。我注意到,例如,如果将样式放在UserControl资源中,则e.NewValue可能首先为null(可能取决于所使用的控件-我在Infragistics XamDataTree中的XamDataTreeNodeControl上使用了此控件)。因此,我在OnPropertyTriggersChanged中添加了一些完整性检查:if(e.NewValue!= null)
MetalMikester 2011年

隐式样式应用Setter时,有人对这种方法有疑问吗?我已经知道它可以与非隐式样式(带有键的样式)一起正常工作,但是如果它是隐式样式,则会出现循环引用异常。
詹森·弗兰克

1
不错的解决方案,但不幸的是,它在WinRT中不起作用,因为x:Shared在该平台上不存在...
Thomas Levesque

1
我可以确认此解决方案有效。非常感谢您的分享。不过,我尚未尝试使用隐式样式。
Golvellius 2013年

2
@Jason Frank,谢谢,就像为其他人提供的参考...我使它在两种情况下都可以使用:隐式和显式。实际上,我问一个问题,我会将所有代码放在什么地方来帮助其他人,但有人估计我的问题是重复的。我无法给出我所发现的一切来回答自己的问题。我想我发现了很多不错的东西。:-( ...我希望这不会太频繁,因为这种行为剥夺其他用户有用的信息出现。
埃里克·韦莱

27

总结一下答案,并撰写这篇很棒的文章《风格中的行为混合》,我来到了这个简短而又方便的通用解决方案:

我做了一个通用类,任何行为都可以继承它。

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东西,为样式中的每个组件注册了行为实例。有关更多详细信息,请参见链接。


奇迹般有效!结合我的Scrollingbehavior,我摆脱了内部RowDetailsTemplate-Datagrids而不滚动父Datagrids的问题。
Philipp Michalski

很高兴为您提供帮助,享受=)
罗玛·波罗多夫(Rom Borodov

1
行为中具有依赖项属性的数据绑定又如何呢?
JobaDiniz '16

我不知道如何与用户联系或拒绝通过负面反馈进行个人编辑。因此,亲爱的@Der_Meister和其他编辑器,请在尝试编辑之前仔细阅读代码。它可能会影响其他用户以及我的声誉。在这种情况下,通过删除IsEnabledForStyle属性并坚持用静态方法替换它,可以消除在xaml中绑定到它的可能性,这是此问题的重点。看来您直到最后都没有阅读代码。遗憾的是,我不能拒绝您的编辑,因此请在以后小心。
罗玛·波罗多夫

1
@RomaBorodov,一切都在XAML中工作。这是定义附加属性(与依赖属性不同)的正确方法。请参阅文档:docs.microsoft.com/zh-cn/dotnet/framework/wpf/advanced/…–
Der_Meister

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无法识别或无法访问?
伊戈尔·梅萨罗斯

不好意思,抱歉。我一经评论,我就意识到GetIsSingleClickEditMode应该与您传递给DependencyProperty.RegisterAttached的字符串匹配
Igor Meszaros

OnDetaching添加了另一个事件处理程序,该事件处理程序应该已修复(在编辑帖子时无法修改单个字符...)
BalintPogatsa

11

我有另一个想法,避免为每种行为创建附加属性:

  1. 行为创建者界面:

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

    public class BehaviorCreatorCollection : Collection<IBehaviorCreator> { }
    
  3. 附加行为的Helper类:

    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. 现在,您的行为将实现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. 现在在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>

不过,必须为每种行为编写此代码有点像PITA。
斯蒂芬·德鲁

0

行为代码需要视觉效果,因此我们只能在视觉效果上添加它。因此,我唯一能看到的选择是添加到ControlTemplate内部的元素之一,以便将行为添加到Style中并影响特定控件的所有实例。


0

文章在WPF介绍附加行为实现了只用样式附加的行为,也可以与或有帮助。

“附加行为简介”文章中的技术完全避免使用Interstyle标签上的交互标签。我不知道这是否仅是因为它是一种过时的技术,还是在某些情况下仍可在某些情况下获得某些好处。


2
这不是Blend行为,而是通过简单的附加属性的“行为”。
斯蒂芬·德鲁


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>

4
它如何回答OP?触发器不是通过答案中的样式添加的。
Kryptos

0

基于答案,我提出了一个更简单的解决方案,只需要一个类,而无需在您的行为中实现其他功能。

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"
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.