ObservableCollection项变更时ListView不更新UI问题求助
问题:ObservableCollection项更新后ListView未同步UI变化
我开发一款应用,需要下载文件并在ListView中展示,但遇到问题:ObservableCollection内的项已变更、数值已更新,但ListView未感知到变化,始终不更新UI。
已尝试的无效方案:
- 修改ObservableCollection时调用
[ListView].UpdateLayout(); - 在ViewModel和SoundBoardItem定义中实现INotifyPropertyChanged接口。
相关代码
SoundBoardItem类
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; SoundIconVisible = soundIconVisible ? "Visible" : "Collapsed"; // 简化写法 } public event PropertyChangedEventHandler PropertyChanged; public void OnPropertyChanged(string message) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(message)); } }
SoundBoardItemViewmodel类
public class SoundBoardItemViewmodel { public ObservableCollection<SoundBoardItem> SoundBoardItems = new(); }
页面内下载方法
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); System.Diagnostics.Debug.WriteLine(FilePath); try { streamManifest = await youtube.Videos.Streams.GetManifestAsync(Url); streamInfo = streamManifest.GetAudioOnlyStreams().GetWithHighestBitrate(); audioOnlyStreamInfo = await youtube.Videos.Streams.GetAsync(streamInfo); CurrentName = !string.IsNullOrEmpty(Name) ? Name : (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); // 此处变更未同步到UI System.Diagnostics.Debug.WriteLine("Progress: " + Convert.ToInt32(p * 100) + "%"); }); await youtube.Videos.Streams.DownloadAsync(streamInfo, FilePath + $"video_DEBUG.{streamInfo.Container}", ProgressHandler); // 以下属性变更均未同步到UI soundboardItem.ProgressRingIntermediate = true; soundboardItem.ProgressRingEnabled = false; soundboardItem.BtnEnabled = true; soundboardItem.SoundIconVisible = "Visible"; soundboardItem.SoundLocation = Url; } 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; } }
内存与UI差异截图


解决方法
1. 修复SoundBoardItem的属性通知
你虽然实现了INotifyPropertyChanged接口,但所有属性都是自动属性,没有在setter中触发OnPropertyChanged事件,导致UI无法感知属性变更。需要将每个绑定属性改为带私有字段的实现,示例如下:
private string _soundName; public string SoundName { get => _soundName; set { if (_soundName != value) { _soundName = value; OnPropertyChanged(nameof(SoundName)); } } } private int _progressRingProgress; public int ProgressRingProgress { get => _progressRingProgress; set { if (_progressRingProgress != value) { _progressRingProgress = value; OnPropertyChanged(nameof(ProgressRingProgress)); } } } // 所有需要绑定的属性都要按此方式修改,包括ProgressRingEnabled、BtnEnabled等
2. 将ViewModel中的集合改为属性
WPF/UWP的数据绑定系统只能识别属性,而你当前的SoundBoardItems是字段,需要改为属性:
public class SoundBoardItemViewmodel { public ObservableCollection<SoundBoardItem> SoundBoardItems { get; } = new ObservableCollection<SoundBoardItem>(); }
3. 确保在UI线程更新属性
Progress<double>的回调可能运行在后台线程,直接更新绑定属性会导致线程异常,也可能无法触发UI更新。需要切换到UI线程执行更新:
WPF环境
var ProgressHandler = new Progress<double>(p => { Application.Current.Dispatcher.Invoke(() => { soundboardItem.ProgressRingProgress = Convert.ToInt32(p * 100); }); System.Diagnostics.Debug.WriteLine("Progress: " + Convert.ToInt32(p * 100) + "%"); });
UWP环境
var ProgressHandler = new Progress<double>(p => { DispatcherQueue.TryEnqueue(() => { soundboardItem.ProgressRingProgress = Convert.ToInt32(p * 100); }); System.Diagnostics.Debug.WriteLine("Progress: " + Convert.ToInt32(p * 100) + "%"); });
原因说明
ObservableCollection仅会通知集合的增、删、移动等操作,集合内项的属性变更需要项自身通过INotifyPropertyChanged触发事件;- 绑定系统无法识别字段,必须使用属性才能建立正确的绑定关系;
- 非UI线程直接更新绑定属性会破坏UI线程独占规则,导致更新失效或异常。
内容的提问来源于stack exchange,提问作者Davide_24
相关产品推荐
相关产品推荐

