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

Time:10-06

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:

  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