web-dev-qa-db-fra.com

MVVM: Lier des boutons radio à un modèle de vue?

EDIT: Le problème a été corrigé dans .NET 4.0.

J'ai essayé de lier un groupe de boutons radio à un modèle de vue à l'aide du bouton IsChecked. Après avoir examiné d'autres publications, il apparaît que la propriété IsChecked ne fonctionne tout simplement pas. J'ai monté une courte démonstration qui reproduit le problème, que j'ai inclus ci-dessous.

Voici ma question: Existe-t-il un moyen simple et fiable de lier des boutons radio à l'aide de MVVM? Merci.

Informations complémentaires: La propriété IsChecked ne fonctionne pas pour deux raisons:

  1. Lorsqu'un bouton est sélectionné, les propriétés IsChecked des autres boutons du groupe ne sont pas définies sur false.

  2. Lorsqu'un bouton est sélectionné, sa propre propriété IsChecked n'est pas définie après la première sélection du bouton. Je devine que la liaison est mise au rebut par WPF au premier clic.

Projet de démonstration: Voici le code et le balisage d’une démonstration simple qui reproduit le problème. Créez un projet WPF et remplacez le balisage dans Window1.xaml par ce qui suit:

<Window x:Class="WpfApplication1.Window1"
    xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300" Loaded="Window_Loaded">
    <StackPanel>
        <RadioButton Content="Button A" IsChecked="{Binding Path=ButtonAIsChecked, Mode=TwoWay}" />
        <RadioButton Content="Button B" IsChecked="{Binding Path=ButtonBIsChecked, Mode=TwoWay}" />
    </StackPanel>
</Window>

Remplacez le code dans Window1.xaml.cs par le code suivant (un hack), qui définit le modèle de vue:

using System.Windows;

namespace WpfApplication1
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            this.DataContext = new Window1ViewModel();
        }
    }
}

Ajoutez maintenant le code suivant au projet en tant que Window1ViewModel.cs:

using System.Windows;

namespace WpfApplication1
{
    public class Window1ViewModel
    {
        private bool p_ButtonAIsChecked;

        /// <summary>
        /// Summary
        /// </summary>
        public bool ButtonAIsChecked
        {
            get { return p_ButtonAIsChecked; }
            set
            {
                p_ButtonAIsChecked = value;
                MessageBox.Show(string.Format("Button A is checked: {0}", value));
            }
        }

        private bool p_ButtonBIsChecked;

        /// <summary>
        /// Summary
        /// </summary>
        public bool ButtonBIsChecked
        {
            get { return p_ButtonBIsChecked; }
            set
            {
                p_ButtonBIsChecked = value;
                MessageBox.Show(string.Format("Button B is checked: {0}", value));
            }
        }

    }
}

Pour reproduire le problème, exécutez l'application et cliquez sur le bouton A. Un message apparaîtra pour vous informer que la propriété IsChecked du bouton A a été définie sur true. Sélectionnez maintenant le bouton B. Une autre boîte de message apparaîtra, indiquant que la propriété IsChecked du bouton B a été définie sur true, mais qu'il n'y a pas de boîte de message indiquant que la propriété IsChecked du bouton A a été définie sur false-- la propriété n'a pas été modifiée.

Maintenant, cliquez à nouveau sur le bouton A. Le bouton sera sélectionné dans la fenêtre, mais aucun message ne s'affichera - la propriété IsChecked n'a pas été modifiée. Enfin, cliquez à nouveau sur le bouton B - même résultat. La propriété IsChecked n'est pas du tout mise à jour pour l'un ou l'autre des boutons après la première utilisation du bouton.

58
David Veeneman

Si vous commencez par la suggestion de Jason, le problème devient alors une simple sélection liée dans une liste qui se traduit très bien en un ListBox. À ce stade, il est simple d'appliquer un style à un contrôle ListBox pour qu'il apparaisse sous la forme d'une liste RadioButton.

<ListBox ItemsSource="{Binding ...}" SelectedItem="{Binding ...}">
    <ListBox.ItemContainerStyle>
        <Style TargetType="{x:Type ListBoxItem}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ListBoxItem}">
                        <RadioButton Content="{TemplateBinding Content}"
                                     IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsSelected}"/>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </ListBox.ItemContainerStyle>
</ListBox>
52
John Bowen

On dirait qu'ils ont corrigé la liaison à la propriété IsChecked dans .NET 4. Un projet qui a été interrompu dans VS2008 fonctionne dans VS2010.

17
RandomEngy

