Hudhud
Hudhud

Reputation: 61

Xamarin.Forms: IValueConverter only works once

I am trying to control the background color of a label by changing the color of the selected label. I am following the MVVM pattern, and the way I have implemented is like:

  1. In the Model I have created a boolean, with get and set, which has to detect if an item in my listview is selected. public boolean Selected {get; set;}

  2. In my view, I bind the background color property to the boolean, and set the IValueConverter as the Converter

  3. In the ViewModel, I implement the get and set

It seems that it only checks once, as the background color is always white. I have checked it with breakpoints in the Converter, and it only gets called when the list is initiated, but not when the items are updated.

IValueConverter:

public class SelectedItemColorConverter : IValueConverter
    {

        #region IValueConverter implementation

        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (value is bool)
            {
                if ((Boolean)value)
                    return Color.Red;
                else
                    return Color.White;
            }
            return Color.White;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}

This is the ListView:

<StackLayout x:Name="standingsStackLayout" IsVisible="False">
                <ListView x:Name="standingsList" SeparatorColor="Black" ItemsSource="{Binding StandingsListSource}" SelectedItem="{Binding SelectedItem}">
                    <ListView.ItemTemplate>
                        <DataTemplate>
                            <ViewCell>
                                <Label x:Name="TournamentNameLabel" Text="{Binding TournamentName}" 
                                       TextColor="{StaticResource textColor}" HorizontalTextAlignment="Center" 
                                       VerticalTextAlignment="Center" 
                                       BackgroundColor="{Binding Selected, Converter={StaticResource colorConvert}}"/>
                            </ViewCell>
                        </DataTemplate>
                    </ListView.ItemTemplate>
                </ListView>
            </StackLayout>

ViewModel code:

public HistoricalStandingsData _selectedItem;
    public HistoricalStandingsData SelectedItem
    {
        get { return _selectedItem; }
        set
        {
            if (_selectedItem != value)
            { 
                if(_selectedItem != null)
                    _selectedItem.Selected = false;

                _selectedItem = value;

                if (_selectedItem != null)
                    _selectedItem.Selected = true;


                TournamentLabelName = _selectedItem.TournamentName;

                OnPropertyChanged(nameof(SelectedItem));
                //OnPropertyChanged(nameof(_selectedItem.Selected));
            }
        }
    }

I have added the <ContentPage.Resources> for the Converter

Upvotes: 1

Views: 1262

Answers (1)

orhtej2
orhtej2

Reputation: 2165

Let's have a look at your View

<StackLayout x:Name="standingsStackLayout" IsVisible="False">
    <ListView x:Name="standingsList" SeparatorColor="Black" ItemsSource="{Binding StandingsListSource}" SelectedItem="{Binding SelectedItem}">
        <ListView.ItemTemplate>
            <DataTemplate>
                <ViewCell>
                    <Label x:Name="TournamentNameLabel" Text="{Binding TournamentName}" 
                           TextColor="{StaticResource textColor}" HorizontalTextAlignment="Center" 
                           VerticalTextAlignment="Center" 
                           BackgroundColor="{Binding Selected, Converter={StaticResource colorConvert}}"/>
                </ViewCell>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</StackLayout>

We can see there are two major data bindings happening here. First, ListView's ItemsSource property is bound to StandingsListSource property of your view model. Two things can change this: Either the object pointed to by StandingsListSource as a whole or the collections contents.

The official documentation on bindings has the following to say regarding binding ListView.ItemsSource:

The ListView is quite sophisticated in handling changes that might dynamically occur in the underlying data, but only if take certain steps. If the collection of items assigned to the ItemsSource property of the ListView changes during runtime—that is, if items can be added to or removed from the collection—use an ObservableCollection class for these items. ObservableCollection implements the INotifyCollectionChanged interface, and ListView will install a handler for the CollectionChanged event.

Let's do just that (full implementation of DataSource class I use as a BindingContext for the form later):

public ObservableCollection<HistoricalStandingsData> StandingsListSource { get; } = new ObservableCollection<HistoricalStandingsData>();

For simplicity I made StandingsListSource a C# 6.0 readonly auto property to eliminate the need of tracking it's reassignment.

Now, since ListView.SelectedItem is bound as well we need some way to notify ListView that selected item was updated from code behind. Enter the second advice from documentation mentioned before:

If properties of the items themselves change during runtime, then the items in the collection should implement the INotifyPropertyChanged interface and signal changes to property values using the PropertyChanged event.

This has 2 implications:

  • HistoricalStandingsData should notify when it's properties change because each row in ListView binds to this property as per DataTemplate:

    public class HistoricalStandingsData : INotifyPropertyChanged
    {
        public HistoricalStandingsData(string name)
        {
            this.TournamentName = name;
        }
    
        private bool selected;
    
        public bool Selected
        {
            get
            {
                return selected;
            }
    
            set
            {
                selected = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Selected)));
            }
        }
    
        public string TournamentName { get; }
    
        // From INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;
    }
    
  • View model class should implement INotifyPropertyChanged to notify on properties, in this case SelectedItem changes.

    class DataSource : INotifyPropertyChanged
    {
        public ObservableCollection<HistoricalStandingsData> Items { get; } = new ObservableCollection<HistoricalStandingsData>();
    
        public HistoricalStandingsData SelectedItem
        {
            // Information on selection is stored in items themselves, use Linq to find the single matching item
            get => Items.Where(x => x.Selected).SingleOrDefault();
            set
            {
                // Reset previous selection
                var item = SelectedItem;
                if (item != null)
                    item.Selected = false;
    
                // Mark new item as selected, raising HistoricalStandingItem.PropertyChanged
                if (value != null)
                    value.Selected = true;
    
                // Notify observers that SelectedItem changed
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedItem)));
            }
        }
    
        // From INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;
    
        public DataSource()
        {
            // Helper ICommand used for appending new items to HistoricalStandingsData
            AddNew = new Command(() =>
            {
                var item2 = new HistoricalStandingsData(DateTime.Now.ToString());
                // Append, notifies observers that collection has changed.
                Items.Add(item2);
                // Set as selected, resetting previous selection
                SelectedItem = item2;
            });
        }
    
        public ICommand AddNew { get; } 
    }
    

AddNew command is optional, I added it for testing purposes.

Upvotes: 1

Related Questions