0
votes

I need to get all the viewcells of my listview programmatically so I could change the background color of a specific child layout within the viewcell. There's no problem with updating the color for a viewcell when it's tapped, but I need to change the color of all viewcells to the default color, whenever a different viewcell is tapped.

While searching for a solution I often found answers where the viewcells were accessed by the runtime properties of the listview (see code below or the second answer in here: Xamarin.Forms: Get all cells/items of a listview), while testing the code I realized that this doesn't work for listviews where grouping is enabled.

IEnumerable<PropertyInfo> pInfos = (connectionsListView as ItemsView<Cell>).GetType().GetRuntimeProperties();
var templatedItems = pInfos.FirstOrDefault(info => info.Name == "TemplatedItems");
if (templatedItems != null)
{
  var cells = templatedItems.GetValue(connectionsListView);
    foreach (ViewCell cell in cells as Xamarin.Forms.ITemplatedItemsList<Xamarin.Forms.Cell>)
    {
        if (cell.BindingContext != null && cell.BindingContext is MyModelClass)
        {
              // Change background color of viewcell
        }
    }
}

When grouping is enabled this code only returns the grouped header viewcells. I couldn't find any answer to change this code so the actual "body" viewcells are returned instead of only the headings. Is there any possible approach to change this code so I get my expected result or do I have to use a custom renderer for this?

Update - Listview xaml code

Here you can see the listview I'm currently using in my XAML. I try to work out a solution where I can bind the background color of each viewcell to the model (to each "document" in my case) but at the moment I couldn't work out how to change the color for each specific viewcell when one is tapped. (I need to only have the background color of the currently selected viewcell to be changed, so all other viewcells have the default background color.)

            <ListView x:Name="DocumentListView"
                      ItemsSource="{Binding GroupedDocuments}"
                      BackgroundColor="WhiteSmoke"
                      HasUnevenRows="True"
                      RefreshCommand="{Binding LoadDocumentsCommand}"
                      IsPullToRefreshEnabled="True"
                      Refreshing="DocumentListView_OnRefreshing"
                      IsRefreshing="{Binding IsBusy, Mode=OneWay}"
                      CachingStrategy="RecycleElement"
                      IsGroupingEnabled="True"
                      GroupDisplayBinding="{Binding Key}"
                      GroupShortNameBinding="{Binding Key}"
                      VerticalOptions="StartAndExpand"
                      HorizontalOptions="StartAndExpand"
                      Margin="0, -20, 0, 0">
                <ListView.GroupHeaderTemplate>
                    <DataTemplate>
                        <ViewCell Height="25">
                            <Label x:Name="DocumentDate"
                                   FontSize="Medium"
                                   TextColor="#2E588C"
                                   VerticalOptions="Center"
                                   HorizontalTextAlignment="Center"
                                   Text="{Binding Key}"/>
                        </ViewCell>
                    </DataTemplate>
                </ListView.GroupHeaderTemplate>
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <ViewCell Height="155" Tapped="DocumentViewCell_OnTapped">
                            <StackLayout Padding="10, 5, 10, 5">
                                <Frame Padding="0" HorizontalOptions="FillAndExpand" HasShadow="True">
                                    <StackLayout Padding="10" Orientation="Horizontal" HorizontalOptions="FillAndExpand" VerticalOptions="StartAndExpand">
                                        <StackLayout HorizontalOptions="StartAndExpand">
                                            <StackLayout Orientation="Horizontal" Spacing="15" Margin="10, 10, 10, 0" HorizontalOptions="StartAndExpand">
                                                <Label Text="{Binding Name}"
                                                       LineBreakMode="NoWrap"
                                                       FontSize="20"
                                                       TextColor="CornflowerBlue"
                                                       FontAttributes="Bold"/>
                                            </StackLayout>
                                            <StackLayout Orientation="Horizontal" Spacing="5" Margin="12, 0, 0, 0" HorizontalOptions="StartAndExpand">
                                                <Label Text="{Binding DocumentType.Name, StringFormat='Typ: {0}'}"
                                                       LineBreakMode="NoWrap"
                                                       FontSize="16"
                                                       TextColor="Black"/>
                                            </StackLayout>
                                            <StackLayout Orientation="Horizontal" Spacing="5" Margin="12, 3, 0, 0" HorizontalOptions="StartAndExpand">
                                                <Label Text="{Binding TotalValue, StringFormat='Gesamtwert: {0:F2} €'}"
                                                       LineBreakMode="NoWrap"
                                                       FontSize="16"
                                                       TextColor="Black"/>
                                            </StackLayout>
                                            <StackLayout Spacing="5" Margin="12, 3, 0, 0" Orientation="Horizontal" HorizontalOptions="StartAndExpand" VerticalOptions="Start">
                                                <Label Text="{Binding TagCollectionString, StringFormat='Tags: {0}'}"
                                                       LineBreakMode="WordWrap"
                                                       FontSize="14"
                                                       TextColor="Black" 
                                                       VerticalOptions="CenterAndExpand"/>
                                            </StackLayout>
                                        </StackLayout>
                                        <StackLayout HorizontalOptions="EndAndExpand" VerticalOptions="Start" Margin="0, 25, 25, 0">
                                            <ImageButton HeightRequest="85" MinimumWidthRequest="85" x:Name="ButtonEditDocument" Source="baseline_more_vert_black_48.png" Clicked="ButtonEditDocument_OnClicked" Margin="0, 0, 15, 0" BackgroundColor="Transparent" WidthRequest="25" />
                                        </StackLayout>
                                    </StackLayout>
                                </Frame>
                            </StackLayout>
                        </ViewCell>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>