Pour le bénéfice de ceux qui recherchent cette question plus tard, voici la solution que j'ai finalement mise en œuvre. Elle s’appuie sur la réponse de John Bowen, que j’ai choisie comme la meilleure solution au problème.

Tout d'abord, j'ai créé un style pour une zone de liste transparente contenant des boutons radio en tant qu'éléments. Ensuite, j'ai créé les boutons pour aller dans la zone de liste - mes boutons sont fixes, plutôt que lus dans l'application sous forme de données, je les ai donc codés en dur dans le balisage.

J'utilise une énumération appelée ListButtons dans le modèle d'affichage pour représenter les boutons de la zone de liste et j'utilise la propriété Tag de chaque bouton pour transmettre une valeur de chaîne de la valeur enum à utiliser pour ce bouton. La propriété ListBox.SelectedValuePath me permet de spécifier la propriété Tag en tant que source de la valeur sélectionnée, que je lie au modèle de vue à l'aide de la propriété SelectedValue. Je pensais qu'il me faudrait un convertisseur de valeur pour convertir entre la chaîne et sa valeur enum, mais les convertisseurs intégrés de WPF ont géré la conversion sans problème.

Voici le balisage complet pour Window1.xaml :

<Window x:Class="RadioButtonMvvmDemo.Window1"
    xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300">

    <!-- Resources -->
    <Window.Resources>
        <Style x:Key="RadioButtonList" TargetType="{x:Type ListBox}">
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="ItemContainerStyle">
                <Setter.Value>
                    <Style TargetType="{x:Type ListBoxItem}" >
                        <Setter Property="Margin" Value="5" />
                        <Setter Property="Template">
                            <Setter.Value>
                                <ControlTemplate TargetType="{x:Type ListBoxItem}">
                                    <Border BorderThickness="0" Background="Transparent">
                                        <RadioButton 
                                            Focusable="False"
                                            IsHitTestVisible="False"
                                            IsChecked="{TemplateBinding IsSelected}">
                                            <ContentPresenter />
                                        </RadioButton>
                                    </Border>
                                </ControlTemplate>
                            </Setter.Value>
                        </Setter>
                    </Style>
                </Setter.Value>
            </Setter>
            <Setter Property="Control.Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ListBox}">
                        <Border BorderThickness="0" Padding="0" BorderBrush="Transparent" Background="Transparent" Name="Bd" SnapsToDevicePixels="True">
                            <ItemsPresenter SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>

    <!-- Layout -->
    <Grid>
        <!-- Note that we use SelectedValue, instead of SelectedItem. This allows us 
        to specify the property to take the value from, using SelectedValuePath. -->

        <ListBox Style="{StaticResource RadioButtonList}" SelectedValuePath="Tag" SelectedValue="{Binding Path=SelectedButton}">
            <ListBoxItem Tag="ButtonA">Button A</ListBoxItem>
            <ListBoxItem Tag="ButtonB">Button B</ListBoxItem>
        </ListBox>
    </Grid>
</Window>

Le modèle de vue possède une propriété unique, SelectedButton, qui utilise une énumération ListButtons pour indiquer quel bouton est sélectionné. La propriété appelle un événement de la classe de base que j'utilise pour les modèles de vue, ce qui déclenche l'événement PropertyChanged:

namespace RadioButtonMvvmDemo
{
    public enum ListButtons {ButtonA, ButtonB}

    public class Window1ViewModel : ViewModelBase
    {
        private ListButtons p_SelectedButton;

        public Window1ViewModel()
        {
            SelectedButton = ListButtons.ButtonB;
        }

        /// <summary>
        /// The button selected by the user.
        /// </summary>
        public ListButtons SelectedButton
        {
            get { return p_SelectedButton; }

            set
            {
                p_SelectedButton = value;
                base.RaisePropertyChangedEvent("SelectedButton");
            }
        }

    }
} 

Dans mon application de production, le variable SelectedButton appellera une méthode de classe de service qui effectuera l'action requise lorsqu'un bouton est sélectionné.

Et pour être complet, voici la classe de base:

using System.ComponentModel;

namespace RadioButtonMvvmDemo
{
    public abstract class ViewModelBase : INotifyPropertyChanged
    {
        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region Protected Methods

        /// <summary>
        /// Raises the PropertyChanged event.
        /// </summary>
        /// <param name="propertyName">The name of the changed property.</param>
        protected void RaisePropertyChangedEvent(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);
                PropertyChanged(this, e);
            }
        }

        #endregion
    }
}

