Triggering PropertyChanged on a ListView's ItemsSource doesn't update if the source hasn't changed

cmeerencmeeren USMember ✭✭

The code below is simplified and will not run, but I hope it serves to get the message across.

Consider the following model of some project where EnteredAt is the time you started working on the project (ObservableObject is an implementation of INotifyPropertyChanged which I know is working):

public class Project : ObservableObject
{
    private DateTime enteredAt;

    public DateTime EnteredAt
    {
        get { return enteredAt; }
        set { SetAndNotify(ref enteredAt, value); }
    }
    public string Name { get; set; }
}

The projects are shown in a ListView. Here's the simplified XAML (no significant code-behind). TimeSinceEnteredAt is a value converter which converts a DateTime (EnteredAt) to a string showing the hours, minutes, and seconds since that time (i.e., it simply formats DateTime.Now - enteredAt).

<ContentPage>
  <ListView ItemsSource="{Binding Projects}">
    <ListView.ItemTemplate>
      <DataTemplate>
        <ViewCell>
          <StackLayout Orientation="Horizontal">
            <Label Text="{Binding Name}" />
            <Label Text="{Binding EnteredAt, Converter={StaticResource TimeSinceEnteredAt}}" />
          </StackLayout>
        </ViewCell>
      </DataTemplate>
    </ListView.ItemTemplate>
  </ListView>
</ContentPage>

The view is bound to the following view model. In the constructor, I start a timer which should have the effect of continually updating the ListView so that 0h0m0s changes to 0h0m1s, etc. That is where I'm stuck.

public class ProjectListPageModel : ObservableObject
{
    private ObservableCollection<Project> projects;

    public ObservableCollection<Project> Projects
    {
        get { return projects; }
        set { SetAndNotify(ref projects, value); }
    }

    public ProjectListPageModel()
    {
        // Code here to populate Projects

        // HELP: This is where I'm stuck.
        Device.StartTimer(TimeSpan.FromSeconds(1), () =>
        {
            // METHOD 1: DOES NOT WORK
            // Trigger PropertyChanged on Projects to tell the ListView it's been updated
            NotifyPropertyChanged(nameof(Projects));
            return true;

            // METHOD 2: WORKS
            // Set Projects to a copy of itself.
            Projects = new ObservableCollection<Project>(Projects);
            return true;

            // METHOD 3: WORKS
            // Trigger PropertyChanged on each Project to tell the ListView it's been updated
            // This requires ObservableObject's NotifyPropertyChanged method to be public
            // (it was protected before), or a new "proxy" method on Project which can be called here
            foreach (Project p in Projects) { 
                p.NotifyPropertyChanged(nameof(p.EnteredAt));
            }
            return true;
        });
    }
}

When I trigger PropertyChanged on the ListView's ItemsSource (method 1), I would expect the ListView to redraw, just like it does with method 2. Why doesn't that happen? Does Xamarin.Forms perform an extra check to see if it has really changed, even when I manually trigger PropertyChanged? And is there a more elegant way to solve my problem than method 3?

Posts

  • ClintStLaurentClintStLaurent USUniversity ✭✭✭✭✭

    That's correct. The property is the ObservableCollection, so property changed event is for when the entire Collection changed through the property's set method. you have to use the CollectionChanged event if you want to be notified of an item WITHIN the collection.

  • BillyMartinBillyMartin USMember ✭✭✭
    edited May 2018

    I have a similar issue @ClintStLaurent and @JamesMontemagno , but I'm using MVVMHelpers. I run this code in my ViewModel, but the change won't take effect until I back out of the page and then re-enter. It dims out the label in a listview if the user hasn't paid for Premium:

     private void ExecuteUpdatePremiumSettingsCommand()
            {
                if (App.Premium)
                    Opacity = 1.0;
                else
                    Opacity = 0.5;
    
                foreach (var item in BoardList)
                {
                    if (item.PremiumFeature)
                        item.TextOpacity = Opacity;
                    else
                    item.TextOpacity = 1.0;
                }
            }
    
Sign In or Register to comment.