Xamarin.Forms Convertitori di valori di associazione

In genere i data binding trasferiscono i dati da una proprietà di origine a una proprietà di destinazione e in alcuni casi dalla proprietà di destinazione alla proprietà di origine. Si tratta di un semplice trasferimento quando la proprietà di origine e quella di destinazione sono dello stesso tipo o quando un tipo può essere convertito nell'altro tipo tramite una conversione implicita. Se non è questo il caso, è necessaria una conversione del tipo.

Nell'articolo Formattazione delle stringhe è stato illustrato come usare la proprietà StringFormat di un data binding per convertire un tipo qualsiasi in una stringa. Per altri tipi di conversioni, è necessario scrivere codice specializzato in una classe che implementa l'interfaccia IValueConverter. Il piattaforma UWP (Universal Windows Platform) contiene una classe simile denominata IValueConverter nello Windows.UI.Xaml.Data spazio dei nomi , ma si trova IValueConverter nello spazio dei Xamarin.Forms nomi . Le classi che implementano IValueConverter sono denominate convertitori di valori, ma vengono spesso definite convertitori di associazioni o convertitori di valori di associazione.

Interfaccia di IValueConverter

Si supponga di voler definire un data binding in cui la proprietà di origine è di tipo int mentre la proprietà di destinazione è di tipo bool. Questo data binding deve generare un valore false quando l'origine del numero intero è uguale a 0; in caso contrario, deve generare true.

A tal proposito, è necessaria una classe che implementa l'interfaccia IValueConverter:

public class IntToBoolConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return (int)value != 0;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return (bool)value ? 1 : 0;
    }
}

Impostare un'istanza di questa classe per la proprietà Converter della classe Binding o per la proprietà Converter dell'estensione di markup Binding. Questa classe diventa parte dal data binding.

Il metodo Convert viene chiamato quando i dati vengono trasferiti dall'origine alla destinazione in binding OneWay o TwoWay. Il parametro value rappresenta l'oggetto o il valore ricevuto dall'origine del data binding. Il metodo deve restituire un valore del tipo della destinazione del data binding. Il metodo illustrato di seguito esegue il cast del parametro value per un valore int e lo confronta con 0 per un valore restituito bool.

Il metodo ConvertBack viene chiamato quando i dati vengono trasferiti dalla destinazione all'origine in binding TwoWay o OneWayToSource. Il metodo ConvertBack esegue la conversione inversa. Suppone che il parametro value sia un tipo bool della destinazione e lo converte in valore restituito int per l'origine.

Se il data binding include anche un'impostazione StringFormat, il convertitore di valori viene richiamato prima che il risultato sia formattato come stringa.

La pagina Abilita pulsanti nell'esempio illustra come usare questo convertitore di valori in un data binding. Viene creata un'istanza di IntToBoolConverter nel dizionario risorse della pagina, a cui si fa riferimento con un'estensione di markup StaticResource per impostare la proprietà Converter nei due data binding. È prassi molto comune condividere i convertitori i dati tra più data binding nella pagina:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:DataBindingDemos"
             x:Class="DataBindingDemos.EnableButtonsPage"
             Title="Enable Buttons">
    <ContentPage.Resources>
        <ResourceDictionary>
            <local:IntToBoolConverter x:Key="intToBool" />
        </ResourceDictionary>
    </ContentPage.Resources>

    <StackLayout Padding="10, 0">
        <Entry x:Name="entry1"
               Text=""
               Placeholder="enter search term"
               VerticalOptions="CenterAndExpand" />

        <Button Text="Search"
                HorizontalOptions="Center"
                VerticalOptions="CenterAndExpand"
                IsEnabled="{Binding Source={x:Reference entry1},
                                    Path=Text.Length,
                                    Converter={StaticResource intToBool}}" />

        <Entry x:Name="entry2"
               Text=""
               Placeholder="enter destination"
               VerticalOptions="CenterAndExpand" />

        <Button Text="Submit"
                HorizontalOptions="Center"
                VerticalOptions="CenterAndExpand"
                IsEnabled="{Binding Source={x:Reference entry2},
                                    Path=Text.Length,
                                    Converter={StaticResource intToBool}}" />
    </StackLayout>
</ContentPage>

Se un convertitore di valori viene usato in più pagine dell'applicazione, è possibile crearne un'istanza nel dizionario risorse all'interno del file App.xaml.

