0

I'm trying to make a UI with list of data(measurements) that changes every second.

I have a model:

namespace DebugApp.Model;
public class Channel
{
    public byte Id { get; set; }
    public string FwVersion { get; set; }
    public int Measurement1{ get; set; }
    public int Measurement2 { get; set; }
}

and a list of data in ViewModel:

namespace DebugApp.ViewModel;
public class ViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private ObservableCollection<Channel> channelInfo = new ObservableCollection<Channel>();
    public ObservableCollection<Channel> ChannelInfo
    {
        get => channelInfo;
        set
        {
            OnPropertyChanged();
        }
    }
    public void OnPropertyChanged([CallerMemberName] string name = null) =>
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

The init of UI:

namespace DebugApp.View;
public partial class MainPage : ContentPage
{
  ObservableCollection<Channel> channelInfo = new ObservableCollection<Channel>();
  ViewModel viewModel = new ViewModel();
  public MainPage()
  {
  InitializeComponent();
  ..
  }
..

To initially make the first display of data in the list, I call this function:

 private void GetAllChannelsInfo()
 {
    ushort[] registerContent;
    channelInfo.Clear();
    for (byte channel = 1; channel <= myReader.noOfConnectedChannels; channel++)
    {
      registerContent = myReader.ReadRegisters(channel, myReader.RegVersionMajor, 7);
      if (registerContent != null)
      {
        channelInfo.Add(new Channel()
        {
          Id = channel,
          FwVersion = registerContent[0].ToString() + "." +
          registerContent[1].ToString() + "." +
          registerContent[2].ToString(),
          Meas1 = (Int16)registerContent[5],
          Meas2 = (Int16)registerContent[6],
        });
      }
    }
    viewModel.ChannelInfo = channelInfo;
    BindingContext = this.viewModel;
  }

In the XAML file:

..
    xmlns:viewmodel="clr-namespace:DebugApp.ViewModel">
..
    <ContentPage.BindingContext>
       <viewmodel:ViewModel/>
    </ContentPage.BindingContext>
..
               <ListView ItemsSource="{Binding ChannelInfo}">
                    <ListView.ItemTemplate>
                        <DataTemplate>
                            <ViewCell>
                                <StackLayout Spacing="5"
                                             Orientation="Horizontal">
                                    <Label Text="{Binding Id,
                                        StringFormat='Channel {0}:    '
                                        }"
                                           WidthRequest="85"/>
                                    <Label Text="{Binding FwVersion,
                                        StringFormat='FwVer: {0}, '
                                        }"
                                           WidthRequest="100"/>
                                    <Label Text="{Binding Meas1,
                                        StringFormat='M1: {0}, '
                                        }"
                                           WidthRequest="140"/>
                                    <Label Text="{Binding Meas2,
                                        StringFormat='M2: {0}'
                                        }"
                                           WidthRequest="140"/>
                                </StackLayout>
                            </ViewCell>
                        </DataTemplate>
                    </ListView.ItemTemplate>
                </ListView>

This works and the data are shown, but then after the initialization of the table/list, then once a second I a function where I only change the value of channelInfo[x].Meas1 and Meas2:

  private void GetChannelsUpdate()
  {
..
    channelInfo[channel].Meas1 = someNumber;
    channelInfo[channel].Meas2 = someOtherNumber;
..    
    viewModel.ChannelInfo = channelInfo;
    BindingContext = this.viewModel;
  }

But the data are not being updated in my UI. I have tried to debug this and can see that the data are being updated in ViewModel and the OnPropertyChanged() are called, but the data in the UI are not changed.

So, why aren't the List being updated when the data inside are changed?

3 Answers3

1

Whatever you are updating needs to implement some INotify interface. So if you change any properties of a Channel object, it needs to implement INotifyPropertyChanged, and raise the event when the property is changed.

The observable collection only cares about objects being added/removed/reordered. It does not know, nor care, if objects inside the list is modified.

public class Channel : INotifyPropertyChanged
{
    private string fwVersion;
    private int measurement1;
    private int measurement2;
    public byte Id { get; init; }

    public string FwVersion
    {
        get => fwVersion;
        set => SetField(ref fwVersion, value);
    }

    public int Measurement1
    {
        get => measurement1;
        set => SetField(ref measurement1, value);
    }

