Use RelayCommand with not only buttons - xaml

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" />

Related

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

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.

How to make GradientStopCollection Observable?

i have run into a problem where i want to show a list of gradient stops in a listbox. The problem is that putting the gradientstops in a collection of type ObservableCollection works, but using a GradientStopCollection does not.
When i Use GradientStopCollection, the items that are in the list before the window is initialized are shown, but when a button is pressed to add a third item, the UI is not updated.
Calling OnPropertyChanged does not result in the UI being updated. I have made a small example to try to reproduce the problem.
So how can get the window to correctly update even when i use a gradientstop collection?
using System.Windows;
using System.Windows.Media;
namespace WpfApp1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
DataContext = new ViewModel();
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
ViewModel vm = (DataContext as ViewModel);
vm.Collection.Add(new GradientStop(Colors.Red, 0.5));
//This line has no effect:
vm.OnPropertyChanged("Collection");
}
}
}
Viewmodel:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;
namespace WpfApp1
{
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public GradientStopCollection Collection
{
get
{
return collection;
}
set
{
collection = value;
}
}
//Replacing GradientStopCollection
// with ObservableCollection<GradientStop> makes it work
GradientStopCollection collection;
public ViewModel()
{
GradientStop a = new GradientStop(Colors.Green, 0);
GradientStop b = new GradientStop(Colors.Yellow, 1.0);
collection = new GradientStopCollection() { a, b } ;
OnPropertyChanged("Collection");
}
public void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
handler?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
public class Converter : IValueConverter
{
public object Convert(object value, Type targettype, object parameter, CultureInfo cultureInfo)
{
if (value is Color color)
return new SolidColorBrush(color);
return Binding.DoNothing;
}
public object ConvertBack(object value, Type targettype, object parameter, CultureInfo cultureInfo)
{
throw new NotImplementedException();
}
}
}
And finally the xaml:
<Grid>
<Grid.Resources>
<local:Converter x:Key="ColorConverter"/>
<DataTemplate DataType="{x:Type GradientStop}">
<TextBlock
Width="50"
Background="{Binding Color, Converter={StaticResource ColorConverter}}"
Text="block"
/>
</DataTemplate>
</Grid.Resources>
<ListBox
x:Name="GradientListBox"
Width="72"
Height="92"
ItemsSource="{Binding Collection}" />
<Button Content="Button" HorizontalAlignment="Left" Margin="169,264,0,0" VerticalAlignment="Top" Width="75" Click="Button_Click"/>
</Grid>
I don't think that there is any easy way around this problem.
You could create your own collection class, inheriting from GradientStopCollection and implementing the interface INotifyCollectionChanged, effectively making an ObservableGradientStopCollection.
You can probably find an implementation of INotifyCollectionChanged as an excmple.
It might be easier, just to keep two collections, although it seems like bad style.

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>