J'espère que cela pourra aider!

9
David Veeneman

Une solution consiste à mettre à jour le ViewModel pour les boutons radio dans le configurateur des propriétés. Lorsque le bouton A est défini sur True, définissez le bouton B sur false.

Un autre facteur important lors de la liaison à un objet dans le DataContext est que l'objet doit implémenter INotifyPropertyChanged. Lorsqu'une propriété liée change, l'événement doit être déclenché et inclure le nom de la propriété modifiée. (Contrôle nul omis dans l'échantillon par souci de concision.)

public class ViewModel  : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected bool _ButtonAChecked = true;
    public bool ButtonAChecked
    {
        get { return _ButtonAChecked; }
        set 
        { 
            _ButtonAChecked = value;
            PropertyChanged(this, new PropertyChangedEventArgs("ButtonAChecked"));
            if (value) ButtonBChecked = false;
        }
    }

    protected bool _ButtonBChecked;
    public bool ButtonBChecked
    {
        get { return _ButtonBChecked; }
        set 
        { 
            _ButtonBChecked = value; 
            PropertyChanged(this, new PropertyChangedEventArgs("ButtonBChecked"));
            if (value) ButtonAChecked = false;
        }
    }
}

Modifier:

Le problème est que lorsque vous cliquez pour la première fois sur le bouton B, la valeur IsChecked change et que la liaison passe, mais le bouton A ne passe pas par son état non contrôlé à la propriété ButtonAChecked. En mettant à jour manuellement dans le code le configurateur de propriétés ButtonAChecked sera appelé lors du prochain clic sur le bouton A.

3
Doug Ferguson

Voici une autre façon de le faire 

VUE:

<StackPanel Margin="90,328,965,389" Orientation="Horizontal">
        <RadioButton Content="Mr" Command="{Binding TitleCommand, Mode=TwoWay}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Mode=Self}, Mode=TwoWay}" GroupName="Title"/>
        <RadioButton Content="Mrs" Command="{Binding TitleCommand, Mode=TwoWay}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Mode=Self}, Mode=TwoWay}" GroupName="Title"/>
        <RadioButton Content="Ms" Command="{Binding TitleCommand, Mode=TwoWay}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Mode=Self}, Mode=TwoWay}" GroupName="Title"/>
        <RadioButton Content="Other" Command="{Binding TitleCommand, Mode=TwoWay}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Mode=Self}}" GroupName="Title"/>
        <TextBlock Text="{Binding SelectedTitle, Mode=TwoWay}"/>
    </StackPanel>

ViewModel:

 private string selectedTitle;
    public string SelectedTitle
    {
        get { return selectedTitle; }
        set
        {
            SetProperty(ref selectedTitle, value);
        }
    }

    public RelayCommand TitleCommand
    {
        get
        {
            return new RelayCommand((p) =>
            {
                selectedTitle = (string)p;
            });
        }
    }
2
Enkosi

Pas sûr des bogues IsChecked, vous pouvez apporter un refactor à votre modèle de vue: la vue comporte plusieurs états mutuellement exclusifs représentés par une série de boutons radio, dont un seul peut être sélectionné à tout moment. Dans le modèle de vue, il suffit d'avoir 1 propriété (par exemple une énumération) qui représente les états possibles: stateA, stateB, etc. Ainsi, vous n'avez pas besoin de tous les ButtonAIsChecked individuels, etc. 

2
Jason Roberts

Une petite extension de answer : John Bowen - Cela ne fonctionne pas lorsque les valeurs ne mettent pas en œuvre ToString(). Au lieu de définir la Content de RadioButton sur un TemplateBinding, insérez simplement une ContentPresenter dans celui-ci, comme ceci:

<ListBox ItemsSource="{Binding ...}" SelectedItem="{Binding ...}">
    <ListBox.ItemContainerStyle>
        <Style TargetType="{x:Type ListBoxItem}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ListBoxItem}">
                        <RadioButton IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsSelected}">
                            <ContentPresenter/>
                        </RadioButton>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </ListBox.ItemContainerStyle>
</ListBox>

De cette façon, vous pouvez également utiliser DisplayMemberPath ou ItemTemplate selon vos besoins. RadioButton vient juste "d'emballer" les éléments, en fournissant la sélection.

2
Joachim Mairböck

Je sais que cette question est ancienne et que le problème initial avait été résolu dans .NET 4. En toute honnêteté, il s'agit d'un sujet légèrement hors sujet. 

