Come aggiungere un comportamento di fusione in uno Style Setter


88

Ho creato un comportamento Blend per Button. Come posso impostarlo su tutti i miei pulsanti nell'app.

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

Tuttavia, quando provo:

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

Ottengo l'errore

La proprietà "Behaviors" non ha un setter accessibile.

Risposte:


76

Ho avuto lo stesso problema e ho trovato una soluzione. Ho trovato questa domanda dopo averla risolta e vedo che la mia soluzione ha molto in comune con quella di Mark. Tuttavia, questo approccio è leggermente diverso.

Il problema principale è che comportamenti e trigger si associano a un oggetto specifico e quindi non è possibile utilizzare la stessa istanza di un comportamento per più oggetti associati diversi. Quando definisci il tuo comportamento, XAML in linea applica questa relazione uno a uno. Tuttavia, quando si tenta di impostare un comportamento in uno stile, lo stile può essere riutilizzato per tutti gli oggetti a cui si applica e questo genererà eccezioni nelle classi di comportamento di base. In effetti gli autori hanno fatto uno sforzo considerevole per impedirci anche solo di provare a farlo, sapendo che non avrebbe funzionato.

Il primo problema è che non possiamo nemmeno costruire un valore di impostazione del comportamento perché il costruttore è interno. Quindi abbiamo bisogno del nostro comportamento e di attivare le classi di raccolta.

Il problema successivo è che il comportamento e le proprietà associate al trigger non hanno setter e quindi possono essere aggiunti solo con XAML in linea. Questo problema lo risolviamo con le nostre proprietà associate che manipolano il comportamento primario e le proprietà di attivazione.

Il terzo problema è che la nostra raccolta di comportamenti è valida solo per un singolo target di stile. Questo viene risolto utilizzando una funzionalità XAML poco utilizzata x:Shared="False"che crea una nuova copia della risorsa ogni volta che viene referenziata.

Il problema finale è che i comportamenti e i trigger non sono come gli altri setter di stile; non vogliamo sostituire i vecchi comportamenti con i nuovi comportamenti perché potrebbero fare cose completamente diverse. Quindi, se accettiamo che una volta aggiunto un comportamento non puoi rimuoverlo (e questo è il modo in cui i comportamenti funzionano attualmente), possiamo concludere che i comportamenti e i trigger dovrebbero essere additivi e questo può essere gestito dalle nostre proprietà associate.

Ecco un esempio che utilizza questo approccio:

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

L'esempio utilizza trigger ma i comportamenti funzionano allo stesso modo. Nell'esempio mostriamo:

  • lo stile può essere applicato a più blocchi di testo
  • diversi tipi di data binding funzionano tutti correttamente
  • un'azione di debug che genera testo nella finestra di output

Ecco un esempio di comportamento, il nostro DebugAction. Più propriamente è un'azione, ma attraverso l'abuso del linguaggio chiamiamo comportamenti, trigger e azioni "comportamenti".

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

Infine, le nostre collezioni e le proprietà annesse per far funzionare tutto questo. Per analogia con Interaction.Behaviors, la proprietà targetizzata viene chiamata SupplementaryInteraction.Behaviorsperché impostando questa proprietà, aggiungerai comportamenti ai Interaction.Behaviorstrigger e allo stesso modo.

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 il gioco è fatto, comportamenti completamente funzionali e trigger applicati attraverso gli stili.


Grandi cose, funziona magnificamente. Ho notato che se metti lo stile, ad esempio, nelle risorse UserControl, e.NewValue potrebbe essere inizialmente nullo (potrebbe dipendere dal controllo utilizzato - lo sto usando su XamDataTreeNodeControl in un Infragistics XamDataTree). Quindi ho aggiunto un piccolo controllo di integrità in OnPropertyTriggersChanged: if (e.NewValue! = Null)
MetalMikester

Qualcuno ha avuto problemi con questo approccio applicando il Setter in uno stile implicito ? L'ho fatto funzionare bene con uno stile non implicito (uno con una chiave), ma ottengo un'eccezione di riferimento ciclico se è in uno stile implicito.
Jason Frank

1
Bella soluzione, ma sfortunatamente non funziona in WinRT, perché x: Shared non esiste su questa piattaforma ...
Thomas Levesque

1
Posso confermare che questa soluzione funziona. Grazie mille per averlo condiviso. Non l'ho ancora provato con uno stile implicito, però.
Golvellius

2
@ Jason Frank, grazie, proprio come riferimenti per gli altri ... Ho fatto in modo che funzionasse in entrambi i casi: implicito ed esplicito. In effetti faccio una domanda in cui avrei messo tutto il mio codice per aiutare gli altri, ma qualcuno stima che la mia domanda fosse un duplicato. Non posso rispondere alla mia domanda dando tutto quello che ho trovato. Penso di scoprire cose piuttosto carine. :-( ... spero che non accada troppo spesso perché quel comportamento priva gli altri utenti di informazioni utili.
Eric Ouellet

27

Sommando le risposte e questo fantastico articolo Blend Behaviors in Styles , sono giunto a questa soluzione generica breve e conveniente:

Ho creato una classe generica, che potrebbe essere ereditata da qualsiasi 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());
                }    
            }
        }
    }