Update 2 - Usage of Bindings

I worked out how I could use bindings to store the currently selected background of each viewcell in my model. At the moment I'm facing the issue that the UI doesn't update properly when the bound values have changed. Here is the code I have written so far and how the page is updated when a different viewcell is tapped:

Document model class:

public class Document 
{
        public bool HasDefaultColor { get; set; }

        public string CurrentlySelectedColorFromHex
        {
            get => ColorConverter.GetHexString(CurrentlySelectedColor);
        }

        [NotMapped]
        public Color CurrentlySelectedColor => HasDefaultColor ? DefaultColor : ActivatedColor;

        private static readonly Color DefaultColor = Color.WhiteSmoke;
        private static readonly Color ActivatedColor = Color.FromHex("#2E588C");
}

OnTapped function in code-behind:

        private void DocumentViewCell_OnTapped(object sender, EventArgs e)
        {                    
                    var documents = documentRepository.GetAll();
                    foreach (var document in documents)
                        document.HasDefaultColor = true;

                    selectedDocument.HasDefaultColor = false;
                    unitOfWork.Complete();
                    UpdatePage();
}

In UpdatePage() I want to refresh the listview properly after the bound collection has changed:

                viewModel.LoadDocuments();
                DocumentListView.BeginRefresh();

Sorry if this is a beginner question but I couldn't find an answer to this yet or couldn't figure out how to properly update the UI so that the background color of each viewcell is properly updated. At least the bounded values are stored correctly at each OnTapped() call.

Update 3 - Added Converter

Hey guys, I have tried a few things and get stuck with updating the bound property of the model. I have tried data triggers as well but couldn't change those data triggers correctly so they didn't worked as I expected them to do.

Until now I have added a custom bool to Color converter to convert the bound property:

    public class BoolToColorConverter : IValueConverter
    {
        private static readonly Color DefaultColor = Color.WhiteSmoke;
        private static readonly Color ActivatedColor = Color.FromHex("#2E588C");

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is bool activated)
                return activated ? ActivatedColor : DefaultColor;

            return DefaultColor;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is Color color)
            {
                if (color == DefaultColor)
                    return true;
            }

            return false;
        }

The Color converter returns the correct values, but I can't figure out how to update the model property at runtime in the OnTapped() method of each viewcell.

