2

I am trying to change the background color of a selected item in a list view when the selection is done in code from the ViewModel. I have found a number of posts associated to changing the background color when the item is selected by user action (i.e., Tapped)

Xamarin.Forms ListView: Set the highlight color of a tapped item

This solution - Using Item Taped works for when the user taps the item in the list but there are cases when an item in the ListView is selected by the user entering data elsewhere in the form.

I tried adding a handler for the ItemSelected on the ListView

    private void ListView_OnItemSelected(object sender, SelectedItemChangedEventArgs e)
    {
        if ( e.SelectedItem == null ) return;  //The selection is set to null in some cases
        if ( !(sender is ListView listView) ) return;
        if ( !(listView.TemplatedItems[0] is ViewCell cell) ) return;

        SetCellColor(cell);
    }

    private void SetCellColor(ViewCell cell)
    {
        if ( _selectedCell != null )
        {
            _selectedCell.View.BackgroundColor = Color.Transparent;
        }

        cell.View.BackgroundColor = Color.LightGray;
        _selectedCell = cell;
    }

Which seems like it should work but the TemplatedItems collection is an internal Forms object and does not work as I would expect. I do get a ViewCell returned but it does not change the background color.

Looked into doing something with the OnAppearing event of the CustomViewCell but don't see how one would know what is selected in this case. I have also played around with some approaches using renderers but these also seem to not give a way to know an item is selected.

Thanks in advance -Joe

Deczaloth
  • 4,503
  • 1
  • 12
  • 33
Joe H
  • 474
  • 3
  • 10

2 Answers2

2

This can not work in Xamarin Forms , you need to use Native Method to do .

You can Custom a ViewCellRenderer to implement it .About Customizing a ViewCell , you can refer to this document .

Create a NativeCell inherits from ViewCell in Xamarin Forms .Also can add other properties inside the ViewCell .

public class NativeCell : ViewCell
{
    public static readonly BindableProperty NameProperty =
        BindableProperty.Create("Name", typeof(string), typeof(NativeCell), "");

    public string Name
    {
        get { return (string)GetValue(NameProperty); }
        set { SetValue(NameProperty, value); }
    }
}

Used in Xaml :

<ListView x:Name="listview"
                        MinimumHeightRequest="96"
                        Margin="16,0">
    <ListView.ItemTemplate>
        <DataTemplate>
            <app19:NativeCell Name="{Binding Name}"/>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

Adding DataSource for ListView :

listview.ItemsSource = DataSource.GetList();

I will not show the code of DataSource , just contains the Name Property and its default data .

Finally, in iOS, you need to add the NativeViewCellRenderer inherits from ViewCellRenderer :

public class NativeViewCellRenderer : ViewCellRenderer
{
    public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, UITableView tv)
    {
        var nativeCell = (NativeCell)item;

        UITableViewCell cell = new UITableViewCell();

        cell.TextLabel.Text = nativeCell.Name;
        cell.SelectedBackgroundView = new UIView
        {
            BackgroundColor = UIColor.Blue, //Set the selected color is Blue
        };
        return cell;
    }
}

Now you will see the effect :

enter image description here

And the same with iOS in Android , you need to add the NativeViewCellRenderer inherits from ViewCellRenderer :

class NativeViewCellRenderer : ViewCellRenderer
{
    private Android.Views.View _cellCore;
    private Drawable _unselectedBackground;
    private bool _selected = false;
    protected override Android.Views.View GetCellCore(Cell item, Android.Views.View convertView, ViewGroup parent, Context context)
    {
        _cellCore = (context as Activity).LayoutInflater.Inflate(Resource.Layout.NativeAndroidCell, null);
        TextView nameTextView = _cellCore.FindViewById<TextView>(Resource.Id.NameText);

        var nativeCell = (NativeCell)item;
        nameTextView.Text = nativeCell.Name;

        _selected = false;
        _unselectedBackground = _cellCore.Background;
        return _cellCore;
    }

    protected override void OnCellPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        base.OnCellPropertyChanged(sender, e);
        if (e.PropertyName == "IsSelected")
        {
            _selected = !_selected;
            if (_selected)
            {
                //var extendedViewCell = sender as CustomViewCell;
                _cellCore.SetBackgroundColor(Android.Graphics.Color.Red);
            }
            else
            {
                _cellCore.SetBackground(_unselectedBackground);
            }
        }
    }
}

