98

I have crated a Blend behavior for Button. How can I set that to all of my Buttons in the app.

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

However, when I try:

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

I get the error

The property "Behaviors" does not have an accessible setter.

Eric
  • 95,302
  • 53
  • 242
  • 374
Jobi Joy
  • 49,102
  • 20
  • 108
  • 119

10 Answers10

83

I had the same problem and I've come up with a solution. I found this question after I solved it and I see that my solution bears a lot in common with Mark's. However, this approach is a little different.

The main problem is that behaviors and triggers associate with a specific object and so you cannot use the same instance of a behavior for multiple different associated objects. When you define your behavior inline XAML enforces this one-to-one relationship. However, when you try to set a behavior in a style, the style can be re-used for all the objects it applies to and this will throw exceptions in the base behavior classes. In fact the authors went to considerable effort to prevent us from even trying to do this, knowing that it wouldn't work.

The first problem is that we cannot even construct a behavior setter value because the constructor is internal. So we need our own behavior and trigger collection classes.

The next problem is that the behavior and trigger attached properties don't have setters and so they can only be added to with in-line XAML. This problem we solve with our own attached properties that manipulate the primary behavior and trigger properties.

The third problem is that our behavior collection is only good for a single style target. This we solve by utilizing a little-used XAML feature x:Shared="False" which creates a new copy of the resource each time it is referenced.

The final problem is that behaviors and triggers are not like other style setters; we don't want to replace the old behaviors with the new behaviors because they could do wildly different things. So if we accept that once you add a behavior you cannot take it away (and that's the way behaviors currently work), we can conclude that behaviors and triggers should be additive and this can be handled by our attached properties.

Here is a sample using this approach:

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

The example uses triggers but behaviors work the same way. In the example, we show:

  • the style can be applied to multiple text blocks
  • several types of data binding all work correctly
  • a debug action that generates text in the output window

Here's an example behavior, our DebugAction. More properly it is an action but through the abuse of language we call behaviors, triggers and actions "behaviors".

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

Finally, our collections and attached properties to make this all work. By analogy with Interaction.Behaviors, the property you target is called SupplementaryInteraction.Behaviors because by setting this property, you will add behaviors to Interaction.Behaviors and likewise for triggers.

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

and there you have it, fully-functional behaviors and triggers applied through styles.

