4

I have a ListView which is bound to an ObservableCollection.

Is there a way to update a single cell whenever a property of a SomeModel item changed, without reloading the ListView by changing the ObservableCollection?

(Question is copied from https://forums.xamarin.com/discussion/40084/update-item-properties-in-a-listviews-observablecollection, as is my answer there.)

ToolmakerSteve
  • 18,547
  • 14
  • 94
  • 196

2 Answers2

14

As I can see you are trying to use MVVM as a pattern for your Xamarin.Forms app. You are already using the ObservableCollection for displaying a list of the data. When a new item is added or removed from collection UI will be refreshed accordingly and that is because the ObserverbleCollection is implementing INotifyCollectionChanged.

What you want to achieve with this question is next behaviour, when you want to change the particular value for the item in the collection and update the UI the best and simplest way to achieve that is to implement INotifyPropertyChanged for a model of the item from your collection.

Bellow, I have a simple demo example on how to achieve that, your answer is working as I can see but I am sure this example would be nicer for you to use it.

I have simple Button with command and ListView which holds my collection data.

Here is my page, SimpleMvvmExamplePage.xaml:

<StackLayout>

    <Button Text="Set status" 
            Command="{Binding SetStatusCommand}" 
            Margin="6"/>

    <ListView ItemsSource="{Binding Cars}"
              HasUnevenRows="True">

        <ListView.ItemTemplate>
            <DataTemplate>
                <ViewCell>

                    <StackLayout Orientation="Vertical" 
                                 Margin="8">

                        <Label Text="{Binding Name}" 
                               FontAttributes="Bold" />

                        <StackLayout Orientation="Horizontal">

                            <Label Text="Seen?" 
                                   VerticalOptions="Center"/>

                            <CheckBox IsChecked="{Binding Seen}"
                                      Margin="8,0,0,0" 
                                      VerticalOptions="Center" 
                                      IsEnabled="False" />
                        </StackLayout>

                     </StackLayout>

                </ViewCell>
            </DataTemplate>
          </ListView.ItemTemplate>
     </ListView>

 </StackLayout>

The basic idea from this demo is to change the value of the property Seen and set value for the CheckBox when the user clicks on that Button above the ListView.

This is my Car.cs class.

public class Car : INotifyPropertyChanged
{
    private string name;
    public string Name
    {
        get { return name; }
        set 
        { 
            name = value;
            OnPropertyChanged();
        }
    }

    private bool seen;
    public bool Seen
    {
        get { return seen; }
        set 
        { 
            seen = value;
            OnPropertyChanged();
        }
    }

    // Make base class for this logic, something like BindableBase
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

In the full demo example which is on my Github, I am using my BindableBase class where I handle raising the INotifyPropertyChanged when some property value is changed with this SetProperty method in the setter of the props.

You can find the implementation here: https://github.com/almirvuk/Theatrum/tree/master/Theatrum.Mobile/Theatrum.Mobile

The last thing to show is my ViewModel for this page, and inside of the ViewModel, I will change the value of Seen property to True for items in the collection when the user clicks on the Button above the ListView. Here is my SimpleMvvmExamplePageViewModel.cs

public class SimpleMvvmExamplePageViewModel
{
    public ObservableCollection<Car> Cars { get; set; }

    public ICommand SetStatusCommand { get; private set; }

    public SimpleMvvmExamplePageViewModel()
    {
        // Set simple dummy data for our ObservableCollection of Cars
        Cars = new ObservableCollection<Car>()
        {
            new Car()
            {
                Name = "Audi R8",
                Seen = false
            },

            new Car()
            {
                Name = "BMW M5",
                Seen = false
            },

            new Car()
            {
                Name = "Ferrari 430 Scuderia",
                Seen = false
            },

            new Car()
            {
                Name = "Lamborghini Veneno",
                Seen = false
            },

            new Car()
            {
                Name = "Mercedes-AMG GT R",
                Seen = false
            }
        };

        SetStatusCommand = new Command(SetStatus);
    }

    private void SetStatus()
    {
        Car selectedCar = Cars.Where(c => c.Seen == false)
            .FirstOrDefault();

        if (selectedCar != null)
        {
            // Change the value and update UI automatically
            selectedCar.Seen = true;
        }
    }
}

This code will help us to achieve this kind of behaviour: When the user clicks on the Button we will change value of the property of the item from collection and UI will be refreshed, checkbox value will be checked.

The final result of this demo could be seen on this gif bellow.

enter image description here

P.S. I could combine this with ItemTapped event from ListView but I wanted to make this very simple so this example is like this.

Hope this was helpful for you, wishing you lots of luck with coding!

Almir Vuk
  • 2,983
  • 1
  • 18
  • 22
6

Any UI associated with a model item will be refreshed, if replace the item with itself, in the Observable Collection.

Details:

In ViewModel, given property:

public ObservableCollection<Item> Items { get; set; } = new ObservableCollection<Item>();

Where Item is your model class.
After adding some items (not shown), suppose you want to cause item "item" to refresh itself:

public void RefreshMe(Item item)
{
    // Replace the item with itself.
    Items[Items.IndexOf(item)] = item;
}

NOTE: The above code assumes "item" is known to be in "Items". If this is not known, test that IndexOf returns >= 0 before performing the replacement.

In my case, I had a DataTemplateSelector on the collection, and the item was changed in such a way that a different template was required. (Specifically, clicking on the item toggled it between collapsed view and expanded/detailed view, by the TemplateSelector reading an IsExpanded property from the model item.)

NOTE: tested with a CollectionView, but AFAIK will also work with the older ListView class.
Tested on iOS and Android.

Technical Note:
This replacement of an item presumably triggers a Replace NotifyCollectionChangedEvent, with newItems and oldItems both containing only item.

ToolmakerSteve
  • 18,547
  • 14
  • 94
  • 196