You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;
}

解决方案

核心问题分析

  1. 线程上下文不匹配:CodeTypesLoaded事件在后台线程触发(异步加载完成后默认在非UI线程执行),WPF数据绑定要求属性变更通知必须在UI线程发送,否则视图无法感知。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 00:38:14