Dans la plupart des cas où j'ai voulu utiliser RadioButtons dans MVVM, c'est pour choisir entre les éléments d'un enum, cela nécessite de lier une propriété bool dans l'espace VM à chaque bouton et de les utiliser pour définir une propriété globale enum qui reflète la sélection réelle, cela devient très fastidieux et très rapide. J'ai donc proposé une solution réutilisable, très facile à mettre en œuvre et ne nécessitant pas de ValueConverters.

La vue est à peu près la même chose, mais une fois que vous avez votre enum en place, le côté VM peut être créé avec une seule propriété.

MainWindowVM

using System.ComponentModel;

namespace EnumSelectorTest
{
  public class MainWindowVM : INotifyPropertyChanged
  {
    public EnumSelectorVM Selector { get; set; }

    private string _colorName;
    public string ColorName
    {
      get { return _colorName; }
      set
      {
        if (_colorName == value) return;
        _colorName = value;
        RaisePropertyChanged("ColorName");
      }
    }

    public MainWindowVM()
    {
      Selector = new EnumSelectorVM
        (
          typeof(MyColors),
          MyColors.Red,
          false,
          val => ColorName = "The color is " + ((MyColors)val).ToString()
        );
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void RaisePropertyChanged(string propertyName)
    {
      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
  }
}

La classe qui effectue tout le travail hérite de DynamicObject. Vu de l'extérieur, il crée une propriété bool pour chaque élément du préfixe enum préfixé par 'Is', 'IsRed', 'IsBlue', etc. pouvant être lié à XAML. Avec une propriété Value contenant la valeur réelle enum.

public enum MyColors
{
  Red,
  Magenta,
  Green,
  Cyan,
  Blue,
  Yellow
}

EnumSelectorVM

using System;
using System.ComponentModel;
using System.Dynamic;
using System.Linq;

namespace EnumSelectorTest
{
  public class EnumSelectorVM : DynamicObject, INotifyPropertyChanged
  {
    //------------------------------------------------------------------------------------------------------------------------------------------
    #region Fields

    private readonly Action<object> _action;
    private readonly Type _enumType;
    private readonly string[] _enumNames;
    private readonly bool _notifyAll;

    #endregion Fields

    //------------------------------------------------------------------------------------------------------------------------------------------
    #region Properties

    private object _value;
    public object Value
    {
      get { return _value; }
      set
      {
        if (_value == value) return;
        _value = value;
        RaisePropertyChanged("Value");
        _action?.Invoke(_value);
      }
    }

    #endregion Properties

    //------------------------------------------------------------------------------------------------------------------------------------------
    #region Constructor

    public EnumSelectorVM(Type enumType, object initialValue, bool notifyAll = false, Action<object> action = null)
    {
      if (!enumType.IsEnum)
        throw new ArgumentException("enumType must be of Type: Enum");
      _enumType = enumType;
      _enumNames = enumType.GetEnumNames();
      _notifyAll = notifyAll;
      _action = action;

      //do last so notification fires and action is executed
      Value = initialValue;
    }

    #endregion Constructor

    //------------------------------------------------------------------------------------------------------------------------------------------
    #region Methods

    //---------------------------------------------------------------------
    #region Public Methods

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
      string elementName;
      if (!TryGetEnumElemntName(binder.Name, out elementName))
      {
        result = null;
        return false;
      }
      try
      {
        result = Value.Equals(Enum.Parse(_enumType, elementName));
      }
      catch (Exception ex) when (ex is ArgumentNullException || ex is ArgumentException || ex is OverflowException)
      {
        result = null;
        return false;
      }
      return true;
    }

    public override bool TrySetMember(SetMemberBinder binder, object newValue)
    {
      if (!(newValue is bool))
        return false;
      string elementName;
      if (!TryGetEnumElemntName(binder.Name, out elementName))
        return false;
      try
      {
        if((bool) newValue)
          Value = Enum.Parse(_enumType, elementName);
      }
      catch (Exception ex) when (ex is ArgumentNullException || ex is ArgumentException || ex is OverflowException)
      {
        return false;
      }
      if (_notifyAll)
        foreach (var name in _enumNames)
          RaisePropertyChanged("Is" + name);
      else
        RaisePropertyChanged("Is" + elementName);
      return true;
    }

    #endregion Public Methods

    //---------------------------------------------------------------------
    #region Private Methods

    private bool TryGetEnumElemntName(string bindingName, out string elementName)
    {
      elementName = "";
      if (bindingName.IndexOf("Is", StringComparison.Ordinal) != 0)
        return false;
      var name = bindingName.Remove(0, 2); // remove first 2 chars "Is"
      if (!_enumNames.Contains(name))
        return false;
      elementName = name;
      return true;
    }

