Why does Xamarin.Forms databinding work for one control but not another? - xaml

I am developing a Xamarin app which I am testing on an Android device. I have a XAML view and I am binding an enum property in the viewmodel to multiple controls - one for text value, and the other to background color with an IValueConverter. Relevant XAML code:
<ContentView
BackgroundColor="{Binding MyField, Converter={StaticResource MyFieldEnumValueToColorConverter}}"
>
<ContentView.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding MyFieldClickCommand}" />
</ContentView.GestureRecognizers>
<Label
Text="{Binding MyField}"
/>
</ContentView>
IValueConverter implementation:
public class MyFieldEnumValueToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is MyFieldEnum)
{
switch ((MyFieldEnum)value)
{
case MyFieldEnum.Value1:
return Color.Orange;
case MyFieldEnum.Value2:
return Color.Green;
case MyFieldEnum.Value3:
return Color.Red;
}
}
return Color.White;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Property in the viewmodel (yes, it does implement INotifyPropertyChanged):
public event PropertyChangedEventHandler PropertyChanged;
private MyFieldEnum _myField;
public MyFieldEnum MyField
{
get => _myField;
set
{
_myField= value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MyField)));
}
}
When I load this page, the controls load properly for every distinct enum value: the text and the background color both reflect the actual value.
In a click handler, I simply set the property value like this:
MyField = MyFieldEnum.Value2;
The text changes, but the background color does not. Why?
I also tried introducing a new field (of type Color), implement the IValueConverter logic directly in the getter, and bind that to the BackgroundColor attribute of the ContentView. Same issue. The page has many other data-bound items and all work properly except this one.
UPDATE: it appears that the problem is with the ContentView. I put the exact same BackgroundColor binding to the Label itself and there it works as expected, but for the ContentView it does not.

Related

How to add : with label where text value is binding dynamically?

How I can add : with string at my label in xaml in xamarin.forms. I have a text coming from app resource file by (i18n:Translate Text=Supplier). Now with this text I also add : after this text. I don't want to add : in app resource with text. I want to do that it on xaml only. I tried with StringFormat but don't know how I can do it.
You can use a Value Converter to change the value on your Binding, but it's tricky because you can't easily add a converter while using i18n:Translate. But I still see three possible solutions to your problem:
1. Property without Value Converter
The easiest way would be to create a Property which gets the translated text and then adds a colon to your text:
ViewModel:
public string Supplier
{
get { return AppResources.Supplier + ":"; }
}
XAML:
<Label Text="{Binding Supplier}"/>
2. Property with Value converter
Another way is to create a property which gets the translated text and then add the colon via a Value Converter:
ViewModel:
public string Supplier
{
get { return AppResources.Supplier; }
}
Converter class:
public class ColonConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value += ":";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value.ToString().Remove(value.ToString().Length - 1);
}
}
XAML:
<ContentPage.Resources>
<ResourceDictionary>
<local:ColonConverter x:Key="ColonConverter" />
</ResourceDictionary>
</ContentPage.Resources>
...
<Label Text={Binding Supplier, Converter={StaticResource ColonConverter}}"/>
3. Create your own Translate Extension and Value Converter
I didn't test this, but I found this SO answer which gives an example on how to achieve that. This way you don't need to add properties to your ViewModel, so you only have to adjust your XAML once you set up the Translate Extension and Converter. But it's needs some work to write your own Translate Extension.
Custom TranslateExtension:
[ContentProperty("Text")]
public class TranslateExtension : IMarkupExtension
{
const string ResourceId = "Project.Resources.AppResources";
public string Text { get; set; }
public IValueConverter Converter { get; set; }
public object ProvideValue(IServiceProvider serviceProvider)
{
if (Text == null)
return null;
ResourceManager resourceManager = new ResourceManager(ResourceId, typeof(TranslateExtension).GetTypeInfo().Assembly);
string translatedText = resourceManager.GetString(Text, CultureInfo.CurrentCulture);
if (this.Converter != null)
{
translatedText = Converter.Convert(translatedText, typeof(string), null, CultureInfo.CurrentCulture).ToString() ?? translatedText;
}
return translatedText;
}
}
XAML:
xmlns:strings="clr-namespace:Project.Utils;assembly=Project"
<ContentPage.Resources>
<ResourceDictionary>
<converters:ColonSpaceConverter x:Key="ColonSpaceConverter" />
</ResourceDictionary>
</ContentPage.Resources>
<Label Text="{strings:Translate Money, Converter={StaticResource ColonSpaceConverter}}" />
This can be achieved in many ways, drafting two of them: (Value of Label will be changed on sliding the Slider)
Method 1
<Label Text="Slide to change Value"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand"
x:Name="lblSliderValue" FontSize="Title" Margin="60"></Label>
<Slider ValueChanged="Slider_ValueChanged"></Slider>
And in CodeBehind file,
private void Slider_ValueChanged(object sender, ValueChangedEventArgs e)
{
lblSliderValue.Text = e.NewValue.ToString("0.00");
lblSliderValue.BackgroundColor = Color.Black;
lblSliderValue.TextColor = Color.White;
}
Method 2 (No need of code in CodeBehind file)
<Label Text="Slide to change Value"
VerticalOptions="Center"
HorizontalOptions="Center"
Text="{Binding Source={x:Reference sldExample}, Path=Value, StringFormat='{0:F2}'}"></Label>
<Slider x:Name="sldExample" BackgroundColor="Yellow" ThumbColor="Violet"></Slider>

