Search code examples
c#xamarinxamarin.formsxamarin.androidframe

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


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?


Solution

  • 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;
    }