    #endregion Private Methods

    #endregion Methods

    //------------------------------------------------------------------------------------------------------------------------------------------
    #region Events

    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void RaisePropertyChanged(string propertyName)
    {
      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    #endregion Events
  }
}

Pour répondre aux modifications, vous pouvez vous abonner à l'événement NotifyPropertyChanged ou transmettre une méthode anonyme au constructeur, comme indiqué ci-dessus.

Et enfin le MainWindow.xaml

<Window x:Class="EnumSelectorTest.MainWindow"
    xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.Microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Title="MainWindow" Height="350" Width="525">  
  <Grid>
    <StackPanel>
      <RadioButton IsChecked="{Binding Selector.IsRed}">Red</RadioButton>
      <RadioButton IsChecked="{Binding Selector.IsMagenta}">Magenta</RadioButton>
      <RadioButton IsChecked="{Binding Selector.IsBlue}">Blue</RadioButton>
      <RadioButton IsChecked="{Binding Selector.IsCyan}">Cyan</RadioButton>
      <RadioButton IsChecked="{Binding Selector.IsGreen}">Green</RadioButton>
      <RadioButton IsChecked="{Binding Selector.IsYellow}">Yellow</RadioButton>
      <TextBlock Text="{Binding ColorName}"/>
    </StackPanel>
  </Grid>
</Window>

J'espère que quelqu'un d'autre trouvera cela utile, parce que je pense que celles-ci vont dans ma boîte à outils.

1
Brown Bear

Vous devez ajouter le nom du groupe pour le bouton radio

   <StackPanel>
        <RadioButton Content="Button A" IsChecked="{Binding Path=ButtonAIsChecked, Mode=TwoWay}" GroupName="groupName" />
        <RadioButton Content="Button B" IsChecked="{Binding Path=ButtonBIsChecked, Mode=TwoWay}" GroupName="groupName" />
    </StackPanel>
1
ptsivakumar
<RadioButton  IsChecked="{Binding customer.isMaleFemale}">Male</RadioButton>
    <RadioButton IsChecked="{Binding customer.isMaleFemale,Converter=      {StaticResource GenderConvertor}}">Female</RadioButton>

Ci-dessous le code pour IValueConverter

public class GenderConvertor : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return !(bool)value;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return !(bool)value;
    }
}

cela a fonctionné pour moi. Même la valeur a été liée sur view et viewmodel en fonction du clic sur le bouton radio. Vrai -> Masculin et Faux -> Féminin

0
jeevan

J'ai un problème très similaire dans VS2015 et .NET 4.5.1

XAML:

                <ListView.ItemsPanel>
                    <ItemsPanelTemplate>
                        <UniformGrid Columns="6" Rows="1"/>
                    </ItemsPanelTemplate>
                </ListView.ItemsPanel>
                <ListView.ItemTemplate>
                    <DataTemplate >
                        <RadioButton  GroupName="callGroup" Style="{StaticResource itemListViewToggle}" Click="calls_ItemClick" Margin="1" IsChecked="{Binding Path=Selected,Mode=TwoWay}" Unchecked="callGroup_Checked"  Checked="callGroup_Checked">

....

Comme vous pouvez le voir dans ce code, j'ai une liste de vues et les éléments du modèle sont des boutons radio qui appartiennent à un nom de groupe.

Si j'ajoute un nouvel élément à la collection avec la propriété Selected définie sur True, il apparaît coché et le reste des boutons reste coché.

Je le résous en obtenant d'abord le bouton coché et en le réglant manuellement sur faux, mais ce n'est pas ainsi que cela est censé être fait.

code derrière:

`....
  lstInCallList.ItemsSource = ContactCallList
  AddHandler ContactCallList.CollectionChanged, AddressOf collectionInCall_change
.....
Public Sub collectionInCall_change(sender As Object, e As NotifyCollectionChangedEventArgs)
    'Whenever collection change we must test if there is no selection and autoselect first.   
    If e.Action = NotifyCollectionChangedAction.Add Then
        'The solution is this, but this shouldn't be necessary
        'Dim seleccionado As RadioButton = getCheckedRB(lstInCallList)
        'If seleccionado IsNot Nothing Then
        '    seleccionado.IsChecked = False
        'End If
        DirectCast(e.NewItems(0), PhoneCall).Selected = True
.....
End sub

`

0
Jose