Binding fires on unloaded view/view-model when creating a new view

If I bind a RadioButton to a view-model property using a type converter, every time I create a view, the setter on the previous ViewModel gets called, even though the view is Unloaded and should not exist anymore. Here is the minimum code to reproduce the issue:
1) Define an enum type:
enum EnumType {
Value1,
Value2,
}
2) Define a convereter:
public class EnumTypeToBooleanConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, string language) {
return true;
}
public object ConvertBack(object value, Type targetType, object parameter, string language) {
return EnumType.Value1;
}
}
3) Define a view-model:
class ViewModel : INotifyPropertyChanged {
private EnumType value;
public ViewModel() {
Debug.WriteLine(string.Format("ViewModel ({0})::ctor", this.GetHashCode()));
}
public EnumType Value {
get {
Debug.WriteLine(string.Format("ViewModel ({0})::Value::get", this.GetHashCode()));
return this.value;
}
set {
Debug.WriteLine(string.Format("ViewModel ({0})::Value::set", this.GetHashCode()));
if (this.value != value) {
this.value = value;
this.OnPropertyChanged();
}
}
}
private void OnPropertyChanged([CallerMemberName] string name = null) {
if (this.PropertyChanged != null) {
var ea = new PropertyChangedEventArgs(name);
this.PropertyChanged(this, ea);
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
4) Define a UserControl (View.xaml)
<UserControl
x:Class="BindingIssue.View"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:BindingIssue"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400"
x:Name="root">
<UserControl.DataContext>
<local:ViewModel x:Name="ViewModel"/>
</UserControl.DataContext>
<Grid>
<ScrollViewer>
<StackPanel>
<RadioButton x:Name="rdo1"
Content="Value1"
IsChecked="{Binding Path=Value, Converter={StaticResource EnumTypeToBooleanConverter}, ConverterParameter=Value1, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<Button x:Name="btnClose"
Click="btnClose_Click"
Content="Close"/>
</StackPanel>
</ScrollViewer>
</Grid>
5) Add code behind of the View:
public View() {
Debug.WriteLine(string.Format("View ({0})::ctor", this.GetHashCode()));
this.InitializeComponent();
this.Loaded += OnLoaded;
this.Unloaded += OnUnloaded;
}
private void btnClose_Click(object sender, RoutedEventArgs e) {
if (this.Parent is Popup) {
Debug.WriteLine("Closing the popup...");
((Popup)this.Parent).IsOpen = false;
}
}
private void OnLoaded(object sender, RoutedEventArgs e) {
Debug.WriteLine(string.Format("View ({0})::Loaded", this.GetHashCode()));
}
private void OnUnloaded(object sender, RoutedEventArgs e) {
Debug.WriteLine(string.Format("View ({0})::Unloaded", this.GetHashCode()));
}
6) MainPage (XAML)
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
x:Name="Grid">
<Button x:Name="btnNewView"
Click="btnNewView_Click"
Content="New View"
Margin="4"/>
</Grid>
7) Add the event handler to the MainPage
private void btnNewView_Click(object sender, RoutedEventArgs e) {
Debug.WriteLine("Opening a new popup...");
View view = new View();
view.HorizontalAlignment = HorizontalAlignment.Center;
view.VerticalAlignment = VerticalAlignment.Center;
Popup popup = new Popup();
popup.Child = view;
popup.HorizontalOffset = 300;
popup.VerticalOffset = 300;
popup.IsOpen = true;
}
Opening and closing popups multiple times results the following output (Please keep track of hash codes):
Opening a new popup...
View (46418718)::ctor
ViewModel (59312528)::ctor
ViewModel (59312528)::Value::get
View (46418718)::Loaded
Closing the popup...
View (46418718)::Unloaded
Opening a new popup...
View (58892413)::ctor
ViewModel (61646925)::ctor
ViewModel (61646925)::Value::get
ViewModel (59312528)::Value::set
View (58892413)::Loaded
Closing the popup...
View (58892413)::Unloaded
Which means the setter for the ViewModel that is created in the Unloaded view model is being called that is a little bit strange. This behavior is the same for both x:Bind and Binding.
I would like to know if there is an explanation on this behavior.
To Clarify more:
A brand new pair of view/view-model instances are created each time but when the new view is being loaded, the setter on the previous instance of view-model is being called. The previous instance of the view is unloaded and should not even exist at that point. (Think of a popup that is being closed each time, and there is not event a reference the old view/view-model.)
Which means the setter for the ViewModel that is created in the Unloaded view
model is being called that is a little bit strange
Firstly, the setter is not called when the view unloaded, it is called when loading the view. You can add the Loading event handle to verify this. Adding loading event code to the code behind of view control as follows:
this.Loading += View_Loading;
private void View_Loading(FrameworkElement sender, object args)
{
Debug.WriteLine(string.Format("View ({0})::Loading", this.GetHashCode()));
}
And the output now will be:
Closing the popup...
View (22452836)::Unloaded
Opening a new popup...
View (58892413)::ctor
ViewModel (61646925)::ctor
View (58892413)::Loading
ViewModel (61646925)::Value::get
ViewModel (19246503)::Value::set
View (58892413)::Loaded
Secondly, we need to look into why setter is called in this scenario.
One is because you set the binding mode to TwoWay. If you remove this property as follows you will not see the setter called since the ViewModel doesn't need to know the changes in the view.
<RadioButton x:Name="rdo1" Content="Value1" IsChecked="{Binding Path=Value, Converter={StaticResource EnumTypeToBooleanConverter}, ConverterParameter=Value1, UpdateSourceTrigger=PropertyChanged}"/>
More details about binding mode please reference this article. Another reason may be the specific for RadioButton control. A RadioButton can be cleared by clicking another RadioButton in the same group, but it cannot be cleared by clicking it again. So when set IsChecked property to true, we thought the property value of the group is updated. This will trigger the TwoWay binding. In your scenrio, you can test this by setting the default value of IsChecked to false as follows, and you will find the setter is not called until you select the rdo1 on the UI. Or you can use another control CheckBox for testing which will also not call the setter until IsChecked value updated.
public class EnumTypeToBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
return EnumType.Value1;
}
}
The behavior is NOT the same if ScrollViewer gets removed from the View
The behavior is NOT the same for lets say a Boolean property
For these two scenarios, I also tested on my side. The result is the same with the outputs above. Since I don't know how you bind the Boolean property, as I mentioned, whether setter is called depend on what the binding mode is and whether you set or update the property. My testing code about binding Boolean is as follows which have same outputs.
View.xaml
<RadioButton x:Name="rdo2"
Content="BoolValue"
IsChecked="{Binding Path=BoolValue, Converter={StaticResource EnumTypeToBooleanConverter}, ConverterParameter=Value1, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
Converter:
public class EnumTypeToBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
return true;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
//return EnumType.Value1;
return true;
}
}
ViewModel;
private bool boolvalue;
public bool BoolValue
{
get
{
Debug.WriteLine(string.Format("ViewModel ({0})::boolvalue::get", this.GetHashCode()));
return this.boolvalue;
}
set
{
Debug.WriteLine(string.Format("ViewModel ({0})::boolvalue::set", this.GetHashCode()));
if (this.boolvalue != value)
{
this.boolvalue = value;
this.OnPropertyChanged();
}
}
}

