Como adicionar um comportamento de mesclagem em um configurador de estilo

88

Criei um comportamento Blend para Button. Como posso definir isso para todos os meus botões no aplicativo.

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

No entanto, quando tento:

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

Eu entendi o erro

A propriedade "Behaviors" não possui configurador acessível.

Jobi Joy
fonte

Respostas:

76

Tive o mesmo problema e encontrei uma solução. Descobri essa questão depois de resolvê-la e vejo que minha solução tem muito em comum com a de Mark. No entanto, essa abordagem é um pouco diferente.

O principal problema é que comportamentos e gatilhos se associam a um objeto específico e, portanto, você não pode usar a mesma instância de um comportamento para vários objetos associados diferentes. Quando você define seu comportamento em linha, o XAML impõe esse relacionamento um a um. No entanto, quando você tenta definir um comportamento em um estilo, o estilo pode ser reutilizado para todos os objetos aos quais se aplica e isso lançará exceções nas classes de comportamento base. Na verdade, os autores fizeram um esforço considerável para nos impedir de tentar fazer isso, sabendo que não funcionaria.

O primeiro problema é que não podemos nem construir um valor configurador de comportamento porque o construtor é interno. Portanto, precisamos de nosso próprio comportamento e classes de coleção de gatilho.

O próximo problema é que o comportamento e as propriedades anexadas ao gatilho não têm configuradores e, portanto, só podem ser adicionados com XAML in-line. Resolvemos esse problema com nossas próprias propriedades anexadas que manipulam o comportamento primário e as propriedades do gatilho.

O terceiro problema é que nossa coleção de comportamento só é boa para um único destino de estilo. Isso nós resolvemos utilizando um recurso XAML pouco usado x:Shared="False"que cria uma nova cópia do recurso cada vez que ele é referenciado.

O problema final é que os comportamentos e gatilhos não são como outros definidores de estilo; não queremos substituir os antigos comportamentos pelos novos, porque eles poderiam fazer coisas totalmente diferentes. Portanto, se aceitarmos que, depois de adicionar um comportamento, você não pode removê-lo (e é assim que os comportamentos funcionam atualmente), podemos concluir que os comportamentos e os gatilhos devem ser aditivos e isso pode ser tratado por nossas propriedades anexadas.

Aqui está um exemplo usando esta abordagem:

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

O exemplo usa gatilhos, mas os comportamentos funcionam da mesma maneira. No exemplo, mostramos:

  • o estilo pode ser aplicado a vários blocos de texto
  • vários tipos de vinculação de dados funcionam corretamente
  • uma ação de depuração que gera texto na janela de saída

Aqui está um exemplo de comportamento, nosso DebugAction. Mais propriamente, é uma ação, mas através do abuso da linguagem chamamos comportamentos, gatilhos e ações de "comportamentos".

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

Finalmente, nossas coleções e propriedades anexadas para fazer tudo isso funcionar. Por analogia com Interaction.Behaviors, a propriedade de destino é chamada SupplementaryInteraction.Behaviorsporque, ao definir essa propriedade, você adicionará comportamentos a Interaction.Behaviorse da mesma forma para gatilhos.

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

e aí está, comportamentos totalmente funcionais e gatilhos aplicados por meio de estilos.

