I am binding the background color for frame in collection view using RelativeSource binding. But the background color is changing for all the frames inside the collection view. I need to set the background color for only the frame which I am selecting. This is my xaml code
<StackLayout Padding="10">
<CollectionView x:Name="list" ItemsSource="{Binding samplelist}">
<CollectionView.ItemsLayout>
<GridItemsLayout Orientation="Vertical" Span="2" HorizontalItemSpacing="10" VerticalItemSpacing="10" />
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="CommonStates">
<VisualState Name="Selected">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="Green" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Frame CornerRadius="10" HasShadow="False" BackgroundColor="{Binding BackgroundTest,Mode=TwoWay, Converter={StaticResource colorConverter}}" HeightRequest="75" Margin="5,0,0,0" >
<StackLayout Orientation="Vertical">
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Command="{Binding Source={x:Reference test}, Path=BindingContext.TriggerScene}"
CommandParameter="{Binding .}"/>
</StackLayout.GestureRecognizers>
This is my code in Viewmodel
public bool FrameColorChange=true;
private Color _backgroundTest;
public Color BackgroundTest
{
get { return _backgroundTest; }
set
{
if (value == _backgroundTest)
return;
_backgroundTest = value;
OnPropertyChanged(nameof(BackgroundTest));
}
}
private async void TriggerScene(Scene scene)
{
if (FrameColorChange==true)
{
BackgroundTest = Color.Gray;
FrameColorChange = false;
}
else
{
BackgroundTest = Color.White;
FrameColorChange = true;
}
}
I have gone through some fixes like
how to access child elements in a collection view?
but nothing helped. I also tried SelectionChanged event.But the problem with SelectionChanged is that it doesn't trigger properly because there is TapGestureRecognizer in my frame. I want the color binding for the selected frame in my TriggerScene command of TapGestureRecognizer in my viewmodel. I don't want to use code behind. I have no clue how to fix this any suggestions?
CodePudding user response:
You could try the code below.
Xaml:
<StackLayout Padding="10">
<CollectionView x:Name="list" ItemsSource="{Binding samplelist}" SelectionMode="Single" SelectionChanged="list_SelectionChanged" >
<CollectionView.ItemsLayout>
<GridItemsLayout Orientation="Vertical" Span="2" HorizontalItemSpacing="10" VerticalItemSpacing="10" />
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<Frame CornerRadius="10" HasShadow="False" BackgroundColor="{Binding BackgroundTest}" HeightRequest="75" Margin="5,0,0,0" >
<StackLayout Orientation="Vertical">
<Label Text="{Binding str}"></Label>
</StackLayout>
</Frame>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</StackLayout>
Code behind:
public Page2()
{
InitializeComponent();
this.BindingContext = new MyViewModel();
}
private void list_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
MyModel previous = e.PreviousSelection.FirstOrDefault() as MyModel;
MyModel current = e.CurrentSelection.FirstOrDefault() as MyModel;
//Set the current to the color you want
current.BackgroundTest = "Red";
if (previous != null)
{
//Reset the previous to defaulr color
previous.BackgroundTest = "Gray";
}
}
ViewModel:
public class MyViewModel
{
public ObservableCollection<MyModel> samplelist { get; set; }
public MyViewModel()
{
samplelist = new ObservableCollection<MyModel>()
{
new MyModel(){ BackgroundTest="Gray", str="hello1"},
new MyModel(){ BackgroundTest="Gray", str="hello2"},
new MyModel(){ BackgroundTest="Gray", str="hello3"},
new MyModel(){ BackgroundTest="Gray", str="hello4"},
new MyModel(){ BackgroundTest="Gray", str="hello5"},
new MyModel(){ BackgroundTest="Gray", str="hello6"},
new MyModel(){ BackgroundTest="Gray", str="hello7"},
new MyModel(){ BackgroundTest="Gray", str="hello8"},
};
}
}
Model:
public class MyModel : INotifyPropertyChanged
{
public string str { get; set; }
private string _backgroundTest;
public string BackgroundTest
{
get { return _backgroundTest; }
set
{
_backgroundTest = value;
OnPropertyChanged("BackgroundTest");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Update:
If you have TapGestureRecognizer
in DataTemplate, you could use VisualState
instead of SelectionChanged
of CollectionView.
<ContentPage.Resources>
<ResourceDictionary>
<Style TargetType="StackLayout">
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup>
<VisualState x:Name="Selected">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="Accent" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="UnSelected">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="Blue" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
</ResourceDictionary>
</ContentPage.Resources>
Xaml:
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped"></TapGestureRecognizer>
</StackLayout.GestureRecognizers>
Code behind:
StackLayout lastElementSelected;
private void TapGestureRecognizer_Tapped(object sender, EventArgs e)
{
if (lastElementSelected != null)
VisualStateManager.GoToState(lastElementSelected, "UnSelected");
VisualStateManager.GoToState((StackLayout)sender, "Selected");
lastElementSelected = (StackLayout)sender;
}
CodePudding user response:
There might be lots of ways to solve your problem, and i will not claim to have found the best, but it is still one (simple) way.
I will add a complete-working-minimal-sample for your below, that does exactly what you want, so feel free to copy-paste and adapt it to your need.
One way to achieve your goal would to:
- Add a property called
Selected
or something alike to the Object that is populating the Collection (samplelist
) that binds to your CollectionView. - Bind the BackgroundColor property of Frame to that
Selected
property and set to it a Converter that changes from a boolean value (is selcted?) to a Color (selection color). - Then when an item in the collection is tapped and the TapGestureRecognizer triggers you can pass the selected item as a CommandParameter to the Command
- In the Command-handler set the
Selected
property of the passed item totrue
. - When the
Selected
property changes, the Converter is called, and the BackgroundColor property is updated.
The following sample exemplifies this:
Page1.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="App1.Page1"
x:Name="test"
xmlns:local="clr-namespace:App1">
<ContentPage.BindingContext>
<local:ViewModel/>
</ContentPage.BindingContext>
<ContentPage.Resources>
<ResourceDictionary>
<local:SelectedToColorConverter x:Key="selectedToColorConverter"/>
</ResourceDictionary>
</ContentPage.Resources>
<ContentPage.Content>
<StackLayout Padding="10">
<CollectionView ItemsSource="{Binding samplelist}">
<CollectionView.ItemsLayout>
<GridItemsLayout Orientation="Vertical" Span="2" HorizontalItemSpacing="10" VerticalItemSpacing="10" />
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout>
<Frame CornerRadius="10"
HasShadow="False"
BackgroundColor="{Binding Selected, Converter={x:StaticResource selectedToColorConverter}}"
HeightRequest="75"
Margin="5,0,0,0" >
<StackLayout Orientation="Vertical">
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Command="{Binding Source={x:Reference test}, Path=BindingContext.TriggerSceneCommand}" CommandParameter="{Binding .}"/>
</StackLayout.GestureRecognizers>
<Label Text="{Binding Text}"/>
<Label Text="{Binding Description}"/>
</StackLayout>
</Frame>
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</StackLayout>
</ContentPage.Content>
</ContentPage>
ViewModel.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using Xamarin.Forms;
namespace App1
{
public class ViewModel
{
public ViewModel()
{
samplelist = new List<item>
{
new item { Text = "Uno", Description = "Uno Description bla bla" },
new item { Text = "Dos", Description = "Dos Description bla bla" },
new item { Text = "Tres", Description = "Tres Description bla bla" }
};
TriggerSceneCommand = new Command<object>(TriggerScene);
}
public List<item> samplelist { get; set; }
public Boolean isMultiSelect = false;
public Command TriggerSceneCommand { get; set; }
private void TriggerScene(object selectedItem)
{
((item)selectedItem).Selected = !((item)selectedItem).Selected;
if (!isMultiSelect)
{
foreach (item otherItem in samplelist)
{
if (otherItem != selectedItem)
{
otherItem.Selected = false;
}
}
}
}
}
public class item : INotifyPropertyChanged
{
public Boolean _selected;
public Boolean Selected
{
get
{
return _selected;
}
set
{
_selected = value;
OnPropertyChanged();
}
}
public String Text { get; set; }
public String Description { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string name = "")
{
var propertyChanged = PropertyChanged;
propertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
public class SelectedToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return ((Boolean)value) ? Color.Gray : Color.White;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
Note that as a bonus there is a property called isMultiSelect
which if true allows multiple items to be marked/colored and if false
, then when one item is selected all the others get their Selected
property set to false
.