将数据加载到数据网格时 C#/WPF 主窗口冻结

Posted

技术标签:

【中文标题】将数据加载到数据网格时 C#/WPF 主窗口冻结【英文标题】:C#/WPF Main Window freezes when loading data into datagrid 【发布时间】:2021-11-15 21:38:18 【问题描述】:

伙计们,我目前正在使用 Prism 构建和 .NET 4.6 构建现有的 WPF 应用程序。我添加了一个包含一些用户控件的新页面。在其中一个用户控件中,我需要将包含几百行(始终低于千行)的 csv 加载到数据网格中。

每次我将数据加载到数据网格中时,整个应用程序都会冻结几秒钟。我试图与调度程序进行异步加载,但应用程序在加载时冻结。我也试过用一个任务来做,但我总是遇到异常(“只允许 UI 线程更改可观察集合”)

在我当前不起作用的异步实现下面,非常感谢任何帮助或想法。

谢谢

产品 XAML

<UserControl x:Class="Module.UserControls.Products"
             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:Module.UserControls"
             xmlns:mvvm="http://prismlibrary.com/"
             mvvm:ViewModelLocator.AutoWireViewModel="true"
             mc:Ignorable="d" 
             d:DesignHeight="650" d:DesignWidth="800" Background="DynamicResource Module.Background" MaxHeight="1500">
    <UserControl.Resources>
        <Style TargetType="DataGridCell">
            <Setter Property="VerticalAlignment" Value="Center" />
        </Style>
        <Style TargetType="TextBox">
            <Setter Property="VerticalContentAlignment" Value="Center" />
            <Setter Property="VerticalAlignment" Value="Center" />
            <Setter Property="FontSize" Value="16" />
            <Setter Property="Foreground" Value="DynamicResource Module.Textbox.Foreground" />
            <Setter Property="Margin" Value="5,5,5,5" />
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="true">
                    <Setter Property="ToolTip"
                            Value="Binding RelativeSource=x:Static RelativeSource.Self, Path=(Validation.Errors)[0].ErrorContent" />
                </Trigger>
            </Style.Triggers>
        </Style>
        <Style TargetType="CheckBox">
            <Setter Property="Margin" Value="5,5,5,5" />
            <Setter Property="HorizontalAlignment" Value="Left" />
            <Setter Property="VerticalAlignment" Value="Center" />
        </Style>
        <Style TargetType="Button">
            <Setter Property="FontSize" Value="16" />
            <Setter Property="FontWeight" Value="Normal" />
            <Setter Property="Background" Value="DynamicResource Module.Button.Background" />
            <Setter Property="Foreground" Value="White" />
        </Style>
        <Style TargetType="ComboBox">
            <Setter Property="Margin" Value="5,5,5,5" />
            <Setter Property="VerticalAlignment" Value="Center" />
            <Setter Property="IsSynchronizedWithCurrentItem" Value="True" />
        </Style>
    </UserControl.Resources>
    <Grid>
        <Grid Margin="0,20,0,0" Background="DynamicResource Module.Block.Background">
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="2*"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Label Grid.Column="0" Grid.Row="0">
                <TextBlock Text="Products" Style="DynamicResource Module.H2" />
            </Label>
            <ScrollViewer Grid.Column="0" Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" Margin="0,10,0,10" OverridesDefaultStyle="True" >
                <ScrollViewer.Resources>
                    <Style TargetType="x:Type ScrollBar">
                        <Setter Property="Background" Value="LightGray"/>
                    </Style>
                </ScrollViewer.Resources>
                <DockPanel  HorizontalAlignment="Stretch">
                    <DataGrid  AutoGenerateColumns = "False" IsReadOnly="False" ItemsSource="Binding ProductCollection, UpdateSourceTrigger=PropertyChanged, IsAsync=True, Mode=TwoWay" HorizontalAlignment="Center"  
                          Width="Auto" HorizontalContentAlignment="Center">
                        <DataGrid.Resources>
                            <Style  TargetType="DataGridCell">
                                <Setter Property="Foreground" Value="Black" />
                                <Setter Property="FontSize" Value="16" />
                                <Setter Property="BorderBrush" Value="White" />
                                <Style.Triggers>
                                    <Trigger Property="IsSelected" Value="True">
                                        <Setter Property="Background" Value="x:Null" />
                                        <Setter Property="BorderBrush" Value="x:Null" />
                                    </Trigger>
                                </Style.Triggers>
                            </Style>
                        </DataGrid.Resources>
                        <DataGrid.Columns>
                            <DataGridTextColumn Header = "Position" Binding="Binding Path=InProductPos, UpdateSourceTrigger=PropertyChanged" Width="Auto" MinWidth="120" />
                            <DataGridTextColumn Header = "Layout Position" Binding="Binding Path = InProductLayout, UpdateSourceTrigger=PropertyChanged" Width="Auto" MinWidth="120"/>
                            <DataGridTextColumn Header = "Name" Binding="Binding Path=InProductDescription, UpdateSourceTrigger=PropertyChanged" Width="Auto" MinWidth="300"/>
                            <DataGridTextColumn Header = "Product Quantity" Binding="Binding Path=InProductPieces, UpdateSourceTrigger=PropertyChanged" Width="Auto" MinWidth="120"/>
                            <DataGridTextColumn Header = "Class" Binding="Binding Path=InProductClass, UpdateSourceTrigger=PropertyChanged" Width="Auto" MinWidth="120"/>
                            <DataGridTextColumn Header = "Product Part ID" Binding="Binding Path=InProductPartID, UpdateSourceTrigger=PropertyChanged" Width="Auto" MinWidth="120"/>
                            <DataGridTextColumn Header = "Number" Binding="Binding Path=InProductNumber, UpdateSourceTrigger=PropertyChanged" Width="Auto" MinWidth="120"/>
                            <DataGridTextColumn Header = "Part list Name" Binding="Binding Path=InProductPartlistName, UpdateSourceTrigger=PropertyChanged" Width="Auto" MinWidth="300"/>
                        </DataGrid.Columns>
                    </DataGrid>
                </DockPanel>
            </ScrollViewer>
        </Grid>
    </Grid>