Rick Sladkey
fonte
Grande coisa, isso funciona lindamente. Percebi que se você colocar o estilo, por exemplo, nos recursos do UserControl, então e.NewValue pode ser nulo no início (pode depender do controle usado - estou usando isso no XamDataTreeNodeControl em um Infragistics XamDataTree). Então, adicionei uma pequena verificação de integridade em OnPropertyTriggersChanged: if (e.NewValue! =
Null
Alguém teve problemas com essa abordagem ao aplicar o Setter em um estilo implícito ? Consegui funcionar bem com um estilo não implícito (um com uma chave), mas recebo uma exceção de referência cíclica se for um estilo implícito.
Jason Frank,
1
Boa solução, mas infelizmente não funciona no WinRT, porque x: Shared não existe nesta plataforma ...
Thomas Levesque
1
Posso confirmar que essa solução funciona. Muito obrigado por compartilhar isso. Ainda não tentei com um estilo implícito, no entanto.
Golvellius
2
@Jason Frank, Obrigado, Assim como referências para outros ... Fiz funcionar em ambos os casos: Implícito e explícito. Na verdade, faço uma pergunta onde eu colocaria todo o meu código para ajudar os outros, mas alguém estima que minha pergunta era uma duplicata. Não posso responder à minha própria pergunta dando tudo o que encontrei. Acho que descobri coisas muito boas. :-( ... Espero que não aconteça com muita frequência, porque esse comportamento priva outros usuários de informações úteis.
Eric Ouellet
27

Resumindo as respostas e este ótimo artigo Combine Behaviors in Styles , cheguei a esta solução genérica curta e conveniente:

Fiz uma classe genérica, que poderia ser herdada por qualquer comportamento.

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

Então, você pode simplesmente reutilizá-lo com muitos componentes como este:

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

E em XAML o suficiente para declarar:

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

Então, basicamente, a classe AttachableForStyleBehavior fez coisas xaml, registrando a instância do comportamento para cada componente no estilo. Para mais detalhes, consulte o link.

Roma Borodov
fonte
Funciona como um encanto! Com o meu Scrollingbehavior combinado, eu me livrei de Inner RowDetailsTemplate-Datagrids que não rolavam nos Datagrids pai.
Philipp Michalski
Fico feliz em ajudar, aproveite =)
Roma Borodov
1
e quanto à vinculação de dados com propriedades de dependência no comportamento?
JobaDiniz
Não sei como entrar em contato com o usuário ou recusar a edição com feedback negativo pessoalmente. Portanto, querido @Der_Meister e outros editores, por favor, leia o código cuidadosamente antes de tentar editá-lo. Isso pode afetar outros usuários e também minha reputação. Nesse caso, ao remover a propriedade IsEnabledForStyle e substituí-la insistentemente por métodos estáticos, você está destruindo a possibilidade de vinculação a ela no xaml, que é o ponto principal desta questão. Parece que você não leu o código até o fim. Infelizmente, não posso rejeitar sua edição com ótimo sinal de menos, portanto, tome cuidado no futuro.
Roma Borodov,
1
@RomaBorodov, tudo funciona em XAML. É uma maneira correta de definir a propriedade anexada (que é diferente da propriedade de dependência). Consulte a documentação: docs.microsoft.com/en-us/dotnet/framework/wpf/advanced/…
Der_Meister
19

1. Criar propriedade anexada

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. Crie um comportamento

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.Crie um estilo e defina a propriedade anexada

        <Style TargetType="{x:Type DataGridCell}">
            <Setter Property="Behaviors:DataGridCellAttachedProperties.IsSingleClickEditMode" Value="True"/>
        </Style>
Dvoskin romano
fonte
Quando tento acessar o DependencyProperty do estilo diz que IsSingleClickEditMode não é reconhecido ou não está acessível?
Igor Meszaros
Desculpe meu erro .. assim que comentei, percebi que GetIsSingleClickEditMode deve corresponder à string que você passou para DependencyProperty.RegisterAttached
Igor Meszaros
OnDetaching adiciona outro manipulador de eventos, isso deve ser corrigido (não é possível modificar um único caractere ao editar uma postagem ...)
BalintPogatsa
11

Tenho outra ideia, para evitar a criação de uma propriedade anexada para cada comportamento:

  1. Interface do criador de comportamento:

    public interface IBehaviorCreator
    {
        Behavior Create();
    }
    
  2. Coleção pequena de ajudantes:

    public class BehaviorCreatorCollection : Collection<IBehaviorCreator> { }
    
  3. Classe auxiliar que anexa o comportamento:

    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. Agora seu comportamento, que implementa 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. E agora use em xaml:

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

Não consegui encontrar o artigo original, mas consegui recriar o efeito.

#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>
Jonathan Allen
fonte
Ter que escrever isso para cada comportamento é um pouco como um PITA.
Stephen Drew
0

O código de comportamento espera um Visual, portanto, podemos adicioná-lo apenas em um visual. Portanto, a única opção que vejo é adicionar a um dos elementos dentro do ControlTemplate para obter o comportamento adicionado ao Style e afetar todas as instâncias de um controle específico.

Jobi Joy
fonte
0

O artigo Introdução aos comportamentos anexados no WPF implementa um comportamento anexado usando apenas Estilo e também pode ser relacionado ou útil.

A técnica no artigo "Introdução aos comportamentos anexados" evita as tags de interatividade por completo, usando em Estilo. Não sei se é por se tratar de uma técnica mais antiquada, ou se ainda confere alguns benefícios onde se deve preferir em alguns cenários.

Conta
fonte
2
Este não é um comportamento do Blend, é um "comportamento" por meio de uma propriedade anexada simples.
Stephen Drew
0

Eu gosto da abordagem mostrada pelas respostas de Roman Dvoskin e Jonathan Allen neste tópico. Porém, quando eu estava aprendendo essa técnica pela primeira vez, aproveitei esta postagem do blog que fornece mais explicações sobre a técnica. E para ver tudo em contexto, aqui está o código-fonte completo da classe sobre a qual o autor fala em sua postagem no blog.

Jason Frank
fonte
0

Declare comportamento individual / gatilho como recursos:

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

Insira-os na coleção:

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

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

</Button>
AnjumSKhan
fonte
4
Como responde ao OP? O gatilho não é adicionado por meio de um estilo em sua resposta.
Kryptos
0

Com base nessa resposta, criei uma solução mais simples, com apenas uma classe necessária e não há necessidade de implementar outra coisa em seus comportamentos.

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
}

e o uso da amostra é

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

Não se esqueça de adicionar este xmlns para usar ArrayList:

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