Cómo agregar un comportamiento de fusión en un Style Setter


88

He creado un comportamiento Blend para Button. ¿Cómo puedo configurar eso para todos mis botones en la aplicación?

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

Sin embargo, cuando intento:

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

Me sale el error

La propiedad "Behaviors" no tiene un setter accesible.

Respuestas:


76

Tuve el mismo problema y se me ocurrió una solución. Encontré esta pregunta después de resolverla y veo que mi solución tiene mucho en común con la de Mark. Sin embargo, este enfoque es un poco diferente.

El problema principal es que los comportamientos y los desencadenantes se asocian con un objeto específico y, por lo tanto, no se puede usar la misma instancia de un comportamiento para varios objetos asociados diferentes. Cuando define su comportamiento, XAML en línea aplica esta relación uno a uno. Sin embargo, cuando intenta establecer un comportamiento en un estilo, el estilo se puede reutilizar para todos los objetos a los que se aplica y esto arrojará excepciones en las clases de comportamiento base. De hecho, los autores hicieron un esfuerzo considerable para evitar que intentáramos hacer esto, sabiendo que no funcionaría.

El primer problema es que ni siquiera podemos construir un valor de establecimiento de comportamiento porque el constructor es interno. Entonces necesitamos nuestro propio comportamiento y clases de colección de disparadores.

El siguiente problema es que el comportamiento y las propiedades adjuntas del desencadenador no tienen establecedores, por lo que solo se pueden agregar con XAML en línea. Este problema lo resolvemos con nuestras propias propiedades adjuntas que manipulan el comportamiento primario y las propiedades de activación.

El tercer problema es que nuestra colección de comportamientos solo es buena para un solo objetivo de estilo. Esto lo resolvemos utilizando una característica XAML poco utilizada x:Shared="False"que crea una nueva copia del recurso cada vez que se hace referencia a él.

El problema final es que los comportamientos y los factores desencadenantes no son como los que establecen otros estilos; no queremos reemplazar los viejos comportamientos con los nuevos porque podrían hacer cosas tremendamente diferentes. Entonces, si aceptamos que una vez que agrega un comportamiento, no puede eliminarlo (y esa es la forma en que funcionan los comportamientos actualmente), podemos concluir que los comportamientos y los desencadenantes deben ser aditivos y esto puede ser manejado por nuestras propiedades adjuntas.

Aquí hay una muestra que utiliza este enfoque:

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

El ejemplo usa disparadores pero los comportamientos funcionan de la misma manera. En el ejemplo, mostramos:

  • el estilo se puede aplicar a varios bloques de texto
  • varios tipos de enlace de datos funcionan correctamente
  • una acción de depuración que genera texto en la ventana de salida

Aquí hay un comportamiento de ejemplo, nuestro DebugAction. Más propiamente es una acción, pero a través del abuso del lenguaje llamamos comportamientos, disparadores y acciones "comportamientos".

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, nuestras colecciones y propiedades adjuntas para que todo esto funcione. Por analogía con Interaction.Behaviors, la propiedad a la que se dirige se llama SupplementaryInteraction.Behaviorsporque al establecer esta propiedad, agregará comportamientos Interaction.Behaviorsy también para los desencadenantes.

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

y ahí lo tiene, comportamientos completamente funcionales y disparadores aplicados a través de estilos.


