Databound BackgroundColor of Grid in ListView - color keeps reverting on iOS

MarkMcGoughMarkMcGough USMember ✭✭
edited March 2018 in Xamarin.Forms

This is a weird one, and it's got me stumped.

My XAML follows the following pattern:

<ContentView ... >
  <ContentView.Resources>
    <ResourceDictionary>
      <local:ItemColorConverter x:Key="ItemColorConverter" />
    </ResourceDictionary>
  </ContentView.Resources>
  <ContentView.Content>
    <ListView>
      <ListView.ItemTemplate>
        <DataTemplate>
          <ViewCell>
            <ViewCell.View>
              <Grid BackgroundColor="{Binding ColorConverterProperty, Converter={StaticResource ItemColorConverter}, Mode=OneWay}">
                (RowDefinitions, ColumnDefinitions, and display elements go here)
              </Grid>
            </ViewCell.View>
          </ViewCell>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>
  </ContentView.Content>
</ContentView>

In my code, I have the following:

public class Item : INotifyPropertyChanged, IComparable<Item>
{
    (fields go here)

    [JsonIgnore]
    public ColorParams ColorConverterProperty
    {
        get
        {
            return new ColorParams(things that the IValueConverter needs to access);
        }
    }
}

My IValueConverter is as follows:

class ItemColorConverter : IValueConverter
{
    private static Color ms_cColor1 = Color.FromRgb(255, 255, 255);
    private static Color ms_cColor2 = Color.FromRgb(192, 16, 0);
    private static Color ms_cColor3 = Color.FromRgb(0, 16, 192);

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        ColorParams cpParams;

        if (value != null && value.GetType() == typeof(ColorParams))
        {
            cpParams = (ColorParams)value;

            if (cpParams.Value1)
            {
                return ms_cColor1;
            }
            else
            {
                if (cpParams.Value2)
                {
                    return ms_cColor2;
                }
                else
                {
                    return ms_cColor3;
                }
            }
        }
        else
        {
            return Color.Transparent;
        }
    }

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

This all works great. I can fire the INotifyPropertyChanged for ColorConverterProperty event on the data class, and the ListView row background color changes appropriately. This is what it's supposed to do, and this works on Android and UWP.

However, just recently, it's stopped working on iOS devices. Well, kind of stopped working. The event fires, the ListView line's background color changes, but a fraction of a second later, it changes back to the first color. I've traced out my app, and it doesn't fire the event a second time, and the color converter doesn't get called twice, only once. But the background color still changes back.

The weird thing is, if I scroll the item in question off the screen, then scroll it back on the screen, it has the correct background color again. It's almost like iOS redraws the item on the screen with the old background color, and only redraws the item when it's scrolled back on screen.

Best Answer

  • MarkMcGoughMarkMcGough US ✭✭
    edited March 2018 Accepted Answer

    I believe I've found a solution to the problem. Using a custom renderer, I can disable the selection indicator on iOS altogether, which seems to be the root cause of the problem.

    using UIKit;
    using Xamarin.Forms;
    using Xamarin.Forms.Platform.iOS;
    
    [assembly: ExportRenderer(typeof(ViewCell), typeof(my.root.namespace.iOS.MyViewCellRenderer))]
    
    namespace my.root.namespace.iOS
    {
        class MyViewCellRenderer : ViewCellRenderer
        {
            public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, UITableView tv)
            {
                var cell = base.GetCell(item, reusableCell, tv);
    
                if (cell != null)
                {
                    cell.SelectionStyle = UITableViewCellSelectionStyle.None;
                }
    
                return cell;
            }
        }
    }
    

    Of course, I will need to extend ViewCell in my portable code, and set the custom renderer only for that class, as only some ListView controls need this background color feature.

    Edit: I've confirmed, this solves the issue for me.

Answers

  • MarkMcGoughMarkMcGough USMember ✭✭

    Interesting, I can't reproduce the problem in a fresh solution. I need to investigate more.

    If anyone has any ideas of why something like this might be happening, any pointers would be much appreciated.

  • MarkMcGoughMarkMcGough USMember ✭✭

    I've managed to reproduce it in a clean project.

    The key to the problem is in the ListView.ItemTapped event.

    private const double SELECTION_GUARD_TIME = 0.1;
    private DateTime m_dtLastItemTapTimeUtc;
    
    private void lvItems_ItemTapped(object sender, ItemTappedEventArgs e)
    {
        if ((DateTime.UtcNow - m_dtLastItemTapTimeUtc).TotalSeconds > SELECTION_GUARD_TIME)
        {
            Item iItem = (Item)e.Item;
            lvItems.SelectedItem = null;
            iItem.Value1 = !iItem.Value1;
            m_dtLastItemTapTimeUtc = DateTime.UtcNow;
        }
    }
    

    It's this piece of code that triggers the issue.

    The selection guard time code here can be ignored. It's a hack fix for UWP; UWP fires the ItemTapped event when an item is selected AND when the selection is cleared, unlike iOS. The "lvItems.SelectedItem = null;" code is to clear the selection, so the background color shows through.

    It seems that, on iOS, updating a databound background color of an item, from the ItemTapped event, causes the color on screen to revert to the previous color, and become "dirty".

  • MarkMcGoughMarkMcGough USMember ✭✭
    edited March 2018 Accepted Answer

    I believe I've found a solution to the problem. Using a custom renderer, I can disable the selection indicator on iOS altogether, which seems to be the root cause of the problem.

    using UIKit;
    using Xamarin.Forms;
    using Xamarin.Forms.Platform.iOS;
    
    [assembly: ExportRenderer(typeof(ViewCell), typeof(my.root.namespace.iOS.MyViewCellRenderer))]
    
    namespace my.root.namespace.iOS
    {
        class MyViewCellRenderer : ViewCellRenderer
        {
            public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, UITableView tv)
            {
                var cell = base.GetCell(item, reusableCell, tv);
    
                if (cell != null)
                {
                    cell.SelectionStyle = UITableViewCellSelectionStyle.None;
                }
    
                return cell;
            }
        }
    }
    

    Of course, I will need to extend ViewCell in my portable code, and set the custom renderer only for that class, as only some ListView controls need this background color feature.

    Edit: I've confirmed, this solves the issue for me.

  • DuchaDucha Member

    Thanks! This solved my problem as well!

Sign In or Register to comment.