La pagina Enable Buttons (Abilita pulsanti) illustra un'esigenza comune che si verifica quando Button esegue un'operazione sulla base del teso digitato dall'utente nella visualizzazione Entry. Se in Entry non viene digitato nulla, Button sarà disabilitato. Ogni oggetto Button contiene un data binding nella relativa proprietà IsEnabled. L'origine del data binding è la proprietà Length della proprietà Text dell'oggetto Entry corrispondente. Se la proprietà Length non è 0, il convertitore di valori restituisce true e Button viene abilitato:

Abilita pulsanti

Si noti che la proprietà Text in ogni oggetto Entry viene inizializzata per una stringa vuota. La proprietà Text è null per impostazione predefinita. In questo caso, il data binding non funzionerà.

Alcuni convertitori di valori vengono scritti specificatamente per determinate applicazioni, mentre altri hanno un uso più generico. Se si sa che un convertitore di valori non viene usato nei binding OneWay, il metodo ConvertBack potrà restituire soltanto null.

Il metodo Convert illustrato in precedenza presuppone in modo implicito che l'argomento value sia di tipo int e il valore restituito debba essere di tipo bool. Analogamente, il metodo ConvertBack presuppone che l'argomento value è di tipo bool e il valore restituito è int. Se non è questo il caso, verrà generata un'eccezione in fase di esecuzione.

È possibile scrivere convertitori di valori in modo che siano più generici e accettino diversi tipi di dati. I metodi Convert e ConvertBack possono usare gli operatori as e is con il parametro value oppure possono chiamare GetType per tale parametro al fine di determinarne il tipo e procedere nel modo appropriato. Il tipo previsto di ogni valore restituito del metodo viene specificato dal parametro targetType. In alcuni casi, i convertitori di valori vengono usati con i data binding di tipi diversi di destinazione. Il convertitore di valori può usare l'argomento targetType per convertire nel tipo corretto.

Se la conversione eseguita varia a seconda delle impostazioni cultura, usare il parametro culture. L'argomento parameter per Convert e ConvertBack viene illustrato più avanti in questo articolo.

Proprietà del convertitore di data binding

Le classi del convertitore di valori possono avere proprietà e parametri generici. Questo convertire di valori specifico converte un valore bool dell'origine in un oggetto di tipo T per la destinazione:

public class BoolToObjectConverter<T> : IValueConverter
{
    public T TrueObject { set; get; }

    public T FalseObject { set; get; }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return (bool)value ? TrueObject : FalseObject;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return ((T)value).Equals(TrueObject);
    }
}

La pagina Switch Indicators (Cambia indicatori) illustra come può essere usato per visualizzare il valore di una visualizzazione Switch. Generalmente si crea un'istanza dei convertitori di valori come risorse in un dizionario risorse. Questa pagina illustra un'alternativa: viene creata un'istanza di ogni convertitore di valori tra tag proprietà-elemento Binding.Converter. x:TypeArguments indica l'argomento generico. TrueObject e FalseObject sono entrambi impostati su oggetti di quel tipo:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:DataBindingDemos"
             x:Class="DataBindingDemos.SwitchIndicatorsPage"
             Title="Switch Indicators">
    <ContentPage.Resources>
        <ResourceDictionary>
            <Style TargetType="Label">
                <Setter Property="FontSize" Value="18" />
                <Setter Property="VerticalOptions" Value="Center" />
            </Style>

            <Style TargetType="Switch">
                <Setter Property="VerticalOptions" Value="Center" />
            </Style>
        </ResourceDictionary>
    </ContentPage.Resources>

    <StackLayout Padding="10, 0">
        <StackLayout Orientation="Horizontal"
                     VerticalOptions="CenterAndExpand">
            <Label Text="Subscribe?" />
            <Switch x:Name="switch1" />
            <Label>
                <Label.Text>
                    <Binding Source="{x:Reference switch1}"
                             Path="IsToggled">
                        <Binding.Converter>
                            <local:BoolToObjectConverter x:TypeArguments="x:String"
                                                         TrueObject="Of course!"
                                                         FalseObject="No way!" />
                        </Binding.Converter>
                    </Binding>
                </Label.Text>
            </Label>
        </StackLayout>

        <StackLayout Orientation="Horizontal"
                     VerticalOptions="CenterAndExpand">
            <Label Text="Allow popups?" />
            <Switch x:Name="switch2" />
            <Label>
                <Label.Text>
                    <Binding Source="{x:Reference switch2}"
                             Path="IsToggled">
                        <Binding.Converter>
                            <local:BoolToObjectConverter x:TypeArguments="x:String"
                                                         TrueObject="Yes"
                                                         FalseObject="No" />
                        </Binding.Converter>
                    </Binding>
                </Label.Text>
                <Label.TextColor>
                    <Binding Source="{x:Reference switch2}"
                             Path="IsToggled">
                        <Binding.Converter>
                            <local:BoolToObjectConverter x:TypeArguments="Color"
                                                         TrueObject="Green"
                                                         FalseObject="Red" />
                        </Binding.Converter>
                    </Binding>
                </Label.TextColor>
            </Label>
        </StackLayout>

        <StackLayout Orientation="Horizontal"
                     VerticalOptions="CenterAndExpand">
            <Label Text="Learn more?" />
            <Switch x:Name="switch3" />
            <Label FontSize="18"
                   VerticalOptions="Center">
                <Label.Style>
                    <Binding Source="{x:Reference switch3}"
                             Path="IsToggled">
                        <Binding.Converter>
                            <local:BoolToObjectConverter x:TypeArguments="Style">
                                <local:BoolToObjectConverter.TrueObject>
                                    <Style TargetType="Label">
                                        <Setter Property="Text" Value="Indubitably!" />
                                        <Setter Property="FontAttributes" Value="Italic, Bold" />
                                        <Setter Property="TextColor" Value="Green" />
                                    </Style>                                    
                                </local:BoolToObjectConverter.TrueObject>

                                <local:BoolToObjectConverter.FalseObject>
                                    <Style TargetType="Label">
                                        <Setter Property="Text" Value="Maybe later" />
                                        <Setter Property="FontAttributes" Value="None" />
                                        <Setter Property="TextColor" Value="Red" />
                                    </Style>
                                </local:BoolToObjectConverter.FalseObject>
                            </local:BoolToObjectConverter>
                        </Binding.Converter>
                    </Binding>
                </Label.Style>
            </Label>
        </StackLayout>
    </StackLayout>