Xamarin Forms show/hide option for entry

Currently I am working on Xamarin.Forms and wondering about any possibility to add show/hide option to an entry field?
I have solved a similar issue by using an expand/collapse icon above a number of entry fields.
The show/hide element in XAML
Add a clickable image with fixed size(20x20) referring to embedded resources in the PCL:
<Image Source="{Binding ShowHideIcon, Converter={StaticResource StringToResImageSourceConverter}}" WidthRequest="20" HeightRequest="20"">
<Image.GestureRecognizers>
<TapGestureRecognizer Command="{Binding ShowHideCommand}" />
</Image.GestureRecognizers>
</Image>
The ViewModel processes the command:
Switch the boolean every time the image is touched.
public bool EntryVisible { get; set; }
public Command ShowHideCommand{
get {
return new Command((object o) => {
EntryVisible = !EntryVisible;
if (EntryVisible) {
ShowHideIcon = "ic_collapse";
} else {
ShowHideIcon = "ic_expand";
}
}
}
}
The label and Entry in XAML
Bind the IsVisible attribute of the Label and Entry to the boolean in the ViewModel.
<Label Text="Quantity" IsVisible="{Binding EntryVisible}" />
<Entry Text="{Binding Quantity}" IsVisible="{Binding EntryVisible}" />
For completeness sake, I have used https://developer.xamarin.com/guides/xamarin-forms/working-with/images/#Embedded_Images to store images ic_expand.png and ic_collapse.png in the PCL Resources folder.
A Converter is required to turn a string e.g. "ic_expand" into an image reference that XAML can use.
public class StringToResImageSourceConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
var resString = (string)value;
if (!string.IsNullOrEmpty(resString)) {
return ImageSource.FromResource("ProjectName.Resources." + resString + ".png");
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
Entry entry = new Entry();
// Hide it
entry.IsVisible = false;

Binding Indexer

I have a collection of items in my app, and I want to set the Content of a ContentPresenter to one of these items. The item will be randomly defined by an int index. I can bind an item like this:
<ContentPresenter Content={Binding Items[0]}/>
but not like this:
<ContentPresenter Content={Binding Items[{Binding Index}]}/>
I've seen a number of answers suggesting using MultiBinding in WPF, but this isn't available in UWP. Is there an alternative?
You could create a view model property, returning Items[Index]:
public string RandomItem => Items[Index];
For the PropertyChanged notifications to work, you will need to raise the event whenever Index or Items changes, e.g.:
public int Index
{
get { return _index; }
set
{
_index = value;
RaisePropertyChanged();
RaisePropertyChanged(() => RandomItem);
}
}
If you prefer to have the logic in the view and go the multi-binding way, you can use the Cimbalino toolkit. For that to work, first add 2 NuGet packages:
Cimbalino.Toolkit
Microsoft.Xaml.Behaviors.Uwp.Managed
Now you can create a converter:
public class CollectionIndexConverter : MultiValueConverterBase
{
public override object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var collection = (IList) values[0];
var index = (int?) values[1];
return index.HasValue ? collection[index.Value] : null;
}
public override object[] ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new System.NotImplementedException();
}
}
And use it from XAML:
<ContentPresenter>
<interactivity:Interaction.Behaviors>
<behaviors:MultiBindingBehavior PropertyName="Content" Converter="{StaticResource CollectionIndexConverter}">
<behaviors:MultiBindingItem Value="{Binding Items}" />
<behaviors:MultiBindingItem Value="{Binding Index}" />
</behaviors:MultiBindingBehavior>
</interactivity:Interaction.Behaviors>
</ContentPresenter>

