Search code examples
c#windowsuser-interfacelistviewwinui-3

ListView does not update when item inside ObservableCollection is changed


I am currently trying to download a file and show that in my app inside a ListView. I am having one issue with this. The ObservableCollection does get changed and the values are different. The ListView, however, does not know it changed and never updates the UI to show the changes.

What I've tried so far that did not work:

  • using [ListView].UpdateLayout() when changing the ObservableCollection;
  • using INotifyPropertyChanged in both the ViewModel and the definition for SoundBoardItem.

Code:

    public class SoundBoardItem : INotifyPropertyChanged
    {
        public string SoundName { get; set; }
    
        public string SoundLocation { get; set; }
        public string SoundLocationIcon { get; set; }
        public string SoundIconColor { get; set; }
        public string SoundIconVisible { get; set; }
        public string SoundIconTooltip { get; set; }
    
        public string SoundKeybindForecolor { get; set; }
        public string SoundKeybind { get; set; }
        public bool ProgressRingEnabled { get; set; }
        public bool ProgressRingIntermediate { get; set; }
        public int ProgressRingProgress { get; set; }
        public bool BtnEnabled { get; set; }
    
        public SoundBoardItem(string soundName, string soundLocation, string soundLocationIcon, bool soundIconVisible, string soundIconTooltip, string soundKeybind, bool progressRingEnabled, bool btnEnabled, bool progressRingIntermediate = true, int progressRingProgress = 100, string soundIconColor = null, string soundKeybindForecolor = null)
        {
        
            SoundName = soundName;
            SoundLocation = soundLocation;
            SoundLocationIcon = soundLocationIcon;
            SoundIconColor = soundIconColor;
            SoundIconTooltip = soundIconTooltip;
            SoundKeybindForecolor = soundKeybindForecolor;
            SoundKeybind = soundKeybind;
            ProgressRingEnabled = progressRingEnabled;
            ProgressRingIntermediate = progressRingIntermediate;
            ProgressRingProgress = progressRingProgress;
            BtnEnabled = btnEnabled;
            if (soundIconVisible == true) { SoundIconVisible = "Visible"; } else { SoundIconVisible = "Collapsed"; } // ease of use
        }

    public event PropertyChangedEventHandler PropertyChanged; // Also tried to use this in the ViewModel, but it didn't work either.
    public void OnPropertyChanged(string message)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(message));
        }
    }
}
public class SoundBoardItemViewmodel
{
    public ObservableCollection<SoundBoardItem> SoundBoardItems = new();
}

(Inside Page)

private async void DownloadSoundFile(object sender, RoutedEventArgs e, string Name, string Url)
    {
        if (soundBoardItemViewmodel.SoundBoardItems.Any(x => x.SoundLocation == Url || x.SoundLocation == "Downloading... (" + Url + ")")) return;

        YoutubeClient youtube = new();

        StreamManifest streamManifest;
        IStreamInfo streamInfo;
        Stream audioOnlyStreamInfo;
        string CurrentName;

        var FilePath = AppDomain.CurrentDomain.BaseDirectory + "DownloadedSounds\\";

        if (!Directory.Exists(FilePath))
            Directory.CreateDirectory(FilePath);

        // print file path to debug output
        System.Diagnostics.Debug.WriteLine(FilePath);


        try
        {
            streamManifest = await youtube.Videos.Streams.GetManifestAsync(Url);
            streamInfo = streamManifest.GetAudioOnlyStreams().GetWithHighestBitrate();
            audioOnlyStreamInfo = await youtube.Videos.Streams.GetAsync(streamInfo);

            if (Name != "")
                CurrentName = Name;
            else
                CurrentName = (await youtube.Videos.GetAsync(Url)).Title;

            SoundBoardItem soundboardItem = new(CurrentName, "Downloading... (" + Url + ")", DownloadedFileIcon, false, "Downloaded File", "None", true, false, false, 0);
            soundBoardItemViewmodel.SoundBoardItems.Add(soundboardItem);

            var ProgressHandler = new Progress<double>(p => 
            { 
                soundboardItem.ProgressRingProgress = Convert.ToInt32(p * 100); // same here; Changes do not show up in UI.
                System.Diagnostics.Debug.WriteLine("Progress: " + Convert.ToInt32(p * 100) + "%"); 
            });

            await youtube.Videos.Streams.DownloadAsync(streamInfo, FilePath + $"video_DEBUG.{streamInfo.Container}", ProgressHandler);

            soundboardItem.ProgressRingIntermediate = true;
            soundboardItem.ProgressRingEnabled = false;
            soundboardItem.BtnEnabled = true;
            soundboardItem.SoundIconVisible = "Visible";
            soundboardItem.SoundLocation = Url; // Values get changed but the changes do not appear.
        }
        catch (Exception ex)
        {
            await ShellPage.g_AppMessageBox.ShowMessagebox("Download Error", "An error has occured while downloading the youtube video: " + Url + "\n\nPlease make sure that the URL is correct and links to a valid youtube video.\nPlease note that age restricted videos are not supported.\n\nException: " + ex.Message, "", "", "Okay", ContentDialogButton.Close);
            return;
        }



    }

(Images showing difference in memory and UI) Image of the listview item in memory

Image of the listview item


Solution

  • As it's mentioned in the comments, you need to issue PropertyChanged on each setter. You also need fields for each property. This shows how to use INotifyPropertyChanged.

    But I recommend you to use the CommunityToolkit.Mvvm NuGet package. It brings source generators that create INotifyPropertyChanged related boilerplate code for you. Your code should look something like this:

    // The class needs to be `partial`.
    public partial SoundBoardItem : ObservableProperty
    {
        // The source generators will create a "SoundName" property 
        // implemented with INotifyPropertyChanged for you.    
        [ObservableProperty]
        private string _soundName;
    
        [ObservableProperty]
        private string _soundLocation;
    
        // The rest of your code here...
    }