Gran material, funciona de maravilla. Noté que si pones el estilo, por ejemplo, en los recursos de UserControl, entonces e.NewValue puede ser nulo al principio (podría depender del control usado; estoy usando esto en XamDataTreeNodeControl en un Infragistics XamDataTree). Así que agregué un pequeño control de cordura en OnPropertyTriggersChanged: if (e.NewValue! =
Null

¿Alguien ha tenido algún problema con este enfoque al aplicar el Setter en un estilo implícito ? Logré que funcione bien con un estilo no implícito (uno con una clave), pero obtengo una excepción de referencia cíclica si está en un estilo implícito.
Jason Frank

1
Buena solución, pero desafortunadamente no funciona en WinRT, porque x: Shared no existe en esta plataforma ...
Thomas Levesque

1
Puedo confirmar que esta solución funciona. Muchas gracias por compartirlo. Sin embargo, todavía no lo he probado con un estilo implícito.
Golvellius

2
@Jason Frank, Gracias, Solo como referencias para otros ... Hice que funcionara en ambos casos: Implícito y explícito. De hecho, hago una pregunta en la que habría puesto todo mi código para ayudar a otros, pero alguien estima que mi pregunta era un duplicado. No puedo responder a mi propia pregunta dando todo lo que he encontrado. Creo que descubro cosas muy bonitas. :-( ... Espero que no suceda con demasiada frecuencia porque ese comportamiento priva a otros usuarios de información útil.
Eric Ouellet

27

Resumiendo las respuestas y este gran artículo Mezcla de comportamientos en estilos , llegué a esta solución genérica, breve y conveniente:

Hice una clase genérica, que podría ser heredada por cualquier comportamiento.

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

Entonces, simplemente podría reutilizarlo con muchos componentes como este:

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

Y en XAML lo suficiente para declarar:

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

Entonces, básicamente, la clase AttachableForStyleBehavior hizo cosas xaml, registrando la instancia de comportamiento para cada componente en estilo. Para obtener más detalles, consulte el enlace.


¡Funciona de maravilla! Con mi Scrollingbehavior combinado, me deshice de Inner RowDetailsTemplate-Datagrids que no desplazaban los Datagrids principales.
Philipp Michalski

Me alegro de ayudar, disfruta =)
Roma Borodov

1
¿Qué pasa con el enlace de datos con propiedades de dependencia en el comportamiento?
JobaDiniz

No sé cómo contactar al usuario o rechazar la edición con comentarios negativos personalmente. Así que, querido @Der_Meister y otros editores, lea el código detenidamente antes de intentar editarlo. Podría afectar a otros usuarios y también a mi reputación. En este caso, al eliminar la propiedad IsEnabledForStyle y reemplazarla insistentemente con métodos estáticos, se destruye la posibilidad de vincularla en xaml, que es el punto principal de esta pregunta. Parece que no leíste el código hasta el final. Lamentablemente, no puedo rechazar su edición con un gran inconveniente, así que tenga cuidado en el futuro.
Roma Borodov

1
@RomaBorodov, todo funciona en XAML. Es una forma correcta de definir la propiedad adjunta (que es diferente de la propiedad de dependencia). Consulte la documentación: docs.microsoft.com/en-us/dotnet/framework/wpf/advanced/…
Der_Meister

19

1.Crear propiedad adjunta

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.Crear un comportamiento

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.Cree un estilo y establezca la propiedad adjunta

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

Cuando intento acceder a DependencyProperty desde el estilo, dice que IsSingleClickEditMode no se reconoce o no se puede acceder.
Igor Meszaros

Lo siento mi mal ... tan pronto como comenté me di cuenta de que GetIsSingleClickEditMode debería coincidir con la cadena que pasas a DependencyProperty.RegisterAttached
Igor Meszaros

OnDetaching agrega otro controlador de eventos, esto debería arreglarse (no se puede modificar un solo carácter al editar una publicación ...)
BalintPogatsa

11

Tengo otra idea, para evitar la creación de una propiedad adjunta para cada comportamiento:

  1. Interfaz de creador de comportamiento:

    public interface IBehaviorCreator
    {
        Behavior Create();
    }
    
  2. Pequeña colección de ayudantes:

    public class BehaviorCreatorCollection : Collection<IBehaviorCreator> { }
    
  3. Clase auxiliar que adjunta el comportamiento:

    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. Ahora su comportamiento, 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. Y ahora úsalo en xaml:

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

5

No pude encontrar el artículo original, pero pude recrear el efecto.

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

Sin embargo, tener que escribir esto para cada comportamiento es un poco PITA.
Stephen Drew

0

El código de comportamiento espera un Visual, por lo que podemos agregarlo solo en un visual. Entonces, la única opción que pude ver es agregar a uno de los elementos dentro de ControlTemplate para que el comportamiento se agregue al Estilo y afecte a todas las instancias de un control en particular.


0

El artículo Introducción a los comportamientos adjuntos en WPF implementa un comportamiento adjunto usando solo Estilo y también puede estar relacionado o ser útil.

La técnica del artículo "Introducción a los comportamientos adjuntos" evita las etiquetas de interactividad por completo, utilizando on Style. No sé si esto es solo porque es una técnica más anticuada, o si eso aún confiere algunos beneficios donde uno debería preferirlo en algunos escenarios.


2
Este no es un comportamiento de mezcla, es un "comportamiento" a través de una propiedad adjunta simple.
Stephen Drew

0

Me gusta el enfoque mostrado por las respuestas de Roman Dvoskin y Jonathan Allen en este hilo. Sin embargo, cuando estaba aprendiendo esa técnica por primera vez, me beneficié de esta publicación de blog que ofrece más explicaciones sobre la técnica. Y para ver todo en contexto, aquí está el código fuente completo de la clase de la que habla el autor en su publicación de blog.


0

Declare el comportamiento / desencadenante individual 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>

Insértelos en la colección:

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

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

</Button>

4
¿Cómo responde el OP? El disparador no se agrega a través de un estilo en su respuesta.
Kryptos

0

Basándome en esta respuesta, hice una solución más simple, con solo una clase necesaria y no hay necesidad de implementar otra cosa en sus comportamientos.

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
}

y el uso de muestra es

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

No olvide agregar este xmlns para usar ArrayList:

xmlns:collections="clr-namespace:System.Collections;assembly=mscorlib"
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.