Use RelayCommand with not only buttons

I am using MVVM Light in my project and I am wondering if there is any way to use RelayCommand with all controls (ListView or Grid, for example).
Here is my current code:
private void Item_Tapped(object sender, TappedRoutedEventArgs e)
{
var currentItem = (TechItem)GridControl.SelectedItem;
if(currentItem != null)
Frame.Navigate(typeof(TechItem), currentItem);
}
I want to move this code to Model and use RelayCommand, but the ListView, Grid and other controls don't have Command and CommandParameter attributes.
What does MVVM Light offer to do in such cases?
Following on from the link har07 posted this might be of some use to you as I see you mention CommandParameter.
It is possible to send the "Tapped" item in the list to the relay command as a parameter using a custom converter.
<ListView
x:Name="MyListView"
ItemsSource="{Binding MyCollection}"
ItemTemplate="{StaticResource MyTemplate}"
IsItemClickEnabled="True">
<i:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="ItemClick">
<core:InvokeCommandAction Command="{Binding ViewInMoreDetail}" InputConverter="{StaticResource TapConverter}" />
</core:EventTriggerBehavior>
</i:Interaction.Behaviors>
</ListView>
Custom converter class
public class TapConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
var args = value as ItemClickEventArgs;
if (args != null)
return args.ClickedItem;
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
In your view model you then have a relaycommand.
public RelayCommand<MyObject> MyRelayCommand
{
get;
private set;
}
In your constructor initialise the relay command and the method you want to fire when a tap happens.
MyRelayCommand = new RelayCommand<MyObject>(HandleTap);
This method receives the object that has been tapped in the listview as a parameter.
private void HandleTap(MyObject obj)
{
// obj is the object that was tapped in the listview.
}
Don't forget to add the TapConverter to your App.xaml
<MyConverters:TapConverter x:Key="TapConverter" />