</ContentPage>

Nell'ultima delle tre coppie Switch e Label l'argomento generico è impostato su Style, mentre gli oggetti Style interi vengono specificati per i valori di TrueObject e FalseObject. Questi eseguono l'override dello stile implicito per Label impostato nel dizionario risorse. Le proprietà in tale stile vengono quindi assegnate in modo esplicito all'oggetto Label. L'attivazione/disattivazione di Switch determina la modifica dell'oggetto Label corrispondente:

Indicatori switch

È anche possibile usare Triggers per implementare modifiche simili nell'interfaccia utente basata su altre visualizzazioni.

Parametri del convertitore di data binding

La classe Binding definisce una proprietà ConverterParameter. Anche l'estensione di markup Binding definisce una proprietà ConverterParameter. Se si imposta questa proprietà, il valore viene passato ai metodi Convert e ConvertBack come argomento parameter. Sebbene l'istanza del convertitore di valori sia condivisa tra diversi data binding, la proprietà ConverterParameter può essere diversa in modo da eseguire conversioni leggermente diverse.

L'uso di ConverterParameter viene illustrato con un programma di selezione a colori. In questo caso RgbColorViewModel contiene tre proprietà di tipo double denominate Red, Green, e Blue che vengono usate per costruire un valore Color:

public class RgbColorViewModel : INotifyPropertyChanged
{
    Color color;
    string name;

    public event PropertyChangedEventHandler PropertyChanged;

    public double Red
    {
        set
        {
            if (color.R != value)
            {
                Color = new Color(value, color.G, color.B);
            }
        }
        get
        {
            return color.R;
        }
    }

    public double Green
    {
        set
        {
            if (color.G != value)
            {
                Color = new Color(color.R, value, color.B);
            }
        }
        get
        {
            return color.G;
        }
    }

    public double Blue
    {
        set
        {
            if (color.B != value)
            {
                Color = new Color(color.R, color.G, value);
            }
        }
        get
        {
            return color.B;
        }
    }

    public Color Color
    {
        set
        {
            if (color != value)
            {
                color = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Red"));
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Green"));
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Blue"));
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Color"));

                Name = NamedColor.GetNearestColorName(color);
            }
        }
        get
        {
            return color;
        }
    }

    public string Name
    {
        private set
        {
            if (name != value)
            {
                name = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Name"));
            }
        }
        get
        {
            return name;
        }
    }
}

Le proprietà Red, Green e Blue sono comprese in un intervallo tra 0 e 1. Potrebbe tuttavia essere preferibile visualizzare i componenti come valori esadecimali a due cifre.

