Comment ajouter un comportement de fusion dans un Setter de style


88

J'ai créé un comportement de fusion pour Button. Comment puis-je définir cela pour tous mes boutons dans l'application.

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

Cependant, quand j'essaye:

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

J'obtiens l'erreur

La propriété "Behaviors" n'a pas de setter accessible.

Réponses:


76

J'ai eu le même problème et j'ai trouvé une solution. J'ai trouvé cette question après l'avoir résolue et je vois que ma solution a beaucoup en commun avec celle de Mark. Cependant, cette approche est un peu différente.

Le problème principal est que les comportements et les déclencheurs s'associent à un objet spécifique et que vous ne pouvez donc pas utiliser la même instance d'un comportement pour plusieurs objets associés différents. Lorsque vous définissez votre comportement en ligne, XAML applique cette relation un-à-un. Cependant, lorsque vous essayez de définir un comportement dans un style, le style peut être réutilisé pour tous les objets auxquels il s'applique et cela lèvera des exceptions dans les classes de comportement de base. En fait, les auteurs ont déployé des efforts considérables pour nous empêcher même d'essayer de le faire, sachant que cela ne fonctionnerait pas.

Le premier problème est que nous ne pouvons même pas construire une valeur de paramètre de comportement car le constructeur est interne. Nous avons donc besoin de notre propre comportement et de déclencher des classes de collecte.

Le problème suivant est que le comportement et les propriétés jointes du déclencheur n'ont pas de setters et ne peuvent donc être ajoutés qu'avec du XAML en ligne. Nous résolvons ce problème avec nos propres propriétés attachées qui manipulent le comportement principal et déclenchent les propriétés.

Le troisième problème est que notre collection de comportements n'est valable que pour une seule cible de style. Nous résolvons ce problème en utilisant une fonctionnalité XAML peu utilisée x:Shared="False"qui crée une nouvelle copie de la ressource chaque fois qu'elle est référencée.

Le dernier problème est que les comportements et les déclencheurs ne sont pas comme les autres créateurs de style; nous ne voulons pas remplacer les anciens comportements par les nouveaux, car ils pourraient faire des choses très différentes. Donc, si nous acceptons qu'une fois que vous avez ajouté un comportement, vous ne pouvez pas le supprimer (et c'est ainsi que les comportements fonctionnent actuellement), nous pouvons conclure que les comportements et les déclencheurs doivent être additifs et cela peut être géré par nos propriétés attachées.

Voici un exemple utilisant cette approche:

<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'exemple utilise des déclencheurs mais les comportements fonctionnent de la même manière. Dans l'exemple, nous montrons:

  • le style peut être appliqué à plusieurs blocs de texte
  • plusieurs types de liaison de données fonctionnent tous correctement
  • une action de débogage qui génère du texte dans la fenêtre de sortie

Voici un exemple de comportement, notre DebugAction. Plus exactement, c'est une action, mais par l'abus du langage, nous appelons les comportements, les déclencheurs et les actions des «comportements».

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

Enfin, nos collections et propriétés attachées pour que tout cela fonctionne. Par analogie avec Interaction.Behaviors, la propriété que vous ciblez est appelée SupplementaryInteraction.Behaviorscar en définissant cette propriété, vous ajouterez des comportements à Interaction.Behaviorset de même pour les déclencheurs.

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

et voilà, des comportements et des déclencheurs entièrement fonctionnels appliqués à travers les styles.


Super truc, cela fonctionne à merveille. J'ai remarqué que si vous mettez le style, par exemple, dans les ressources UserControl, alors e.NewValue peut être nul au début (cela peut dépendre du contrôle utilisé - j'utilise ceci sur le XamDataTreeNodeControl dans un Infragistics XamDataTree). J'ai donc ajouté une petite vérification de cohérence dans OnPropertyTriggersChanged: if (e.NewValue! = Null)
MetalMikester

Quelqu'un a-t-il eu un problème avec cette approche lors de l'application du Setter dans un style implicite ? Je l'ai fait fonctionner correctement avec un style non implicite (un avec une clé), mais j'obtiens une exception de référence cyclique si c'est dans un style implicite.
Jason Frank

1
Belle solution, mais malheureusement cela ne fonctionne pas dans WinRT, car x: Shared n'existe pas sur cette plate-forme ...
Thomas Levesque

1
Je peux confirmer que cette solution fonctionne. Merci beaucoup de l'avoir partagé. Cependant, je ne l'ai pas encore essayé avec un style implicite.
Golvellius

