MVVM应用中从Store获取数据后视图未更新问题排查
问题描述
在MVVM应用中,通过静态方法创建ViewModel并从Store加载数据、接收通知,但Store发出CodeTypesLoaded通知后视图未更新。Store使用了Lazy类,首次加载ViewModel时,静态构造方法先执行,随后OnCodeTypesLoaded被调用,但切换ViewModel时视图能正常显示。将XAML Binding的IsAsync设为true可解决问题,但会导致界面闪烁,并非最优解。ViewModel已实现OnPropertyChanged。
依赖注入代码
services.AddSingleton<CreateViewModel<ProductViewModel>>(services => () => CreateProductViewModel(services)); private static ProductViewModel CreateProductViewModel(IServiceProvider services) { return ProductViewModel.LoadProduct(services.GetRequiredService<IProductEndpoint>(),services.GetRequiredService<ICodeTypesStore>()); }
ProductViewModel代码
public class ProductViewModel : ViewModelBase { private readonly IProductEndpoint _productEndpoint; private readonly ICodeTypesStore _codeTypesStore; public ProductViewModel(IProductEndpoint productEndpoint, ICodeTypesStore codeTypesStore) { _productEndpoint = productEndpoint; _codeTypesStore = codeTypesStore; _codeTypesStore.CodeTypesLoaded += OnCodeTypesLoaded; LoadProductCommand = new LoadProductCommand(this, productEndpoint); LoadCodeTypesCommand = new LoadCodeTypesCommand(codeTypesStore); } private void OnCodeTypesLoaded() { SystemTypes = _codeTypesStore.SystemTypes(SistemTypes.IT_Management); Product.SetNonExistantIdentifiers(SystemTypes); } public ICommand LoadProductCommand { get; } public ICommand LoadCodeTypesCommand { get; } public static ProductViewModel LoadProduct(IProductEndpoint productEndpoint, ICodeTypesStore codeTypesStore) { ProductViewModel viewModel = new ProductViewModel(productEndpoint,codeTypesStore); viewModel.LoadProductCommand.Execute(null); viewModel.LoadCodeTypesCommand.Execute(null); return viewModel; } }
DisplayProduct类代码
public class DisplayProduct : UI.Product { public DisplayProduct(){} private List<UI.Sistem> _systems; public List<UI.Sistem> NonExistantIdentifiers { get; private set; } public DisplayProduct(UI.Product prd): base(prd.ID,prd.Name,prd.IsCompra,prd.IsProducao,prd.IsVenda,prd.Identifiers) { } public void SetNonExistantIdentifiers(List<UI.Sistem> systems) { _systems = systems; SetNonExistantIdentifiers(); } private void SetNonExistantIdentifiers() { NonExistantIdentifiers = _systems.Where(s => !base.Identifiers.Select(x => x.System.ID).ToList().Contains(s.ID)).ToList(); } }
ProductIDsNonLoaded用户控件XAML代码
<UserControl x:Class="WPFStuff.Controls.ProductIDsNonLoaded" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:WPFStuff.Controls" xmlns:helpers="clr-namespace:WPFStuff.Helpers" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> <ItemsControl ItemsSource="{Binding NonExistantIdentifiers}"> <ItemsControl.ItemTemplate> <DataTemplate> <WrapPanel> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" SharedSizeGroup="Label"/> </Grid.ColumnDefinitions> <TextBlock Text="{Binding Name}"/> </Grid> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" SharedSizeGroup="Value"/> </Grid.ColumnDefinitions> <TextBox MinWidth="150" helpers:SelectAllFocusBehavior.Enable="True"> <TextBox.Resources> <helpers:BindingProxy x:Key="TargetProxy2" Data="{Binding RegExpression}"/> </TextBox.Resources> <TextBox.Text> <Binding Path="Name" UpdateSourceTrigger="PropertyChanged"> <Binding.ValidationRules> <helpers:CodeTypeValidationRule> <helpers:CodeTypeValidationRule.CodeTypePattern> <helpers:CodeTypeValidationParameter RegExPattern="{Binding Data, Source={StaticResource TargetProxy2}}"/> </helpers:CodeTypeValidationRule.CodeTypePattern> </helpers:CodeTypeValidationRule> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> </Grid> </WrapPanel> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl>
ProductView用户控件XAML代码
<UserControl x:Class="WPFStuff.Views.ProductView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:WPFStuff.Views" xmlns:controls="clr-namespace:WPFStuff.Controls" xmlns:helpers="clr-namespace:WPFStuff.Helpers" mc:Ignorable="d" FontSize="20" d:DesignHeight="450" d:DesignWidth="800"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <controls:Product DataContext="{Binding Product}" /> <WrapPanel Grid.IsSharedSizeScope="True" Grid.Row="1"> <controls:ProductsIDs DataContext="{Binding Product}" /> <controls:ProductIDsNonLoaded DataContext="{Binding Product}"/> </WrapPanel> </Grid>
CodeTypesStore代码
public class CodeTypesStore : ICodeTypesStore { private readonly ICodeTypesEndpoint _codeTypesEndpoint; private readonly List<UI.Sistem> _systemCodeTypes; private readonly List<UI.CodeType> _productCodeTypes; private Lazy<Task> _loadCodeTypesLazy; public event Action CodeTypesLoaded; public CodeTypesStore(ICodeTypesEndpoint codeTypesEndpoint) { _codeTypesEndpoint = codeTypesEndpoint; _productCodeTypes = new(); _systemCodeTypes = new(); _loadCodeTypesLazy = CreateLoadCodeTypes(); } private bool _isLoading; public bool IsLoading { get { return _isLoading; } set { _isLoading = value; } } public async Task LoadCodeTypes() { await _loadCodeTypesLazy.Value; CodeTypesLoaded?.Invoke(); } private Lazy<Task> CreateLoadCodeTypes() { return new Lazy<Task>(() => InitializeCodeTypes()); } private async Task InitializeCodeTypes() { IEnumerable<UI.Sistem> s = await _codeTypesEndpoint.GetSystemTypesCodes(); IEnumerable<UI.CodeType> c = await _codeTypesEndpoint.GetProductsCodeTypes(); _systemCodeTypes.Clear(); _productCodeTypes.Clear(); _systemCodeTypes.AddRange(s); _productCodeTypes.AddRange(c); //CodeTypesLoaded?.Invoke(); } public List<UI.Sistem> SystemTypes(SistemTypes systemTypes) { return _systemCodeTypes.Where(sct => sct.SystemType?.SystemTypes == systemTypes).ToList(); } public IEnumerable<UI.CodeType> ProductCodeTypes => _productCodeTypes; }
解决方案
核心问题分析
- 线程上下文不匹配:
CodeTypesLoaded事件在后台线程触发(异步加载完成后默认在非UI线程执行),WPF数据绑定要求属性变更通知必须在UI线程发送,否则视图无法感知。 DisplayProduct未实现INotifyPropertyChanged:NonExistantIdentifiers重新赋值时没有触发属性变更通知,视图无法得知集合已更新。
具体修复步骤
1. 确保事件在UI线程触发
修改CodeTypesStore的LoadCodeTypes方法,将事件触发切换到UI线程:
public async Task LoadCodeTypes() { await _loadCodeTypesLazy.Value; // 通过Dispatcher切换到UI线程触发事件 Application.Current.Dispatcher.Invoke(() => CodeTypesLoaded?.Invoke()); }
2. 让DisplayProduct实现属性变更通知
修改DisplayProduct类,实现INotifyPropertyChanged接口并在属性赋值时触发通知:
public class DisplayProduct : UI.Product, INotifyPropertyChanged { public DisplayProduct(){} private List<UI.Sistem> _systems; private List<UI.Sistem> _nonExistantIdentifiers; public List<UI.Sistem> NonExistantIdentifiers { get => _nonExistantIdentifiers; private set { _nonExistantIdentifiers = value; OnPropertyChanged(nameof(NonExistantIdentifiers)); } } public DisplayProduct(UI.Product prd): base(prd.ID,prd.Name,prd.IsCompra,prd.IsProducao,prd.IsVenda,prd.Identifiers) { } public void SetNonExistantIdentifiers(List<UI.Sistem> systems) { _systems = systems; SetNonExistantIdentifiers(); } private void SetNonExistantIdentifiers() { NonExistantIdentifiers = _systems.Where(s => !base.Identifiers.Select(x => x.System.ID).ToList().Contains(s.ID)).ToList(); } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
3. 优化事件订阅避免内存泄漏(可选)
在ViewModel销毁时取消订阅CodeTypesLoaded事件:
public class ProductViewModel : ViewModelBase { // ... 原有代码 ... protected override void Dispose(bool disposing) { if (disposing) { _codeTypesStore.CodeTypesLoaded -= OnCodeTypesLoaded; } base.Dispose(disposing); } }
切换ViewModel时正常的原因
切换ViewModel时,新实例在UI线程完成初始化和绑定,此时CodeTypesLoaded事件触发时绑定上下文已完全建立,且可能巧合在UI线程执行,因此视图能正常更新。
内容的提问来源于stack exchange,提问作者Corneliu
相关产品推荐
相关产品推荐