    public int Measurement2
    {
        get => measurement2;
        set => SetField(ref measurement2, value);
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }
}
JonasH
  • 28,608
  • 2
  • 10
  • 23
  • Thanks Jonas. I am quite a newbie on MVVM and C#. So my question might be dumb :-) But the INotifyPropertyChanged are here: 'public class ViewModel : INotifyPropertyChanged' and I call the 'OnPropertyChanged();' to update. Could you please make a small example on how I should make the data update? – M. Herrmann Mar 17 '23 at 08:43
  • @M.Herrmann *every* object that changes need to implement INotifyPropertyChanged (or equivalent interface), it is not sufficient to have it on the root object. Added an example, note that some refactoring tools can do most of the implementation for you. – JonasH Mar 17 '23 at 08:50
  • Thanks @JonasH! I will try this. Using a refactoring tool for this would probably be a good idea. What tool would you recommend? – M. Herrmann Mar 17 '23 at 09:05
0

In the maui, you can use the CommunityToolkit.Mvvm nuget package to save a lot of time.

  1. Add the CommunityToolkit.Mvvm nuget package in your project with the Nuget package manager.
  2. Change your Channel class to:
public partial class Channel : ObservableObject
{
    [ObservableProperty]
    public byte id;
    [ObservableProperty]
    public string fwVersion;
    [ObservableProperty]
    public int measurement1;
    [ObservableProperty]
    public int measurement2;
}
  1. Change your ViewModel class:
public class ViewModel : ObservableObject
{
    private ObservableCollection<Channel> channelInfo = new ObservableCollection<Channel>();
}

And then you can run your project to check it.

For more information, you can refer to the official document about the CommunityToolkit.Mvvm and this case which show an example about using it.

There is also an example about .net maui cannot get updated fields in an Observable collection to update in bound collection view

Liyun Zhang - MSFT
  • 8,271
  • 1
  • 2
  • 14
0

I have now rewritten and simplified the test code based on the inputs from @Liyun Zhang - MSFT and @JonasH. Thanks! It is working, with two initial channels, where the counter channel are updated every second. The code now looks like this:

View:

<ContentPage 
x:Class="MVVM_Playground.View.MainPage"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:model="clr-namespace:MVVM_Playground.Model"
xmlns:viewmodel="clr-namespace:MVVM_Playground.ViewModel"
x:DataType="viewmodel:BaseViewModel">

<ScrollView>
    <StackLayout
        Margin="0,5,0,5">
        <Label
            x:Name="lblChannnelInfo"
            Text="Channel Info list:"
            TextColor="Gray"/>
        <ListView ItemsSource="{Binding Channels}"
        RowHeight="25">
            <ListView.ItemTemplate>
                <DataTemplate x:DataType="model:Channel">
                    <ViewCell>
                        <StackLayout Spacing="5"
                                     Orientation="Horizontal">
                            <Label Text="{Binding Id,
                                        StringFormat='Channel {0}:    '
                                        }"
                                   WidthRequest="85"/>
                            <Label Text="{Binding FwVersion,
                                        StringFormat='FwVer: {0}, '
                                        }"
                                   WidthRequest="100"/>
                            <Label Text="{Binding Counter,
                                        StringFormat='Counter: {0}, '
                                        }"
                                   WidthRequest="140"/>
                        </StackLayout>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </StackLayout>
</ScrollView>

...

namespace MVVM_Playground.View;
public partial class MainPage : ContentPage
{
    public MainPage(BaseViewModel viewModel)
    {
        BindingContext = viewModel;
        InitializeComponent();
    }
}

The ViewModel:

namespace MVVM_Playground.ViewModel;
public partial class BaseViewModel : ObservableObject
{
    [ObservableProperty]
    private ObservableCollection<Channel> channels;
    public BaseViewModel()
    {
        Channels = new ObservableCollection<Channel>()
        {
            new Channel()
            {
                Id = 1,
                FwVersion = "1.2.3",
                Counter = 0
            },
            new Channel()
            {
                Id = 2,
                FwVersion = "1.2.3",
                Counter = 0
            },
        };
        // Create a timer tick routine
        IDispatcherTimer tickTimer;
        tickTimer = Application.Current.Dispatcher.CreateTimer();
        tickTimer.Interval = TimeSpan.FromMilliseconds(1000);
        tickTimer.Tick += Timer_Tick;
        tickTimer.Start();
    }
    private void Timer_Tick(object sender, EventArgs e)
    {
        Channels[0].Counter += 1;
        Channels[1].Counter += 2;
    }
} 

And The model:

namespace MVVM_Playground.Model;
public partial class Channel : ObservableObject
{
    [ObservableProperty]
    public byte id;
    [ObservableProperty]
    private string fwVersion;
    [ObservableProperty]
    private int counter;
}