How to set color only for the selected frame in collection view using MVVM in Xamarin forms?

Viewed 1058

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?

3 Answers

I already posted one way to solve your problem in another answer, now i want to provide yet a simpler solution (although i won't claim it is the best).

NOTE that in this solution, contrary to my other Answer, you do not need to add a superfluous Property to the objects in your collection view, but the new property is defined directly in the ViewModel.


One way to solve your problem could be to:

  1. Define a property in your ViewModel called SelectedItem: this will keep track of the currently selected item.
  2. Then you bind the BackgroundColor of your Frame to the new property: SelectedItem and for that you will need a ValueConverter that takes the SelectedItem and a ConverterParameter: the current Frame.
  3. Inside the Frame, in the StackLayout you have the good old TapGestureRecognizer whose handler when called will set the Selected Item.
  4. When SelectedItem is set, OnPropertyChanged is called and ValueConverter is called for each of the items in the CollectionView. The converter then checks if the BindingContext of the Frame (the item to which it is bound!) is the same as the SelectedItem and if so it sets its color to Gray (selected!)

And of course, below i add a complete-minimal-working-sample. Feel free to copy-paste it and play with it.

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 x:Name="frame" CornerRadius="10"  
                                    HasShadow="False" 
                                    BackgroundColor="{Binding Source={x:Reference test}, Path=BindingContext.SelectedItem, Converter={x:StaticResource selectedToColorConverter}, ConverterParameter={x:Reference frame}}" 
                                    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 : INotifyPropertyChanged
    {

        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<item>(TriggerScene);
        }

        public List<item> samplelist { get; set; }


        private item _selectedItem;
        public item SelectedItem
        {
            get => _selectedItem;
            set
            {
                _selectedItem = value;
                OnPropertyChanged();
            }
        }



        public Command TriggerSceneCommand { get; set; }
        private void TriggerScene(item newSelectedItem)
        {
           SelectedItem = newSelectedItem;
        }


        public event PropertyChangedEventHandler PropertyChanged;
        public void OnPropertyChanged([CallerMemberName] string name = "")
        {
            var propertyChanged = PropertyChanged;

            propertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }

    }

    public class item
    {

        public String Text { get; set; }

        public String Description { get; set; }

    }


    public class SelectedToColorConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {

            Color result = Color.White;


            if (value != null && parameter != null && ((Frame)parameter).BindingContext != null && (item)value == (item)((Frame)parameter).BindingContext)
            {
                result = Color.Gray;
            }


            return result;

        }

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

And why not, lets drop here a Bonus.

You can add two lines of code to the TapGestureRecognizer's event handler to return the original color back after some time (three seconds, maybe?).

Just change TriggerScene method in ViewModel as follows (see comments on Code):

private void TriggerScene(item newSelectedItem)
{
    // Highlight selection!
    SelectedItem = newSelectedItem;

    // Sit and wait...
    await Task.Delay(3000);

    // Go back to normal!
    SelectedItem = null;
}

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;

    }

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:

  1. Add a property called Selected or something alike to the Object that is populating the Collection (samplelist) that binds to your CollectionView.
  2. 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).
  3. 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
  4. In the Command-handler set the Selected property of the passed item to true.
  5. 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.

Related