[wp7] Decoro Urbano: RadioButton personalizzati

La pagina di invio delle segnalazioni è sicuramente la funzionalità più importante dell’applicazione Decoro Urbano, e ho dedicato parecchio tempo allo studio della User Interface. In particolare, ho esplorato diverse possibilità per consentire all’utente di scegliere il tipo di segnalazione. Alla fine ho optato per una soluzione all’apparenza banale, ovvero l’uso di controlli di tipo RadioButton con uno style personalizzato, che partendo da quello del controllo Button (cornice e contenuto) aggiunge un background (di colore pari al tema utente) al RadioButton che è “checked”:

image

Per chi è interessato, questo è lo XAML:

<Style x:Key="RadioButtonTileStyle" TargetType="RadioButton">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource PhoneForegroundBrush}"/>
<Setter Property="Foreground" Value="{StaticResource PhoneForegroundBrush}"/>
<Setter Property="BorderThickness" Value="{StaticResource PhoneBorderThickness}"/>
<Setter Property="FontFamily" Value="{StaticResource PhoneFontFamilySemiBold}"/>
<Setter Property="FontSize" Value="{StaticResource PhoneFontSizeMediumLarge}"/>
<Setter Property="Padding" Value="0,3,0,0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RadioButton">
<Grid Background="Transparent">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="MouseOver"/>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Fill" Storyboard.TargetName="RectCommonState">
<DiscreteObjectKeyFrame KeyTime="0" Value="Red"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled"/>
</VisualStateGroup>
<VisualStateGroup x:Name="CheckStates">
<VisualState x:Name="Unchecked"/>
<VisualState x:Name="Indeterminate"/>
<VisualState x:Name="Checked">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="Button">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneAccentBrush}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="Button">
<DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneAccentBrush}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid Background="Transparent" Margin="{StaticResource PhoneTouchTargetOverhang}">
<Border x:Name="Button" Background="Transparent" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="0">
<ContentControl x:Name="ContentContainerEnabled" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Foreground="{TemplateBinding Foreground}" FontSize="{TemplateBinding FontSize}" FontFamily="{TemplateBinding FontFamily}" Padding="{TemplateBinding Padding}" Margin="{TemplateBinding Margin}" HorizontalContentAlignment="{TemplateBinding HorizontalAlignment}" VerticalContentAlignment="{TemplateBinding VerticalAlignment}"/>
</Border>
<Canvas x:Name="CanvasCommonState">
<Rectangle x:Name="RectCommonState" Width="{Binding ActualWidth, ElementName=CanvasCommonState}" Height="{Binding ActualHeight, ElementName=CanvasCommonState}" Fill="{StaticResource PhoneContrastBackgroundBrush}" />
</Canvas>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

Ovviamente anche in questo caso, come nel precedente post, abbiamo la necessità di scegliere il contenuto dei radio bottoni in relazione al tipo di tema (chiaro o scuro). Utilizzeremo dunque lo stesso metodo, facendo attenzione al fatto che ora il Converter deve restituire una ImageSource e non già un ImageBrush, come occorreva fare nel caso della proprietà Background. Per non fare due Converter diversi, ho esteso quello già usato nel precedente post, in modo che restituisca il valore opportuno, a seconda del targetType:

public class ImageSourceConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (targetType != typeof(Brush) && targetType != typeof(ImageSource))
{
throw new NotImplementedException("Target must be of type Brush or of type ImageSource");
}
else
{
if ((value.GetType() != typeof(string)) && (value.GetType() != typeof(Uri)))
{
throw new NotImplementedException("Value must be of type string or Uri");
}
else
{
BitmapImage bitmapImage;
if (value is string)
{
string str = (string)value;
bitmapImage = new BitmapImage(new Uri(str, UriKind.RelativeOrAbsolute));
}
else
{
Uri uri = (Uri)value;
bitmapImage = new BitmapImage(uri);
}
if (targetType == typeof(ImageSource))
{
return bitmapImage;
}
else
{
return new ImageBrush() { ImageSource = bitmapImage };
}
}
}
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}

Sempre seguendo lo stesso metodo, abbiamo delle proprietà nel ViewModel che restituiscono il path dell’immagine adatta al tema corrente, una per ciascun RadioButton (qui è mostrata solo la prima):

public string ThemeAwareRifiutiImagePath
{
get
{
return Utils.LightThemeEnabled ? "Images/RifiutiTile_light.png" : "Images/RifiutiTile.png";
}
}

E finalmente abbiamo una prima versione di XAML relativo al RadioButton:

<RadioButton x:Name="Rifiuti" Grid.Column="0" Style="{StaticResource RadioButtonTileStyle}" >
<Image Source="{Binding Path=ThemeAwareRifiutiImagePath, Converter={StaticResource ImageSourceConverter}}" />
</RadioButton>

E fin qui, ci siamo.

Ora però bisogna collegare la proprietà “IsChecked” dei vari RadioButton alla proprietà ReportTypeID del ViewModel. Come fare? Ovviamente ci sono diversi modi, ma se vogliamo approfittare del fatto che stiamo usando radio bottoni che quindi gestiscono automaticamente lo stato di checked in modo mutuamente esclusivo, dobbiamo evitare di usare code behind, eventi “checked” e quant’altro.