The effect as follow :

enter image description here

Junior Jiang
  • 10,415
  • 1
  • 5
  • 21
  • I tried the solution similar to this and perhaps I did something incorrect but it generated background for every cell not just a selected cell. When a ListView item was selected it did not behave differently. I am only working with iOS at this time – Joe H Jan 10 '20 at 14:28
  • @JoeH Glad you have found the solution ! If my answer be helpful , remember to vote up it :) – Junior Jiang Jan 13 '20 at 03:00
  • @JoeH By the way , also remember to mark answer when you have time . – Junior Jiang Jan 30 '20 at 03:27
2

I did come up with a solution which I am happy with and requires no Renderer. In my custom ViewCell I have added the SelectedBackgroundColor BindableProperty as suggested.

        /// <summary>
    /// The SelectedBackgroundColor property.
    /// </summary>
    public static readonly BindableProperty SelectedBackgroundColorProperty =
        BindableProperty.Create("SelectedBackgroundColor", typeof(Color), typeof(SymbolViewCell), Color.Transparent, propertyChanged:SelectionColorChanged);

    public Color SelectedBackgroundColor
    {
        get => (Color)GetValue(SelectedBackgroundColorProperty);
        set => SetValue(SelectedBackgroundColorProperty, value);
    }

    private static void SelectionColorChanged(BindableObject bindable, object oldvalue, object newvalue)
    {
        if ( !(bindable is SymbolViewCell viewCell) ) return;

        var color = (Color) newvalue;

        viewCell.View.BackgroundColor = color;
    }

I then use a custom Converter. This is actually a generic Converter that is used elsewhere for setting values based on a true/false bound value.

public class ConfigurableBoolConverter<T> : IValueConverter
{
    public ConfigurableBoolConverter() { }

    public ConfigurableBoolConverter(T trueResult, T falseResult)
    {
        TrueResult = trueResult;
        FalseResult = falseResult;
    }

    public T TrueResult { get; set; }
    public T FalseResult { get; set; }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (TrueResult == null || FalseResult == null) return !(bool)value;


        return value is bool b && b ? TrueResult : FalseResult;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (TrueResult == null || FalseResult == null) return !(bool)value;

        return value is T variable && EqualityComparer<T>.Default.Equals(variable, TrueResult);
    }
}

In Xaml I define the Converter and set my True/False values to the required background colors:

<converters:ConfigurableBoolConverter x:Key="BackgroundColorConverter"
                                  x:TypeArguments="x:String"
                                  TrueResult="Color.LightGray"
                                  FalseResult="Color.Transparent"/>

Then assign the Converter to the custom ViewCell. In the custom ViewCell the SelectedBackgroundColor was set using the Converter. As a note The SymbolViewCell already existed to solve a different issue with an image that was part of the item refreshing correctly

<DataTemplate>
     <views:SymbolViewCell 
          SelectedBackgroundColor="{Binding IsChecked, Converter={StaticResource 
                BackgroundColorConverter}}"/>
</DataTemplate>

IsChecked is a property on the Item of the ItemsDataSource. The ListView already used a a collection of Item objects and this object already had an IsChecked property.

Stripping down the Item object to the bare minimum (BindableBase implements the IPropertyChanged interface):

public class SymbolItem : BindableBase
{
    private bool? _isChecked;


    public SymbolItem(LegendInfo legendInfo, FeatureTemplate featureTemplate, ArcGISFeatureTable featureTable, IEnumerable<string> requiredFields)
    {
        IsChecked = false;
    }



    public bool? IsChecked
    {
        get => _isChecked;
        set => SetProperty(ref _isChecked, value);
    }
}

This solution would not work if the ItemsDataSource was a collection of string objects because it does require the additional property and you would need a bound SelectedItem property as a place to trigger the change in IsChecked property. But one could make a simple object with a name and IsChecked property to bind. Personally I think this added code is a lot simpler than writing a Renderer to handle things.

public SymbolItem SelectedSymbolItem
{
    get => _selectedSymbolItem;
    set
    {
        if ( _selectedSymbolItem != null ) _selectedSymbolItem.IsChecked = false;
        SetProperty(ref _selectedSymbolItem, value);
    }
}
Joe H
  • 474
  • 3
  • 10