</UserControl>

按钮 XAML

<UserControl x:Class="Module.UserControls.ButtonRow"
             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:resx="clr-namespace:Module.Properties"
             xmlns:local="clr-namespace:Module.UserControls"
             xmlns:mvvm="http://prismlibrary.com/"
             mvvm:ViewModelLocator.AutoWireViewModel="true"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
        <BooleanToVisibilityConverter x:Key="BoolToVis" />
        <Style TargetType="DataGridCell">
            <Setter Property="VerticalAlignment" Value="Center" />
        </Style>
        <Style TargetType="TextBox">
            <Setter Property="VerticalContentAlignment" Value="Center" />
            <Setter Property="FontSize" Value="16" />
            <Setter Property="Foreground" Value="DynamicResource Module.Textbox.Foreground" />
            <Setter Property="Margin" Value="5,5,5,5" />
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="true">
                    <Setter Property="ToolTip"
                            Value="Binding RelativeSource=x:Static RelativeSource.Self, Path=(Validation.Errors)[0].ErrorContent" />
                </Trigger>
            </Style.Triggers>
        </Style>
        <Style x:Key="StyleButtonWhite" TargetType="Button">
            <Setter Property="Foreground" Value="White" />
        </Style>
        <Style x:Key="StyleButtonWhiteWhite" TargetType="Button">
            <Setter Property="Foreground" Value="White" />
            <Setter Property="Background" Value="x:Null" />
        </Style>
        <Style TargetType="Button">
            <Setter Property="FontSize" Value="16" />
            <Setter Property="FontWeight" Value="Normal" />
            <Setter Property="Background" Value="DynamicResource Module.Button.Background" />
            <Setter Property="Foreground" Value="White" />
        </Style>
        <Style TargetType="ComboBox">
            <Setter Property="Margin" Value="5,5,5,5" />
            <Setter Property="VerticalAlignment" Value="Center" />
        </Style>
    </UserControl.Resources>
    <Grid>
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,10">
            <Button
                Margin="12,0,12,0"
                Command="Binding LoadProductsCommand"
                Width="101" Height="40"
                Style="StaticResource StyleButtonWhite"
                Background="DynamicResource Module.Button.Background">
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="Import Products" Foreground="White" />
                </StackPanel>
            </Button>
        </StackPanel>
    </Grid>
