Attached behavior

De Banane Atomic
Aller à la navigationAller à la recherche

Définition

Un behavior est une surcouche d'attached property.
L'AB est définie dans une classe non-statique pour pouvoir ensuite être attachée à un DependencyObject.
L'AB correspond a une fonctionnalité autonome qui peut s'ajouter à des DependencyObjects pour étendre leurs fonctionnalités.

Paquet NuGet à installer

Microsoft.Xaml.Behaviors.Wpf

Le plus récent: Exemples

  • Microsoft.Xaml.Behaviors
    • Behavior
  • System.Windows.Interactivity
    • Behavior

Expression.Blend.Sdk.WPF

  • Microsoft.Expression.Interactions
    • MouseDragElementBehavior
  • System.Windows.Interactivity
    • Behavior

InvokeCommandAction

Permet d'executer une commande suite à un évènement ou au changement de valeur d'une propriété.
Ici on execute la commande SelectionChangedCmd lorsque l’évènement SelectionChanged est lancé.

MainWindow.xaml
<Window xmlns:Behaviors="http://schemas.microsoft.com/xaml/behaviors">
    <Grid>
        <ComboBox x:Name="cb">
            <ComboBoxItem>Item #1</ComboBoxItem>
            <ComboBoxItem IsSelected="True">Item #2</ComboBoxItem>

            <Behaviors:Interaction.Triggers>
                <Behaviors:EventTrigger EventName="SelectionChanged">
                    <Behaviors:InvokeCommandAction Command="{Binding ChangeColorCmd}"
                                                   CommandParameter="{Binding ElementName=cb}" />
                </Behaviors:EventTrigger>
            </Behaviors:Interaction.Triggers>
        </ComboBox>

CallMethodAction

Permet d'executer une méthode suite à un évènement ou au changement de valeur d'une propriété.
La méthode doit être publique et ne pas avoir d'arguments.

MainWindow.xaml
<Window xmlns:Behaviors="http://schemas.microsoft.com/xaml/behaviors">

    <Behaviors:Interaction.Triggers>
        <Behaviors:EventTrigger EventName="Loaded">
            <Behaviors:CallMethodAction TargetObject="{Binding Mode=OneWay}"
                                        MethodName="OnViewLoaded" />
        </Behaviors:EventTrigger>
    </Behaviors:Interaction.Triggers>
MainViewModel.cs
public void OnViewLoaded()
{ }

ChangePropertyAction

Permet de modifier un propriété suite à un évènement ou au changement de valeur d'une propriété.

MainWindow.xaml
<Window xmlns:Behaviors="http://schemas.microsoft.com/xaml/behaviors">

    <RadioButton Content="Good"
                 VerticalAlignment="Stretch"
                 IsChecked="True"
                 x:Name="rb1">
        <Behaviors:Interaction.Triggers>
            <Behaviors:DataTrigger Binding="{Binding IsChecked, ElementName=rb1}"
                                   Value="True">
                <Behaviors:ChangePropertyAction TargetObject="{Binding}"
                                                PropertyName="CustomerStatus">
                    <Behaviors:ChangePropertyAction.Value>
                        <local:CustomerStatus>Good</local:CustomerStatus>
                    </Behaviors:ChangePropertyAction.Value>
                </Behaviors:ChangePropertyAction>
            </Behaviors:DataTrigger>
        </Behaviors:Interaction.Triggers>
    </RadioButton>

    <RadioButton Content="Bad"
                 VerticalAlignment="Stretch"
                 x:Name="rb2">
        <Behaviors:Interaction.Triggers>
            <Behaviors:DataTrigger Binding="{Binding IsChecked, ElementName=rb2}"
                                   Value="True">
                <Behaviors:ChangePropertyAction TargetObject="{Binding}"
                                                PropertyName="CustomerStatus">
                    <Behaviors:ChangePropertyAction.Value>
                        <local:CustomerStatus>Bad</local:CustomerStatus>
                    </Behaviors:ChangePropertyAction.Value>
                </Behaviors:ChangePropertyAction>
            </Behaviors:DataTrigger>
        </Behaviors:Interaction.Triggers>
    </RadioButton>
MainViewModel.cs
public class MainViewModel : BindableBase
{
    public CustomerStatus CustomerStatus { get; set; }
}

public enum CustomerStatus
{
    Good,
    Bad
}

Custom Behavior

  1. écrire une fonctionnalité dans le code-behind
  2. déplacer le code-behind dans un behavior
MyTextBoxBehavior.cs
// un behavior pour les TextBox
public class MyTextBoxBehavior : Behavior<TextBox>
{
    private static Regex decimalRegex = 
        new Regex($@"^\d+{CultureInfo.CurrentCulture.NumberFormat.PercentDecimalSeparator}?\d*$", RegexOptions.Compiled);

