0

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

Keith POON
  • 86
  • 6
  • You are getting it all wrong. The view model should not be derived from ObservableCollection. Instead of adding element to the base class instance, add them to `Tasks`. The ItemsSource Binding whould have `Path=Tasks`. – Clemens Jul 03 '23 at 09:28
  • I am writing angular before, I think the point that I miss was the data binding and life cycle. Can I bind multiple data results to the DataContext? Actually, there would be 3 WebAPIs that the program should call and display the latest returned data every 5 seconds. – Keith POON Jul 03 '23 at 09:31

1 Answers1

1

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.

Clemens
  • 123,504
  • 12
  • 155
  • 268
  • it is working, awesome. how do this work? is the binding will lookup from the current level resource and then go to the upper-level resource? – Keith POON Jul 04 '23 at 03:35
  • what if two local resources have the property with the same name? – Keith POON Jul 04 '23 at 03:36
  • the screen keeps un-humanity flashing because it clears the collection, and the ItemTemplateSelector I use will redraw the textbox, label... what do you think to resolve this? – Keith POON Jul 04 '23 at 03:44
  • assume get data every 5 seconds, maybe 10-30 records return, and only 12 records will show on the page, each page shows 10 seconds and then turn into the next page records. I think there will be an array/list storing the return data, a seconds timer trigger in every 10s, check on the returned data array/list, and display the data if it is not empty. should i don't clear the ObservableCollection, but only replace the value inside to resolve the flashing problem? – Keith POON Jul 04 '23 at 03:44
  • The Binding works because the DataContext is set to an instance of the view model. It has nothing to do with XAML resources. For the flickering, instead of replacing items in the collection, you may keep items and update their properties. The Item class must then implement INotifyPropertyChanged. – Clemens Jul 04 '23 at 06:46