La soluzione più semplice è stata quella di usare anche qui un Converter, sfruttando il ConverterParameter. In pratica, faccio il binding di tutti i radio bottoni alla proprietà ReportTypeID del ViewModel, ma convertendola al valore booleano (vero o falso) ottenuto dal confronto del valore di tale proprietà con il valore che corrisponde al singolo radio bottone, definito nel ConverterParameter. Ad esempio, poiché al RadioButton della tipologia “Rifiuti” corrisponde per convenzione il valore “1”, tale valore viene passato al Converter per mezzo del ConverterParameter. Ogni qualvolta il valore del ReportTypeID è proprio pari a “1”, tale confronto da esito positivo e la proprietà “IsChecked” di tale RadioButton viene settata a True.

Da notare che in questo caso il Converter funziona in entrambe le direzioni di binding, di modo che il cambiamento delle proprietà “IsChecked” dei vari RadioButton si riflette automaticamente nell’aggiornamento della proprietà ReportTypeID del ViewModel:

public class ReportTypeToBoolConverter : IValueConverter
{

public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value.GetType() != typeof(string))
{
throw new NotImplementedException("Value must be of type string");
}
else
{
if (parameter.GetType() != typeof(string))
{
throw new NotImplementedException("Parameter must be of type string");
}
else
{
return (((string)value) == ((string)parameter));

}
}
}

public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (parameter.GetType() != typeof(string))
{
throw new NotImplementedException("Parameter must be of type string");
}
else
{
return (string)parameter;
}
}

}

Il cambio della proprietà ReportTypeID  notifica anche il cambiamento di un’altra proprietà, che viene usata per mostrare la descrizione del tipo di segnalazione, ovvero ReportTypeDesc:

private string reportTypeID;
public string ReportTypeID
{
get
{
return reportTypeID;
}
set
{
if (reportTypeID != value)
{
reportTypeID = value;
NotifyPropertyChanged("ReportTypeID");
NotifyPropertyChanged("ReportTypeDesc");
}
}
}

public string ReportTypeDesc
{
get
{
return (reportTypeID == "1") ? "RIFIUTI" :
(reportTypeID == "2") ? "VANDALISMO/INCURIA" :
(reportTypeID == "3") ? "DISSESTO STRADALE" :
(reportTypeID == "4") ? "ZONE VERDI" :
(reportTypeID == "5") ? "SEGNALETICA" :
(reportTypeID == "6") ? "AFFISSIONI ABUSIVE" : "";
}
}

Ora possiamo mostrare lo XAML completo dei RadioButton:

<!-- Report type choice -->
<Grid Grid.Row="0" Margin="-10,0,-10,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>

<RadioButton x:Name="Rifiuti" Grid.Column="0" Style="{StaticResource RadioButtonTileStyle}"
IsChecked="{Binding Path=ReportTypeID,
Converter={StaticResource ReportTypeToBoolConverter},
ConverterParameter=1, Mode=TwoWay}"
>
<Image Source="{Binding Path=ThemeAwareRifiutiImagePath, Converter={StaticResource ImageSourceConverter}}" />
</RadioButton>

<RadioButton x:Name="VandalismoIncuria" Grid.Column="1" Style="{StaticResource RadioButtonTileStyle}"
IsChecked="{Binding Path=ReportTypeID,
Converter={StaticResource ReportTypeToBoolConverter},
ConverterParameter=2, Mode=TwoWay}"
>
<Image Source="{Binding Path=ThemeAwareVandalismoIncuriaImagePath, Converter={StaticResource ImageSourceConverter}}" />
</RadioButton>

<RadioButton x:Name="DissestoStradale" Grid.Column="2" Style="{StaticResource RadioButtonTileStyle}"
IsChecked="{Binding Path=ReportTypeID,
Converter={StaticResource ReportTypeToBoolConverter},
ConverterParameter=3, Mode=TwoWay}"
>
<Image Source="{Binding Path=ThemeAwareDissestoStradaleImagePath, Converter={StaticResource ImageSourceConverter}}" />
</RadioButton>

<RadioButton x:Name="ZoneVerdi" Grid.Column="3" Style="{StaticResource RadioButtonTileStyle}"
IsChecked="{Binding Path=ReportTypeID,
Converter={StaticResource ReportTypeToBoolConverter},
ConverterParameter=4, Mode=TwoWay}"
>
<Image Source="{Binding Path=ThemeAwareZoneVerdiImagePath, Converter={StaticResource ImageSourceConverter}}" />
</RadioButton>

<RadioButton x:Name="Segnaletica" Grid.Column="4" Style="{StaticResource RadioButtonTileStyle}"
IsChecked="{Binding Path=ReportTypeID,
Converter={StaticResource ReportTypeToBoolConverter},
ConverterParameter=5, Mode=TwoWay}"
>
<Image Source="{Binding Path=ThemeAwareSegnaleticaImagePath, Converter={StaticResource ImageSourceConverter}}" />
</RadioButton>

<RadioButton x:Name="AffissioniAbusive" Grid.Column="5" Style="{StaticResource RadioButtonTileStyle}"
IsChecked="{Binding Path=ReportTypeID,
Converter={StaticResource ReportTypeToBoolConverter},
ConverterParameter=6, Mode=TwoWay}"
>
<Image Source="{Binding Path=ThemeAwareAffissioniAbusiveImagePath, Converter={StaticResource ImageSourceConverter}}" />
</RadioButton>
</Grid>

Di seguito alcune immagini della pagina, dov’è possibile notare l’adattamento di tutti gli elementi grafici al tema corrente:

Screen1    Screen2

Screen3   Screen4   Screen5

That’s all folks!

«aprile»
domlunmarmergiovensab
25262728293031
1234567
891011121314
15161718192021
22232425262728
293012345