    // dependency property
    public bool EnableDecimalInput
    {
        get { return (bool) GetValue(EnableDecimalInputProperty); }
        set { SetValue(EnableDecimalInputProperty, value); }
    }
    public static readonly DependencyProperty EnableDecimalInputProperty = 
        DependencyProperty.Register("EnableDecimalInputProperty", typeof(bool),
            typeof(NumericInputBehavior), new PropertyMetadata(false));

    // called when XAML is parsed and behavior is added
    protected override void OnAttached()
    {
        // AssociatedObject correspond au control attachant le behavior
        AssociatedObject.PreviewTextInput += OnPreviewTextInput;
    }

    // never called unless programmatic code removes behavior from parent collection or calls Detach
    protected override void OnDetaching()
    {
        AssociatedObject.PreviewTextInput -= OnPreviewTextInput;
    }

    private void OnPreviewTextInput(object sender, TextCompositionEventArgs e)
    {
        if (EnableDecimalInput)
        {
            if (!decimalRegex.IsMatch(AssociatedObject.Text + e.Text))
                e.Handled = true;
        }
        else
        {
            if (!Char.IsDigit(e.Text[0]))
                e.Handled = true;
        }
    }
}
MyWindow.xaml
<Window xmlns:Behaviors="http://schemas.microsoft.com/xaml/behaviors">
    <TextBox>
        <Behaviors:Interaction.Behaviors>
            <local:MyTextBoxBehavior EnableDecimalInput="True" />
        </Behaviors:Interaction.Behaviors>
    </TextBox>

Attacher le behavior via un style

MyWindow.xaml
<telerik:RadGridView ItemsSource="{Binding Items}"
                     ValidatesOnDataErrors="InViewMode">
    <telerik:RadGridView.RowStyle>
        <Style TargetType="telerik:GridViewRow"
               BasedOn="{StaticResource {x:Type telerik:GridViewRow}}">
            <Setter Property="local:MyBehavior.IsEnabled" Value="True" />
        </Style>
    </telerik:RadGridView.RowStyle>

</telerik:RadGridView>
MyBehavior.cs
class MyBehavior : Behavior<GridViewRow>
{
    protected override void OnAttached()
    {
        // AssociatedObject correspond au control attachant le behavior
        base.OnAttached();
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
    }

    public static bool GetIsEnabled(DependencyObject element)
    {
        return (bool) element.GetValue(IsEnabledProperty);
    }

    public static void SetIsEnabled(DependencyObject element, bool value)
    {
        element.SetValue(IsEnabledProperty, value);
    }

    public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.RegisterAttached(
        "IsEnabled",
        typeof(bool),
        typeof(MyBehavior),
        new PropertyMetadata(default(bool), OnIsEnabledChanged));

    private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var behaviors = Interaction.GetBehaviors(d);
        if ((bool)e.NewValue)
        {
            if (!behaviors.OfType<MyBehavior>().Any())
            {
                behaviors.Add(new MyBehavior());
            }
        }
        else
        {
            behaviors.RemoveAll(b => b is MyBehavior);
        }
    }
}

Custom behavior vs custom control

Utilisé de préférence un custom behavior, sauf si:

  • il faut accéder aux membres protected
  • il faut modifier le control template

Custom Trigger

Comme le custom behavior mais définit seulement les conditions de déclenchement pas le behavior. Celui-ci est définit dans le xaml à chaque déclaration du trigger.

EmployeeTrigger.cs
public class EmployeeTrigger : TriggerBase<TextBlock>
{
    protected override void OnAttached()
    {
        AssociatedObject.Loaded += OnLoaded;
        AssociatedObject.Unloaded += OnUnloaded;
    }

    protected override void OnDetaching()
    {
        AssociatedObject.Loaded -= OnLoaded;
        AssociatedObject.Unloaded -= OnUnloaded;
    }

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        AssociatedObject.DataContextChanged += OnDataContextChanged;
        Refresh();
    }

    private void OnUnloaded(object sender, RoutedEventArgs e)
    {
        AssociatedObject.DataContextChanged -= OnDataContextChanged;
    }

    private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        Refresh();
    }

    private void Refresh()
    {
        if (AssociatedObject.DataContext is Employee employee && employee.IsNew)
        {
            base.InvokeActions(null);
        }
    }
}
MyWindow.xaml
<Window xmlns:Behaviors="http://schemas.microsoft.com/xaml/behaviors">

    <ItemsControl ItemsSource="{Binding Employees}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding Name}">

                    <Behaviors:Interaction.Triggers>
                        <local:EmployeeTrigger>
                            <Behaviors:ChangePropertyAction PropertyName="Foreground">
                                <Behaviors:ChangePropertyAction.Value>
                                    <SolidColorBrush Color="Red" />
                                </Behaviors:ChangePropertyAction.Value>
                            </Behaviors:ChangePropertyAction>
                        </local:EmployeeTrigger>
                    </Behaviors:Interaction.Triggers>