PowerShell结合WPF DataGrid加载大量数据时界面卡顿问题求助
问题描述
公司内存在创建带变音符号用户、已婚用户重命名samaccountname属性等场景,需要开发一个GUI工具用于创建/更新相关内容。当前尝试在WPF DataGrid中展示约1800个AD组(通过循环模拟),并使用<ScrollViewer/>实现滚动,但在加载、筛选、选择条目时界面出现明显卡顿。已提供相关WPF和PowerShell代码,此前的方案不符合实际环境,需要找到PowerShell环境下的有效优化方案。
WPF代码
<controls:MetroWindow x:Class="AdminPanel_WPFNET.Test_MassiveDataGrid" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:AdminPanel_WPFNET" mc:Ignorable="d" Title="Test_MassiveDataGrid" Height="450" Width="800" xmlns:controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro" xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"> <Window.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" /> <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" /> <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Red.xaml" /> </ResourceDictionary.MergedDictionaries> <BooleanToVisibilityConverter x:Key="BoolToVis" /> </ResourceDictionary> </Window.Resources> <Grid> <StackPanel> <!-- Grid Columns for having the Label on the right... --> <Grid Margin="0,0,0,3"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <TextBox x:Name="TxbUserUCGroupFilter" Grid.Column="0" Background="Transparent" Margin="0,0,0,3" controls:TextBoxHelper.UseFloatingWatermark="True" controls:TextBoxHelper.ClearTextButton="True" controls:TextBoxHelper.Watermark="Group Filter"> <TextBox.ToolTip> <StackPanel> <TextBlock FontWeight="Bold" FontSize="14" Margin="0,0,0,5">Group Filter Search!</TextBlock> <WrapPanel> <TextBlock> You can quickly search for a group. Might be a bit too much. Who knows. <LineBreak /> Action was Enter-Button; Just type the name. Less names, the faster. </TextBlock> </WrapPanel> <Border BorderBrush="Silver" BorderThickness="0,1,0,0" Margin="0,8" /> <WrapPanel> <TextBlock FontWeight="Bold" FontStyle="Italic" Foreground="{DynamicResource MahApps.Brushes.Accent}"> ENTER </TextBlock> </WrapPanel> </StackPanel> </TextBox.ToolTip> </TextBox> <Label x:Name="LblUserUCGroupCounter" Grid.Column="1" Content="Count Groups: 0"/> </Grid> <ScrollViewer Height="300"> <DataGrid d:ItemsSource="{d:SampleData ItemCount=3}" x:Name="DgrUserUCGroupMemberships" VirtualizingStackPanel.IsVirtualizing="True" AutoGenerateColumns="True" SelectionMode="Single" IsReadOnly="True" Focusable="False" Background="Transparent"> <DataGrid.RowStyle> <Style TargetType="DataGridRow"> <Setter Property="Background" Value="Transparent" /> <Setter Property="Cursor" Value="Hand" /> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="Background" Value="Blue" /> </Trigger> <Trigger Property="IsSelected" Value="True"> <Setter Property="Background" Value="Green" /> <Setter Property="Foreground" Value="Yellow" /> </Trigger> </Style.Triggers> </Style> </DataGrid.RowStyle> <DataGrid.CellStyle> <Style TargetType="DataGridCell"> <Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="Background" Value="Transparent" /> <Setter Property="BorderThickness" Value="0" /> </Style> </DataGrid.CellStyle> <DataGrid.Resources> <Style TargetType="DataGridColumnHeader"> <Setter Property="Background" Value="Transparent" /> <Setter Property="BorderThickness" Value="0" /> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="Background" Value="Red" /> </Trigger> </Style.Triggers> </Style> </DataGrid.Resources> <DataGrid.Columns> <DataGridTextColumn Header="IsMemberText" Binding="{Binding IsMember}" /> <DataGridCheckBoxColumn Header="IsMemberCHK" Binding="{Binding IsMember}" /> <DataGridTextColumn Width="*" Header="GroupName" Binding="{Binding Name}" /> </DataGrid.Columns> </DataGrid> </ScrollViewer> </StackPanel> </Grid> </controls:MetroWindow>
PowerShell代码
$ErrorActionPreference = "Stop" #region Paths # Just test, so no ScriptRoot for now. $IsDomain = try { (Get-CimInstance -Class Win32_ComputerSystem).PartOfDomain, (Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain | Select-Object -First 1 } catch {} $BasePath = if (!$IsDomain) { "C:\dev\Projects\Admin-Tools-2022\AdminPanel\AdminPanel" } else { "C:\dev\Projekte\2022_Admin_Tools\AdminPanel\AdminPanel" } $ProjectPath = "$BasePath\Interface\AdminPanel_WPFNET" $XAMLFile = Get-Item "$ProjectPath\Test_MassiveDataGrid.xaml" $AssemblyLocation = "$ProjectPath" #endregion #region Load Assemblies needed for testing $null = [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") $null = [System.Reflection.Assembly]::LoadWithPartialName("PresentationFramework") foreach ($Assembly in (Get-ChildItem $AssemblyLocation -Filter *.dll -Recurse)) { Write-Host "Load assembly: $($Assembly.FullName)" -ForegroundColor Yellow try { $null = [System.Reflection.Assembly]::LoadFrom($Assembly.FullName) } catch { Write-Host "Error loading '$($Assembly.Name)': $($_.Exception.Message)" -BackgroundColor Black -ForegroundColor Red } } #endregion #region Load GUI [xml]$XAML = ( (Get-Content -Path $XAMLFile.FullName -Encoding UTF8) ) $AttributesToRemove = @( 'x:Class', 'mc:Ignorable' ) @( "Window", "MetroWindow" ) | ForEach-Object { Write-Verbose "Removing properties for '$_' ..." # Standard: Window, main is MetroWindow this time foreach ($Attrib in $AttributesToRemove) { if ($XAML."$_") { if ($XAML."$_".GetAttribute($Attrib)) { $XAML."$_".RemoveAttribute($Attrib) } } else { if ($ErrorWhenNotMetro) { Write-Host "We do not have a MetroWindow property" return } } } } # Removing attributes PowerShell cannot load, like the functions called in C#. $RemoveProperties = @( "SelectionChanged" "TextChanged" "Click" "Checked" "Unchecked" # not UnChecked #"d:ItemsSource" # error! "ItemsSource" ) $XPathString = ($RemoveProperties | ForEach-Object { "@$_" }) -join " or " Write-Host "Loading XPATH: $XPathString ..." -ForegroundColor Cyan $XAML.SelectNodes("//*[$XPathString]") | ForEach-Object { $Node = $_ $NodeName = $_.Name # e.g.: comboboxName $RemoveProperties | ForEach-Object { if ($Node.GetAttribute("$_") ) { Write-Host "Remove attribute '$_' from '$NodeName'" -ForegroundColor Yellow $Node.RemoveAttribute("$_") } } } # Sometimes I test things in WPF with SampleData. d:ItemsSource cannot be "translated" by PowerShell by default. # So I read all d:ItemsSources and remove them this way. Array like above does not work!! $XAML.SelectNodes("//*[@*[contains(translate(name(.),'n','N'),'d:ItemsSource')]]") | ForEach-Object { $Node = $_ $NodeName = $_.Name # e.g.: DgrUserUCGroupMemberships if ($Node.GetAttribute("d:ItemsSource")) { Write-Host "Remove attribute '$_' from '$NodeName'" -ForegroundColor Magenta $Node.RemoveAttribute("d:ItemsSource") } } Write-Host "/end d:ItemsSource" -ForegroundColor Cyan # Load XAML after polishing XML-Code $Reader = (New-Object System.Xml.XmlNodeReader $XAML) $Window = [Windows.Markup.XamlReader]::Load($Reader) # Load all variables needed. $XAML.SelectNodes("//*") | ForEach-Object { try { Set-Variable -Name $_.Name -Value $Window.FindName($_.Name) -Force -Scope Script } catch { Write-Host "NodeError: $($_.Exception.Message)" -BackgroundColor Black -ForegroundColor Red } } #endregion #region Actual Functions + Code $ADGroups = 1..1800 | ForEach-Object { [PSCustomObject]@{ "Name" = "Group $_" "IsMember" = ($true, $false | Get-Random) } } $DgrUserUCGroupMemberships.AutoGenerateColumns = $false $DgrUserUCGroupMemberships.ItemsSource = $ADGroups $LblUserUCGroupCounter.Content = "Group Counter: $($ADGroups.Count)" $TxbUserUCGroupFilter.Add_TextChanged({ # Filtering to input text! $FilterText = "" if ($TxbUserUCGroupFilter.Text) { $FilterText = $TxbUserUCGroupFilter.Text } $FilteredContent = $ADGroups | Where-Object { $_.Name -match "$FilterText" } | Select-Object -First 50 # Converting single object into array!! if (($FilteredContent | Measure-Object).Count -eq 1) { $FilteredContent = @(, $FilteredContent) } # Nothing found. Preventing errors! if (($FilteredContent | Measure-Object).Count -le 0) { return } $LblUserUCGroupCounter.Content = "Group Counter: $($FilteredContent.Count)" $DgrUserUCGroupMemberships.ItemsSource = $FilteredContent }) #endregion #region Open Window and process its return if ($Window.ShowDialog()) { Write-Host "Successfully processed dialog." } else { Write-Host "Window/Dialog not processed successfully." } #endregion
优化方案
1. 修复DataGrid虚拟化失效问题
WPF DataGrid本身内置滚动功能,外层套<ScrollViewer/>会导致虚拟化完全失效——DataGrid会一次性渲染所有1800条数据,这是卡顿的核心原因。
修改WPF代码:
- 移除外层
<ScrollViewer/>,直接给DataGrid设置Height="300" - 补充虚拟化配置属性:
<DataGrid Height="300" VirtualizingStackPanel.VirtualizationMode="Recycling" VirtualizingStackPanel.IsVirtualizing="True" EnableRowVirtualization="True" EnableColumnVirtualization="True" ...>
VirtualizationMode="Recycling"可复用行容器,大幅降低内存和渲染开销。
2. 使用ICollectionView实现高效筛选
当前筛选逻辑每次替换ItemsSource会触发DataGrid全量刷新,改用ICollectionView仅更新视图,无需修改源集合:
修改PowerShell代码:
# 初始化阶段 $ADGroups = 1..1800 | ForEach-Object { [PSCustomObject]@{ "Name" = "Group $_" "IsMember" = ($true, $false | Get-Random) } } # 转为ObservableCollection支持通知,创建视图 $groupCollection = New-Object System.Collections.ObjectModel.ObservableCollection[PSCustomObject] $ADGroups | ForEach-Object { $groupCollection.Add($_) } $groupView = [System.Windows.Data.CollectionViewSource]::GetDefaultView($groupCollection) $DgrUserUCGroupMemberships.ItemsSource = $groupView $LblUserUCGroupCounter.Content = "Group Counter: $($groupCollection.Count)" # 修改筛选事件 $TxbUserUCGroupFilter.Add_TextChanged({ $FilterText = $TxbUserUCGroupFilter.Text.Trim() # 通过视图Filter委托实现筛选 $groupView.Filter = { param($item) if ([string]::IsNullOrEmpty($FilterText)) { return $true } return $item.Name -match [regex]::Escape($FilterText) } $LblUserUCGroupCounter.Content = "Group Counter: $($groupView.Count)" })
[regex]::Escape($FilterText)可避免用户输入的特殊正则字符导致异常。
3. 替换PSCustomObject为自定义类提升绑定性能
PSCustomObject在WPF绑定中存在反射开销,改用轻量自定义类:
Add-Type @" public class ADGroup { public string Name { get; set; } public bool IsMember { get; set; } } "@ # 生成数据时使用自定义类 $ADGroups = 1..1800 | ForEach-Object { $group = New-Object ADGroup $group.Name = "Group $_" $group.IsMember = (Get-Random -Minimum 0 -Maximum 2) -eq 1 $group }
4. 异步加载数据避免UI阻塞
若后续加载真实AD数据,需在后台线程执行,避免UI卡顿:
$uiContext = [System.Threading.SynchronizationContext]::Current $null = [System.Threading.Tasks.Task]::Run({ # 替换为真实AD查询逻辑 $loadedGroups = 1..1800 | ForEach-Object { $group = New-Object ADGroup $group.Name = "Group $_" $group.IsMember = (Get-Random -Minimum 0 -Maximum 2) -eq 1 $group } # 回到UI线程更新集合 $uiContext.Post({ param($groups) $groupCollection.Clear() $groups | ForEach-Object { $groupCollection.Add($_) } $LblUserUCGroupCounter.Content = "Group Counter: $($groupCollection.Count)" }, $loadedGroups) })
5. 关闭冗余DataGrid特性
- 设置
AutoGenerateColumns="False"(已手动定义列,无需自动生成) - 若无需列排序,添加
CanUserSortColumns="False"减少交互开销
内容的提问来源于stack exchange,提问作者Kurth Rocks
相关产品推荐
相关产品推荐