2
@Jason Frank, Merci, Tout comme des références pour les autres ... Je l'ai fait fonctionner dans les deux cas: implicite et explicite. En fait, je pose une question où j'aurais mis tout mon code pour aider les autres, mais quelqu'un estime que ma question était un double. Je ne peux pas répondre à ma propre question en donnant tout ce que j'ai trouvé. Je pense que je découvre de jolies choses. :-( ... J'espère que cela n'arrive pas trop souvent car ce comportement prive les autres utilisateurs d'informations utiles.
Eric Ouellet

27

En résumant les réponses et cet excellent article Blend Behaviors in Styles , je suis arrivé à cette solution générique courte et pratique:

J'ai créé une classe générique, qui pourrait être héritée par n'importe quel comportement.

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

Vous pouvez donc simplement le réutiliser avec de nombreux composants comme celui-ci:

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

Et en XAML assez pour déclarer:

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

Donc, fondamentalement, la classe AttachableForStyleBehavior a fait des choses xaml, enregistrant l'instance de comportement pour chaque composant dans le style. Pour plus de détails, veuillez consulter le lien.


Fonctionne comme un charme! Avec mon Scrollingbehavior combiné, je me suis débarrassé de Inner RowDetailsTemplate-Datagrids ne faisant pas défiler les Datagrids parent.
Philipp Michalski

Heureux de vous aider, profitez-en =)
Roma Borodov

1
qu'en est-il de la liaison de données avec des propriétés de dépendance dans le comportement?
JobaDiniz

Je ne sais pas comment contacter l'utilisateur ou refuser de modifier personnellement avec des commentaires négatifs. Alors chers @Der_Meister et autres éditeurs, veuillez lire attentivement le code avant d'essayer de le modifier. Cela pourrait affecter d'autres utilisateurs et ma réputation aussi. Dans ce cas, en supprimant la propriété IsEnabledForStyle et en la remplaçant avec insistance par des méthodes statiques, vous supprimez la possibilité de s'y lier en xaml, ce qui est le point principal de cette question. On dirait donc que vous n'avez pas lu le code jusqu'à la fin. Tristement, je ne peux pas rejeter votre montage avec un grand moins, alors faites attention à l'avenir.
Roma Borodov

1
@RomaBorodov, tout fonctionne en XAML. C'est une manière correcte de définir la propriété jointe (qui est différente de la propriété de dépendance). Voir la documentation: docs.microsoft.com/en-us/dotnet/framework/wpf/advanced/…
Der_Meister

19

1.Créer une propriété attachée

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. créer un comportement

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.Créez un style et définissez la propriété attachée

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

Lorsque j'essaie d'accéder à DependencyProperty à partir du style, il indique IsSingleClickEditMode n'est pas reconnu ou n'est pas accessible?
Igor Meszaros

Désolé mon mauvais .. dès que j'ai commenté, j'ai réalisé que GetIsSingleClickEditMode devrait correspondre à la chaîne que vous transmettez à DependencyProperty.RegisterAttached
Igor Meszaros

OnDetaching ajoute un autre gestionnaire d'événements, cela devrait être corrigé (impossible de modifier un seul caractère lors de l'édition d'un message ...)
BalintPogatsa

11

J'ai une autre idée, pour éviter la création d'une propriété attachée pour chaque comportement:

  1. Interface de création de comportement:

    public interface IBehaviorCreator
    {
        Behavior Create();
    }
    
  2. Petite collection d'aide:

    public class BehaviorCreatorCollection : Collection<IBehaviorCreator> { }
    
  3. Classe d'assistance qui attache le comportement:

    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. Maintenant votre comportement, qui implémente 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. Et maintenant, utilisez-le 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

Je n'ai pas trouvé l'article original, mais j'ai pu recréer l'effet.

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

Cependant, avoir à écrire ceci pour chaque comportement est un peu un PITA.
Stephen Drew

0

Le code de comportement attend un visuel, nous ne pouvons donc l'ajouter que sur un visuel. Donc, la seule option que je pouvais voir est d'ajouter à l'un des éléments à l'intérieur du ControlTemplate afin d'obtenir le comportement ajouté au Style et d'affecter toute l'instance d'un contrôle particulier.


0

L'article Introduction aux comportements attachés dans WPF implémente un comportement attaché à l'aide de Style uniquement et peut également être lié ou utile.

La technique de l'article «Introduction aux comportements attachés» évite complètement les balises d'interactivité, en utilisant sur Style. Je ne sais pas si c'est simplement parce que c'est une technique plus datée, ou si cela confère encore des avantages là où on devrait la préférer dans certains scénarios.


2
Ce n'est pas un comportement Blend, c'est un "comportement" via une simple propriété attachée.
Stephen Drew

0

J'aime l'approche montrée par les réponses de Roman Dvoskin et Jonathan Allen dans ce fil. Quand j'ai appris cette technique pour la première fois, j'ai bénéficié de ce billet de blog qui fournit plus d'explications sur la technique. Et pour tout voir en contexte, voici le code source complet de la classe dont l'auteur parle dans son article de blog.


0

Déclarez le comportement individuel / le déclencheur en tant que ressources:

<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érez-les dans la collection:

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

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

</Button>

4
Comment répond-il au PO? Le déclencheur n'est pas ajouté via un style dans votre réponse.
Kryptos le

0

Sur la base de cette réponse, j'ai créé une solution plus simple, avec une seule classe nécessaire et il n'est pas nécessaire d'implémenter autre chose dans vos comportements.

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
}

et l'utilisation de l'échantillon est

<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'oubliez pas d'ajouter ce xmlns pour utiliser ArrayList:

xmlns:collections="clr-namespace:System.Collections;assembly=mscorlib"
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.