Search code examples
c#.netxamlwinui-3winui

WinUI Checkbox for First Item in ListView Doesn't Reflect Bound Property Changes from Server Sync


Description:

I'm experiencing a strange behavior in my WinUI 3 application with a ListView displaying a collection of items. The CheckBox in each item template is bound to a bool property called IsComplete. This is a multi-client, server-synchronized application where data changes can occur from different clients and are synced through the server.

Details:

  • When I check or uncheck the CheckBox for any item within the WinUI application, it works correctly and updates the IsComplete property as expected.
  • When changes are made to items in another client, and these changes are synced to the server and then pulled into this WinUI application, all items reflect the updates correctly in both the bound collection and the UI—except for the first item in the ListView.
  • The first item's CheckBox and TextBlock do not visually update to match the new value of IsComplete after a server sync, even though the bound data has been updated correctly (verified via debug step through).

Relevant Code

XAML
<ListView
    Grid.Row="1"
    Margin="10"
    ItemsSource="{x:Bind ViewModel.Items}"
    ScrollViewer.VerticalScrollBarVisibility="Auto">
    <ListView.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <CheckBox
                    Margin="10"
                    Command="{Binding ViewModel.EditItemCommand, ElementName=ThisPage}"
                    CommandParameter="{Binding Id}"
                    Content="{Binding Title}"
                    IsChecked="{Binding IsComplete}" />
                <TextBlock Margin="10" Text="Checked: " />
                <TextBlock Text="{Binding IsComplete}" />
            </StackPanel>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>
Bound collection
[ObservableProperty]
private ConcurrentObservableCollection<TodoItem> items = [];
Refresh command
[RelayCommand]
public async Task RefreshItemsAsync(CancellationToken cancellationToken = default)
{
    try
    {
        IsRefreshing = true;

        // Synchronize data with the remote service (if any).
        await service.SynchronizeAsync(cancellationToken);

        // Pull all items from the database.
        IEnumerable<TodoItem> itemsFromDatabase = await service.TodoItems.OrderBy(item => item.Id).ToListAsync(cancellationToken);

        // Replace all the items in the collection.
        Items.ReplaceAll(itemsFromDatabase);
        //Items.Clear();
        //_ = Items.AddRange(itemsFromDatabase);
    }
    catch (Exception ex)
    {
        NotificationHandler?.Invoke(this, new NotificationEventArgs(ex.GetType().Name, ex.Message, true));
    }
    finally
    {
        IsRefreshing = false;
        NotificationHandler?.Invoke(this, new NotificationEventArgs("Items Refreshed", "", false));
    }
}
Data model
public class TodoItem : OfflineClientEntity
{
    private bool _isComplete = false;
    public string Title { get; set; } = string.Empty;
    public bool IsComplete { get; set; } = false;


    public override string ToString()
        => JsonSerializer.Serialize(this);
}

using System;
using System.ComponentModel.DataAnnotations;

namespace TodoApp.WinUI3.Database;

/// <summary>
/// An abstract class for working with offline entities.
/// </summary>
public abstract class OfflineClientEntity
{
    [Key]
    public string Id { get; set; }
    public DateTimeOffset? UpdatedAt { get; set; }
    public string Version { get; set; }
    public bool Deleted { get; set; }
}

Observed Behavior

  • Clicking the CheckBox directly in the UI updates IsComplete and correctly reflects the change.
  • When another client modifies the first item and syncs the changes, and those changes are pulled into the WinUI application, the bound IsComplete property is correctly updated for the first item (verified with a TextBlock), but the CheckBox visual state does not reflect this change.
  • All other items in the list behave as expected, both when directly modified in the UI and when updated from server synchronization.

Solution

  • First of all, I'm not familiar with ConcurrentObservableCollection, so I'm assuming that it's based on ObservableCollection.

    Now, when you apply changes in your todo items, usually you need to implement INotifyPropertyChanged. You can do it by yourself but since you are using the CommunityToolkit.Mvvm, you can create an ObservableObject base class for TodoItem or just wrap it with a wrapper class.

    public partial class  TodoItemViewModel(TodoItem todoItem) : ObservableObject
    {
        private readonly TodoItem _todoItem = todoItem;
        public string Title
        {
            get => _todoItem.Title;
            set => SetProperty(_todoItem.Title, value, _todoItem, (model, value) => model.Title = value);
        }
        public bool IsComplete
        {
            get => _todoItem.IsComplete;
            set => SetProperty(_todoItem.IsComplete, value, _todoItem, (model, value) => model.IsComplete = value);
        }
    }