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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 22:24:21