</UserControl>

ViewModel 的一部分

public DelegateCommand LoadProductsCommand => new DelegateCommand(LoadProducts, () => true);

private ObservableCollection<Product> _productCollection;

        public ObservableCollection<Products> ProductCollection
        
            get => _productCollection;
            set
            
                _productCollection = value;
                OnPropertyChanged();
            
        

        public async void LoadProducts() 
        
            if (Dispatcher.CurrentDispatcher.CheckAccess())
            
               await Dispatcher.CurrentDispatcher.InvokeAsync(() =>
               
                   ProductCollection.AddRange(FileImport.ImportProducts());
               );
            
        

集合的类

        public string InProductPos
        
            get => _inProductPos;
            set
            
                _inProductPos = value;
                OnPropertyChanged();
            
        

        public string InProductLayout
        
            get => _inProductLayout;
            set
            
                _inProductLayout = value;
                OnPropertyChanged();
            
        

        public string InProductDescription
        
            get => _inProductDescription;
            set
            
                _inProductDescription = value;
                OnPropertyChanged();
            
        

        public int InProductPieces
        
            get => _inProductPieces;
            set
            
                _inProductPieces = value;
                OnPropertyChanged();
            
        

        public int InProductClass
        
            get => _inProductClass;
            set
            
                _inProductClass = value;
                OnPropertyChanged();
            
        

        public int InProductPartID
        
            get => _inProductPartID;
            set
            
                _inProductPartID = value;
                OnPropertyChanged();
            
        

        public string InProductNumber
        
            get => _inProductNumber;
            set
            
                _inProductNumber = value;
                OnPropertyChanged();
            
        

        public string InProductPartlistName
        
            get => _inProductPartlistName;
            set
            
                _inProductPartlistName = value;
                OnPropertyChanged();
            
        

【问题讨论】:

Dispatcher.InvokeAsync 在这里没有帮助,因为ProductCollection.AddRange 仍然在 UI 线程中运行。你应该使用这个:***.com/a/14602121/1136211, ***.com/a/39977500/1136211 或者你可以在工作线程中加载项目,存储它们,然后只在ui线程中添加它们。或者您可以异步加载它们并在 ui-thread 中一一添加。 我已经尝试了解决方案,但是当我尝试将项目加载到集合中时应用程序冻结 删除 UpdateSourceTrigger=PropertyChanged 并且在您了解它的作用之前不要再次使用它。 我会在不同的线程上获取数据。将其作为行视图模型列表返回。等待并返回 ui 线程,新建一个 observablecollection,传递 ctor 中的列表。然后将 productcollection 设置为那个。 【参考方案1】:

您可以尝试如下所示的方法。它在后台线程中加载 Product 集合,并且只将它们添加到 UI 线程中的 ObservableCollection 中。

public async Task LoadProducts() 

    var products = await Task.Run(() => FileImport.ImportProducts());
    
    ProductCollection.AddRange(products);

【讨论】:

ImportsProducts 方法加载 .csv 文件。 csv 的导入速度足够快,不会冻结应用程序。但正如您提到的,加载到可观察集合中似乎会冻结应用程序。 那么你应该使用 BindingOperations.EnableCollectionSynchronization。 或者将产品添加到多个合理大小的块中,并在其间添加一个短 await Task.Delay(...) 以让 UI 跟上。 一个短暂的任务延迟解决了 UI 冻结,同时在 Observable Collection 中添加另一个线程的项目。非常感谢!

以上是关于将数据加载到数据网格时 C#/WPF 主窗口冻结的主要内容,如果未能解决你的问题,请参考以下文章

将用户控件添加到网格异步[重复]

冻结wpf数据网格中的列

如何在 WPF 数据网格中显示 N 个项目?

内存映射文件到 WPF 网格

如何将选定的行从一个 wpf 数据网格复制到另一个 wpf 数据网格? [关闭]

wpf - 寻找在归档datagrid时不冻结ui的方法