Quindi potresti semplicemente riutilizzarlo con molti componenti come questo:

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

E in XAML abbastanza per dichiarare:

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

Quindi, in sostanza, la classe AttachableForStyleBehavior ha creato cose xaml, registrando l'istanza di comportamento per ogni componente in stile. Per ulteriori dettagli, vedere il collegamento.


Funziona come un fascino! Con il mio comportamento di scorrimento combinato mi sono sbarazzato di Inner RowDetailsTemplate-Datagrids che non scorrevano i Datagrids padre.
Philipp Michalski

Felice di aiutare, buon divertimento =)
Roma Borodov

1
che dire del data binding con proprietà di dipendenza nel comportamento?
JobaDiniz

Non so come contattare personalmente l'utente o rifiutare la modifica con feedback negativo. Quindi caro @Der_Meister e altri editor, leggi attentamente il codice prima di provare a modificarlo. Potrebbe influenzare altri utenti e anche la mia reputazione. In questo caso, rimuovendo la proprietà IsEnabledForStyle e sostituendola insistentemente con metodi statici, si distrugge la possibilità di legarsi ad essa in xaml, che è il punto principale di questa domanda. Quindi sembra che tu non abbia letto il codice fino alla fine. Purtroppo non posso rifiutare la tua modifica con un grande segno negativo, quindi fai attenzione in futuro.
Roma Borodov

1
@RomaBorodov, tutto funziona in XAML. È un modo corretto per definire la proprietà associata (che è diversa dalla proprietà di dipendenza). Consulta la documentazione: docs.microsoft.com/en-us/dotnet/framework/wpf/advanced/…
Der_Meister

19

1.Crea proprietà collegata

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.Crea un 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.Crea uno stile e imposta la proprietà allegata

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

Quando provo ad accedere a DependencyProperty dallo stile, viene visualizzato IsSingleClickEditMode non riconosciuto o non accessibile?
Igor Meszaros

Scusa il mio male .. non appena ho commentato mi sono reso conto che GetIsSingleClickEditMode dovrebbe corrispondere alla stringa che passi a DependencyProperty.RegisterAttached
Igor Meszaros

OnDetaching aggiunge un altro gestore di eventi, questo dovrebbe essere corretto (non è possibile modificare un singolo carattere durante la modifica di un post ...)
BalintPogatsa

11

Ho un'altra idea, per evitare la creazione di una proprietà allegata per ogni comportamento:

  1. Interfaccia del creatore di comportamento:

    public interface IBehaviorCreator
    {
        Behavior Create();
    }
    
  2. Piccola collezione di aiutanti:

    public class BehaviorCreatorCollection : Collection<IBehaviorCreator> { }
    
  3. Classe di supporto che allega il 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. Ora il tuo comportamento, che 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 ora usalo in xaml:

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

5

Non sono riuscito a trovare l'articolo originale ma sono riuscito a ricreare l'effetto.

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

Tuttavia, doverlo scrivere per ogni comportamento è un po 'una PITA.
Stephen Drew

0

Il codice di comportamento richiede un oggetto visivo, quindi possiamo aggiungerlo solo su un oggetto visivo. Quindi l'unica opzione che ho potuto vedere è quella di aggiungere a uno degli elementi all'interno del ControlTemplate in modo da ottenere il comportamento aggiunto allo Style e influire su tutte le istanze di un particolare controllo.


0

L'articolo Introduzione ai comportamenti allegati in WPF implementa un comportamento associato utilizzando solo lo stile e può anche essere correlato o utile.

La tecnica nell'articolo "Introduzione ai comportamenti allegati" evita del tutto i tag Interactivity, utilizzando su Style. Non so se questo è solo perché è una tecnica più datata, o se ciò conferisce ancora alcuni vantaggi dove si dovrebbe preferirlo in alcuni scenari.


2
Questo non è un comportamento Blend, è un "comportamento" attraverso una semplice proprietà associata.
Stephen Drew

0

Mi piace l'approccio mostrato dalle risposte di Roman Dvoskin e Jonathan Allen in questo thread. Quando stavo imparando per la prima volta quella tecnica, però, ho beneficiato di questo post sul blog che fornisce ulteriori spiegazioni sulla tecnica. E per vedere tutto nel contesto, ecco l'intero codice sorgente per la classe di cui parla l'autore nel suo post sul blog.


0

Dichiara comportamento / trigger individuale come risorse:

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

Inseriscili nella collezione:

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

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

</Button>

4
Come risponde all'OP? Il trigger non viene aggiunto tramite uno stile nella risposta.
Kryptos,

0

Sulla base di questa risposta ho creato una soluzione più semplice, con una sola classe necessaria e non è necessario implementare qualcos'altro nei tuoi comportamenti.

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 l'utilizzo di esempio è

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

Non dimenticare di aggiungere questo xmlns per utilizzare ArrayList:

xmlns:collections="clr-namespace:System.Collections;assembly=mscorlib"
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.