Rick Sladkey
  • 33,988
  • 6
  • 71
  • 95
  • Great stuff, this works beautifully. I noticed that if you put the style, for example, in the UserControl resources, then e.NewValue may be null at first (might depend on the control used - I'm using this on the XamDataTreeNodeControl in an Infragistics XamDataTree). So I added a little sanity check in OnPropertyTriggersChanged: if (e.NewValue != null) – MetalMikester Dec 09 '11 at 17:10
  • Has anybody had a problem with this approach when applying the Setter in an **implicit** Style? I've gotten it to work fine with a non-implicit style (one with a Key), but I get a cyclic reference exception if its in an implicit style. – Jason Frank Apr 01 '13 at 15:35
  • 1
    Nice solution, but unfortunately it doesn't work in WinRT, because x:Shared doesn't exist on this platform... – Thomas Levesque Jul 06 '13 at 01:54
  • 2
    I can confirm that this solution works. Thank you very much for sharing it. I have not yet tried it with an implicit style, though. – Golvellius Sep 06 '13 at 14:40
  • @Jason Frank, I think I use it as a non implicit and it works fine without cyclic reference exception. Do you use BasedOn which could create the cyclic reference? ... This is how I use it: – Eric Ouellet Feb 11 '16 at 15:16
  • @EricOuellet Thanks for replying. I'm not able to answer your follow-up question as I am not working in this tech right now. But just in case others follow-up, I mentioned in my comment that I *did* get it to work with non-implicit style, and that I only got the cyclic reference exception if it *is* in an *implicit* style. So my question was specifically for the implicit style case. – Jason Frank Feb 11 '16 at 16:12
  • 2
    @Jason Frank, Thanks, Just as references for others... I made it works in both cases: Implicit and explicit. In fact I ask a question where I would have put all of my code to help others but somebody estimate that my question was a duplicate. I cannot answer my own question giving everything I have found. I think I discover pretty nice things. :-( ...I hope it does not happen too often because that behavior deprive other users from useful information. – Eric Ouellet Feb 11 '16 at 17:01
  • 1
    I got an exception `An instance of a Behavior cannot be attached to more than one object at a time` despite `x:Shared="False"`. Turns out the shared property isn't supported in "nested" dictionaries and `ResourceDictionary.MergedDictionaries` apparently counts. However, I got it to work anyway when the resource was defined in a separate XAML file consisting of just a `ResourceDictionary`. Hope that helps someone... – Tim Sylvester Dec 12 '18 at 01:28
  • Further to @TimSylvester's comment, the x:Shared=False behavior is documented here .. https://learn.microsoft.com/en-us/previous-versions/dotnet/netframework-4.0/aa970778(v=vs.100)?redirectedfrom=MSDN. However, it can also be defined in the same file as well, but it has to be at the 'top level', i.e. not within another control's resources section if that control is already part of a resource dictionary. – stoj May 28 '22 at 13:23
32

Summing answers and this great article Blend Behaviors in Styles, I came to this generic short and convinient solution:

I made generic class, which could be inherited by any behavior.

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

So you could simply reuse it with lot of components like this:

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

And in XAML enough to declare:

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

So basicly the AttachableForStyleBehavior class made xaml things, registering the instance of behavior for each component in style. For more details, please see the link.

Matt Gregory
  • 8,074
  • 8
  • 33
  • 40
Roma Borodov
  • 596
  • 4
  • 10
  • Works like a charm! With my Scrollingbehavior combined i got rid of Inner RowDetailsTemplate-Datagrids not scrolling the parent Datagrids. – Philipp Michalski Jul 28 '15 at 11:44
  • Glad to help, enjoy=) – Roma Borodov Dec 06 '15 at 16:28
  • 1
    what about data binding with dependency properties in the Behavior? – JobaDiniz Dec 02 '16 at 11:09
  • I don't know how to contact user or decline edit with negative feedback personally. So dear @Der_Meister and other editors, please read code carefully before you trying to edit it. It could affect other users and my reputation too. In this case, by removing IsEnabledForStyle property and insistently replacing it with static methods, you destroying the possibility of binding to it in xaml, which is the main point of this question. So looks like you didn't read code till the end. Saddly I can't reject your editing with great minus, so just please be careful in future. – Roma Borodov Apr 06 '18 at 14:44
  • 1
    @RomaBorodov, everything works in XAML. It's a correct way to define attached property (which is different from dependency property). See documentation: https://learn.microsoft.com/en-us/dotnet/framework/wpf/advanced/attached-properties-overview#custom-attached-properties – Der_Meister Apr 06 '18 at 15:24
  • Is there any way I can access the property directly in the control xaml? Example: – Luishg Jul 05 '19 at 06:05
  • This looks great, except I have a behavior which needs its own argument passed in (in my case, a Control as a property called PlacementTarget). Is this extendable to allow other things to be bound? – Keith Jul 10 '19 at 19:24
19

1.Create Attached Property

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.Create a Behavior

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.Create a Style and set the attached property

        <Style TargetType="{x:Type DataGridCell}">
            <Setter Property="Behaviors:DataGridCellAttachedProperties.IsSingleClickEditMode" Value="True"/>
        </Style>
Roman Dvoskin
  • 384
  • 4
  • 16
  • When I try to access the DependencyProperty from the style it says IsSingleClickEditMode is not recognized or no accessible? – Igor Meszaros Feb 02 '17 at 11:36
  • Sorry my bad.. as soon as I commented I realised GetIsSingleClickEditMode should match the string you pass in to the DependencyProperty.RegisterAttached – Igor Meszaros Feb 02 '17 at 11:40
  • OnDetaching adds another event handler, this should be fixed (cannot modify a single character when editing a post...) – BalintPogatsa Mar 23 '20 at 11:37
11

I have another idea, to avoid the creation of a attached property for every behavior:

  1. Behavior creator interface:

    public interface IBehaviorCreator
    {
        Behavior Create();
    }
    
  2. Small helper collection:

    public class BehaviorCreatorCollection : Collection<IBehaviorCreator> { }
    
  3. Helper class which attaches the behavior:

    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. Now your behavior, which implements 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. And now use it in xaml:

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

I couldn't find the original article but I was able to recreate the effect.

#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>
H.B.
  • 166,899
  • 29
  • 327
  • 400
Jonathan Allen
  • 68,373
  • 70
  • 259
  • 447
5

Based on this answer I made a simpler solution, with just one class needed and there is no need to implement something else in your behaviors.

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
}

and the sample usage is

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

Don't forget to add this xmlns to use ArrayList:

xmlns:collections="clr-namespace:System.Collections;assembly=mscorlib"
technopriest
  • 161
  • 2
  • 5
  • Nice solution! Very low code impact especially on the behaviors themselves. One thing to watch out for is that the `AssociatedObject` can become `null` after `OnAttached` is call, I assume, because of the cloning. I worked around it by storing a private prop in the behavior with ref to it. – Ernie S Mar 05 '23 at 16:44
0

I like the approach shown by the answers by Roman Dvoskin and Jonathan Allen in this thread. When I was first learning that technique though, I benefited from this blog post which provides more explanation about the technique. And to see everything in context, here is the entire source code for the class that the author talks about in his blog post.

Jason Frank
  • 3,842
  • 31
  • 35
0

Behavior code expects a Visual, so we can add it only on a visual. So the only option I could see is to add to one of the element inside the ControlTemplate so as to get the behavior added to the Style and affect on all the instance of a particular control.

Jobi Joy
  • 49,102
  • 20
  • 108
  • 119
0

Declare individual behavior/trigger as Resources :

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

Insert them in the collection :

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

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

</Button>
AnjumSKhan
  • 9,647
  • 1
  • 26
  • 38
0

The article Introduction to Attached Behaviors in WPF implements an attached behavior using Style only, and may also be related or helpful.

The technique in the "Introduction to Attached Behaviors" article avoids the Interactivity tags altogether, using on Style. I don't know if this is just because it is a more dated technique, or, if that still confers some benefits where one should prefer it in some scenarios.

Bill
  • 1,407
  • 1
  • 15
  • 22