Search code examples
c#wpfasynchronousobservablecollectioninotifycollectionchanged

ObservableCollection binding to dataTemplate with async data


I am very new in WPF, those are very confuse to me.

the main aim is I want a WPF program that fetches data every 5 seconds from WebAPI, and display the data.

I am using this MS sample code to modify: https://github.com/microsoft/WPF-Samples/blob/main/Data%20Binding/DataTemplatingIntro/MainWindow.xaml

I except the screen will print new async data on each second, but its always empty on the screen.

Below is my source:

// mainwindows.xaml
        <local:TaskViewModel x:Key="MyTodoList"/>
        <local:TaskListDataTemplateSelector x:Key="MyDataTemplateSelector"/>
//
<StackPanel>
                <TextBlock FontSize="20" Text="My Task List:"/>
                <ListBox Width="400" Margin="10"
             ItemsSource="{Binding Source={StaticResource MyTodoList}}"
             ItemTemplateSelector="{StaticResource MyDataTemplateSelector}"
             HorizontalContentAlignment="Stretch" 
             IsSynchronizedWithCurrentItem="True"/>
                <TextBlock FontSize="20" Text="Information:"/>
                <ContentControl Content="{Binding Source={StaticResource MyTodoList}}"
                    ContentTemplate="{StaticResource MyTaskTemplate}"/>
            </StackPanel>
// TaskViewModel.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Windows.Data;
using System.ComponentModel;

namespace demo_mah_wpf
{
    public class TaskViewModel : ObservableCollection<Task>, INotifyPropertyChanged, INotifyCollectionChanged
    {
        public event NotifyCollectionChangedEventHandler CollectionChanged;
        private ObservableCollection<Task> _Tasks;
        public ObservableCollection<Task> Tasks
        {
            get { return _Tasks; }
            set
            {
                _Tasks = value;
                //CollectionChanged(this, new PropertyChangedEventArgs());
            }
        }

        private static object _lock = new object();
        public TaskViewModel() : base()
        {
            //this.ctxFactory = ccf; //DB Context, using DI
            this.Tasks = new ObservableCollection<Task>();


            BindingOperations.EnableCollectionSynchronization(Tasks, _lock);

            // use async in .net framework 4.7.2
            // otherwise, complie error: async streams is not available in 7.3
            // https://bartwullems.blogspot.com/2020/01/asynchronous-streams-using.html

            System.Threading.Tasks.Task.Run(async () => {
                //await foreach (Task card in GetAllCards())
                //{
                //    this.Tasks.Add(card);
                //}
                var getTaskResult = this.GetAllTasks();
                if(getTaskResult != null && getTaskResult.Result != null)
                {
                    var getTaskResultList = getTaskResult.Result.ToList();
                    if (getTaskResultList.Count != 0)
                    {
                        foreach (Task task in getTaskResultList)
                        {
                            Add(new Task(task.TaskName, task.Description, task.Priority, task.TaskType));
                            CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add));
                        }
                        //Add(task);

                    }
                }
            });
        }

        private async System.Threading.Tasks.Task<IEnumerable<Task>> GetAllTasks()
        {
            List<Task> Items = new List<Task>();
            for (int i = 1; i <= 3; i++)
            {
                await System.Threading.Tasks.Task.Delay(1000);
                Items.Add(new Task("Development", "Write a WPF program", 2, TaskType.Home));
            }
            return Items;
        }
    }
}

I tried implement INotifyPropertyChanged, INotifyCollectionChanged for TaskViewModel. but still not work.

In debug, the program was ran the code in TaskViewModel.

thanks to Clemens, you help better understand WPF, the source was uploaded for who want it, please free feel to checkout https://github.com/dolphinotaku/demo-wpf/tree/812bd075487dd69a31c691b63fea6eef5931948f


Solution

  • Here is a complete and most simple example of updating a ListBox cyclically by a view model.

    The XAML simply sets the DataContext of the view to an instance of a view model class and assigns an appropriate Binding to the ItemsSource property of a ListBox:

    <Window.DataContext>
        <local:ViewModel/>
    </Window.DataContext>
    <Grid>
        <ListBox ItemsSource="{Binding Items}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding ItemData}"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
    

    The view model declares a read-only ObservableCollection property and sets up a timer that cyclically updates the collection:

    public class Item
    {
        public string ItemData { get; set; }
    }
    
    public class ViewModel
    {
        public ObservableCollection<Item> Items { get; }
            = new ObservableCollection<Item>();
    
        private readonly DispatcherTimer timer = new DispatcherTimer();
        private readonly Random random = new Random();
    
        public ViewModel()
        {
            timer.Interval = TimeSpan.FromSeconds(1);
            timer.Tick += TimerTick;
            timer.Start();
        }
    
        private void TimerTick(object sender, EventArgs args)
        {
            Items.Clear();
            Items.Add(new Item { ItemData = $"Item Data: {random.Next(100)}" });
            Items.Add(new Item { ItemData = $"Item Data: {random.Next(100)}" });
            Items.Add(new Item { ItemData = $"Item Data: {random.Next(100)}" });
            Items.Add(new Item { ItemData = $"Item Data: {random.Next(100)}" });
        }
    }
    

    All UI updates are triggered by the ObservableCollection object.

    If you now want to perform some time-consuming task asnychronously, you may declare the Tick event handler method async and await the task before updating the collection.