Currently this is my OnTapped() method:

        private void DocumentViewCell_OnTapped(object sender, EventArgs e)
        {
                // Determine which document was selected
                if (sender.GetType() == typeof(ViewCell))
                {
                    ViewCell selectedViewCell = (ViewCell)sender;

                    if (selectedViewCell.BindingContext != null && selectedViewCell.BindingContext.GetType() == typeof(Document))
                    {
                        Document document = (Document)selectedViewCell.BindingContext;

                        // Update backing field selectedDocument for correct bindings and to show correct detail page
                        if (document != null)
                            selectedDocument = document;
                    }
                }

Thanks for any help in advance and thanks to anyone who commented so far.

2
You really, really shouldn't be doing this. I am not sure if someone can answer your question as it is legitimate and may have a solution, but the path you take is expected to have a lot of problems at the best case.Ivan Ičin
Okay, I thought this is a valid approach as I have seen this answer online multiple times. Seems like I can't avoid writing a custom renderer. Thanks for your answer.N.iw
Okay, thanks for your help. I will see if I can work out a solution with data bindings.N.iw
Sadly I don't have time to give an extended answer, so this will have to do. Basically you're saying that the backgroundcolor is dependant on the selecteditem. So for me the logical choice would be to just bind it to that and then use a value converter to convert it to a background color. You can bind other values to a value converter so if you declare one and bind the current item, defaultcolor and selectedcolor. All it then needs to do is return the appropriate color depending on wether the input value is the same object as it's bound item.user10608418
In addition to the comment above, here's a link that should provide you with the information needed to setup the value converter: docs.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/…user10608418

2 Answers

1
votes

I finally had some time to work out an answer for you.

I see by now you've already found a work around, but since it's not really in the line of the design philosophy of Xamarin.Forms (It's doing hardcoded work in the code-behind to get what you want instead of using Xamarin.Forms functionality.

Anyway I'll work out 2 possible solutions that are, in my opinion, better in line with a good Xamarin.Forms design.

Option 1:

This option keeps track of the currently selected item and uses a value converter for each item in the list to check if it's equal to that selected item and return a color based on that.

In our ViewModel we need to setup both the collection for our ListView our SelectedItem property that notifies us of property changes, and finally the ItemTappedCommand that will change our SelectedItem:

private ObservableCollection<ItemGroup> _itemGroups;
public ObservableCollection<ItemGroup> ItemGroups
{
    get => _itemGroups;
    set => SetProperty(ref _itemGroups, value);
}

private Item _selectedItem;
public Item SelectedItem
{
    get => _selectedItem;
    set => SetProperty(ref _selectedItem, value);
}

private ICommand _itemTappedCommand;
public ICommand ItemTappedCommand => _itemTappedCommand ?? (_itemTappedCommand = new Command<Item>((item) =>
{
    SelectedItem = item;
}));

Then we will need a ValueConverter that will check equality and return the right Color:

public class EqualityToColorConverter : IValueConverter
{
    public Color EqualityColor { get; set; }

    public Color InequalityColor { get; set; }
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {

        if (value == null || parameter == null)
            return InequalityColor;

        if (parameter is Binding binding && binding.Source is View view)
        {
            parameter = view.BindingContext;
        }

        return value == parameter ? EqualityColor : InequalityColor;
    }

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

This converter uses a small work around to extract the actual item from the parameter, since for some reason it kept returning me a Binding instead of the actual Item.

Now that we have everything in place we can create our page:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:converters="clr-namespace:ColorChangeDemo.Converters"
             x:Class="ColorChangeDemo.Views.SelectedItemPage">
    <ContentPage.Resources>
        <ResourceDictionary>
            <converters:EqualityToColorConverter x:Key="equalityToColorConverter" EqualityColor="Green" InequalityColor="Gray" />
        </ResourceDictionary>
    </ContentPage.Resources>
    <ContentPage.Content>
        <ListView x:Name="ListView"  ItemsSource="{Binding ItemGroups}" GroupShortNameBinding="{Binding Key}" GroupDisplayBinding="{Binding Key}" IsGroupingEnabled="True">
            <ListView.GroupHeaderTemplate>
                <DataTemplate>
                    <ViewCell>
                        <Label Text="{Binding Key}" />
                    </ViewCell>
                </DataTemplate>
            </ListView.GroupHeaderTemplate>
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <Button 
                            x:Name="Button"
                            Text="{Binding Id}" 
                            BackgroundColor="{Binding Source={x:Reference ListView}, Path=BindingContext.SelectedItem, Converter={StaticResource equalityToColorConverter}, ConverterParameter={Binding Source={x:Reference Button}}}"
                            Command="{Binding Source={x:Reference ListView}, Path=BindingContext.ItemTappedCommand}"
                            CommandParameter="{Binding .}"/>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </ContentPage.Content>
</ContentPage>

The upside of this solution is that you don't have to change your Item class (in case you don't have any control over it), the downside however is that every selection change all items will react to the changed value. So if a lot of items are shown this might not be optimal.

Option 2:

This option will add a Selected property to the Item class, and keep track of the previously selected item to be able to deselect it when another item is selected.

Once again we start off with our ViewModel, first our properties:

private ObservableCollection<SelectableItemGroup> _selectableItemGroups;
public ObservableCollection<SelectableItemGroup> SelectableItemGroups
{
    get => _selectableItemGroups;
    set => SetProperty(ref _selectableItemGroups, value);
}

public ICommand ItemTappedCommand { get; }

And in the constructor we create our command. For this to work we create a local variable that we can use to capture in the command so we can keep track of the previously selected item:

SelectableItem previous = null;

ItemTappedCommand = new Command<SelectableItem>((item) =>
{
    if (previous != null)
        previous.Selected = false;
    previous = item;
    item.Selected = true;
});

Now we need a ValueConverter that can convert our Selected property into the right Color:

public class BoolToColorConverter: IValueConverter
{
    public Color TrueColor { get; set; }

    public Color FalseColor { get; set; }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value != null && value is bool boolValue)
            return boolValue ? TrueColor : FalseColor;
        return FalseColor;
    }

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

And once again we have everything setup to create our page:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:converters="clr-namespace:ColorChangeDemo.Converters"
             x:Class="ColorChangeDemo.Views.DeselectPage">
    <ContentPage.Resources>
        <ResourceDictionary>
            <converters:BoolToColorConverter x:Key="boolToColorConverter" TrueColor="Green" FalseColor="Gray" />
        </ResourceDictionary>
    </ContentPage.Resources>
    <ContentPage.Content>
        <ListView x:Name="ListView"  ItemsSource="{Binding SelectableItemGroups}" GroupShortNameBinding="{Binding Key}" GroupDisplayBinding="{Binding Key}" IsGroupingEnabled="True">
            <ListView.GroupHeaderTemplate>
                <DataTemplate>
                    <ViewCell>
                        <Label Text="{Binding Key}" />
                    </ViewCell>
                </DataTemplate>
            </ListView.GroupHeaderTemplate>
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <Button 
                            x:Name="Button"
                            Text="{Binding Id}" 
                            BackgroundColor="{Binding Selected, Converter={StaticResource boolToColorConverter}}"
                            Command="{Binding Source={x:Reference ListView}, Path=BindingContext.ItemTappedCommand}"
                            CommandParameter="{Binding .}"/>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </ContentPage.Content>
</ContentPage>

The upside of this option is that it doesn't traverse the entire list to make sure everything has the right Color.

Demo project

To be able to view this in action I've uploaded a small demo project where I implemented both solutions:

https://github.com/nknoop/ChangeColorDemo

0
votes

(See the answer which @Knoop gave for doing it in the right way with correct xamarin forms bindings and commands)

As I could accomplish my initial goal, which was to change the background color of each viewcell programmatically in code behind, I will post my result as an answer.

At first I learned that iterating over the viewcells is a bad idea because it's against the design pattern. So I used bindings, as well as a custom BoolToColor Converter to dynamically update the viewcells background color.

This is the code I have written:

Model class (Document):

   public class Document : BaseModel<int>, IDocument, INotifyPropertyChanged
    {
        #region INotifyPropertyChanged

        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
        {
            var changed = PropertyChanged;

            changed?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        #endregion // INotifyPropertyChanged

        public bool HasDefaultColor
        {
            get => hasDefaultColor;
            set
            {
                hasDefaultColor = value;
                OnPropertyChanged();
            }
        }
    }

The converter

public class BoolToColorConverter : IValueConverter (see question for detailed code...)

The implementation of the converter in xaml:

<Frame BackgroundColor="{Binding HasDefaultColor, Converter={converters:BoolToColorConverter}}"  Padding="0" HorizontalOptions="FillAndExpand" HasShadow="True">

And finally (which was the part where I got stuck with), properly updating the collection in the binded viewmodel in the OnTapped() method:

        private void DocumentViewCell_OnTapped(object sender, EventArgs e)
        {
            try
            {
                // Determine which document was selected
                if (sender.GetType() == typeof(ViewCell))
                {
                    ViewCell selectedViewCell = (ViewCell)sender;

                    if (selectedViewCell.BindingContext != null && selectedViewCell.BindingContext.GetType() == typeof(Document))
                    {
                        Document document = (Document)selectedViewCell.BindingContext;

                        if (document != null)
                        {
                            // Update default color (viewcell) for binded model
                            document.HasDefaultColor = !document.HasDefaultColor;

                            // Update backing field selectedDocument for correct bindings and to show correct detail page

                            ObservableCollection<Grouping<string, Document>> documents = viewModel.GroupedDocuments;
                            foreach (var group in documents)
                            {
                                foreach (var doc in group)
                                {
                                    if (doc.Name == document.Name)
                                    {
                                        doc.HasDefaultColor = document.HasDefaultColor;
                                    }
                                }
                            }

                            viewModel.GroupedDocuments = documents;
                            selectedDocument = document;
                        }
                    }
               }
           }
       }