Per visualizzare queste informazioni come valori esadecimali in XAML, moltiplicare i valori per 255, convertirli in un numero intero e formattarli usando una specifica di "X2" nella proprietà StringFormat. Le prime due attività, vale a dire la moltiplicazione per 255 e la conversione in un numero intero possono essere gestite dal convertitore di valori. Perché il convertitore di valori sia il più possibile generico, è possibile specificare il fattore di moltiplicazione con la proprietà ConverterParameter. In questo modo i metodi Convert e ConvertBack vengono immessi come argomento parameter:

public class DoubleToIntConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return (int)Math.Round((double)value * GetParameter(parameter));
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return (int)value / GetParameter(parameter);
    }

    double GetParameter(object parameter)
    {
        if (parameter is double)
            return (double)parameter;

        else if (parameter is int)
            return (int)parameter;

        else if (parameter is string)
            return double.Parse((string)parameter);

        return 1;
    }
}

Convert converte da double in int moltiplicando per il valore parameter. ConvertBack divide l'argomento value del numero interno per parameter e restituisce un risultato double. Nel programma illustrato di seguito il convertitore di valori viene usato soltanto unitamente alla formattazione della stringa. ConvertBack non viene pertanto usato.

Il tipo dell'argomento parameter sarà probabilmente diverso a seconda del fatto che il data binding sia definito nel codice o in XAML. Se la proprietà ConverterParameter di Binding è impostata nel codice, è probabile che sia impostato un valore numerico:

binding.ConverterParameter = 255;

La proprietà ConverterParameter è di tipo Object, pertanto il compilatore C# interpreta il valore letterale 255 come numero intero e imposta la proprietà su tale valore.

In XAML invece ConverterParameter è probabile che venga impostato nel modo seguente:

<Label Text="{Binding Red,
                      Converter={StaticResource doubleToInt},
                      ConverterParameter=255,
                      StringFormat='Red = {0:X2}'}" />

255 appare come numero, ma dal momento che ConverterParameter è di tipo Object, il parser XAML considera 255 come stringa.

Per questo motivo, il convertitore di valori illustrato in precedenza include un metodo GetParameter distinto che gestisce i casi di parameter quando è di tipo double, int o string.

La pagina RGB Color Selector (Selettore colori RGB) crea un'istanza DoubleToIntConverter nel rispettivo dizionario risorse seguendo la definizione di due stili impliciti:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:DataBindingDemos"
             x:Class="DataBindingDemos.RgbColorSelectorPage"
             Title="RGB Color Selector">
    <ContentPage.Resources>
        <ResourceDictionary>
            <Style TargetType="Slider">
                <Setter Property="VerticalOptions" Value="CenterAndExpand" />
            </Style>

            <Style TargetType="Label">
                <Setter Property="HorizontalTextAlignment" Value="Center" />
            </Style>

            <local:DoubleToIntConverter x:Key="doubleToInt" />
        </ResourceDictionary>
    </ContentPage.Resources>

    <StackLayout>
        <StackLayout.BindingContext>
            <local:RgbColorViewModel Color="Gray" />
        </StackLayout.BindingContext>

        <BoxView Color="{Binding Color}"
                 VerticalOptions="FillAndExpand" />

        <StackLayout Margin="10, 0">
            <Label Text="{Binding Name}" />

            <Slider Value="{Binding Red}" />
            <Label Text="{Binding Red,
                                  Converter={StaticResource doubleToInt},
                                  ConverterParameter=255,
                                  StringFormat='Red = {0:X2}'}" />

            <Slider Value="{Binding Green}" />
            <Label Text="{Binding Green,
                                  Converter={StaticResource doubleToInt},
                                  ConverterParameter=255,
                                  StringFormat='Green = {0:X2}'}" />

            <Slider Value="{Binding Blue}" />
            <Label>
                <Label.Text>
                    <Binding Path="Blue"
                             StringFormat="Blue = {0:X2}"
                             Converter="{StaticResource doubleToInt}">
                        <Binding.ConverterParameter>
                            <x:Double>255</x:Double>
                        </Binding.ConverterParameter>
                    </Binding>
                </Label.Text>
            </Label>
        </StackLayout>
    </StackLayout>
</ContentPage>    

I valori delle proprietà Red e Green vengono visualizzati con un'estensione di markup Binding. La proprietà Blue, tuttavia, crea un'istanza della classe Binding per illustrare come impostare un valore double esplicito per la proprietà ConverterParameter.

Il risultato è il seguente:

Selettore colori RGB