【问题标题】:How to improve performance of WPF Grid control (.NET 4.0/4.5)?如何提高 WPF Grid 控件(.NET 4.0/4.5)的性能?
【发布时间】:2013-05-15 14:04:19
【问题描述】:

定义:将二维字符串数组(约10列,1600行,固定长度7个字符)作为WPF .NET 4.0 Grid控件的数据源,如下代码sn -p 已用于使用显示数组值的标签填充网格。 注意:Grid 已添加到 XAML 并传递给函数 PopulateGrid(参见清单 1)。视觉输出本质上是只读模式下的表格数据表示(无需双向绑定)。

问题:性能是一个关键问题。在功能强大的 Intel-i3/8GB-DDR3 PC 上运行此操作需要令人难以置信的 3...5 秒;因此,恕我直言,此 WPF Grid 性能至少比预期慢一个数量级,基于与类似控件/任务的比较,例如常规的 WinForm 数据感知控件,甚至 Excel 工作表。

问题 1:在上述场景中是否有办法提高 WPF Grid 的性能?请将您的答案/潜在改进指向下面清单 1 和清单 2 中提供的代码 sn-p。

问题 1a:建议的解决方案可以将数据绑定到额外的数据感知控件,例如 DataGridDataTable。我已在清单 2 中将 string[,] 添加到 DataTable dt 转换器,以便附加控件的 DataContext(或 ItemsSource,等等)属性可以绑定到 dt.DefaultView。因此,以最简单的形式,您能否提供一个紧凑的(最好是几行代码,因为它是在旧式数据感知控件中完成的)和高效(性能方面)的 WPF 数据绑定解决方案DataGridDataTable 对象

非常感谢。

清单 1。从 2D string[,] Values 填充 WPF Grid GridOut 的过程

#region Populate grid with 2D-array values
/// <summary>
/// Populate grid with 2D-array values
/// </summary>
/// <param name="Values">string[,]</param>
/// <param name="GridOut">Grid</param>
private void PopulateGrid(string[,] Values, Grid GridOut)
{
    try
    {
        #region clear grid, then add ColumnDefinitions/RowsDefinitions

        GridOut.Children.Clear();
        GridOut.ColumnDefinitions.Clear();
        GridOut.RowDefinitions.Clear();

        // get column num
        int _columns = Values.GetUpperBound(1) + 1;

        // add ColumnDefinitions
        for (int i = 0; i < _columns; i++)
        {
            GridOut.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
        }

        // get rows num
        int _rows = Values.GetUpperBound(0) + 1;

        // add RowDefinitions
        for (int i = 0; i < _rows; i++)
        {
            GridOut.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
        }
        #endregion

        #region populate grid w/labels
        // populate grid w/labels
        for (int i = 0; i < _rows; i++)
        {
            for (int j = 0; j < _columns; j++)
            {
                // new Label control
                Label _lblValue = new Label();

                // assign value to Label
                _lblValue.Content = Values[i, j].ToString();

                // add Label to GRid
                GridOut.Children.Add(_lblValue);
                Grid.SetRow(_lblValue, i);
                Grid.SetColumn(_lblValue, j);
            }
        }
        #endregion
    }
    catch
    {
        GridOut.Children.Clear();
        GridOut.ColumnDefinitions.Clear();
        GridOut.RowDefinitions.Clear();
    }
}
#endregion

清单 2string[,]DataTable 转换

#region internal: Convert string[,] to DataTable
/// <summary>
/// Convert string[,] to DataTable
/// </summary>
/// <param name="arrString">string[,]</param>
/// <returns>DataTable</returns>
internal static DataTable Array2DataTable(string[,] arrString)
{
    DataTable _dt = new DataTable();
    try
    {
        // get column num
        int _columns = arrString.GetUpperBound(1) + 1;

        // get rows num
        int _rows = arrString.GetUpperBound(0) + 1;

        // add columns to DataTable
        for (int i = 0; i < _columns; i++)
        {
            _dt.Columns.Add(i.ToString(), typeof(string));
        }

        // add rows to DataTable
        for (int i = 0; i < _rows; i++)
        {
            DataRow _dr = _dt.NewRow();
            for (int j = 0; j < _columns; j++)
            {
                _dr[j] = arrString[i,j];
            }
            _dt.Rows.Add(_dr);
        }
        return _dt;
    }
    catch { throw; }
}
#endregion

注意 2。建议使用其 Text 属性而不是 Content 替换 Label 控件 w/TextBlock,就像 Label 的情况一样。它将稍微加快执行速度,而且代码 sn-p 将与 VS 2012 for Win 8 前向兼容,其中不包括 Label

注意 3:到目前为止,我已经尝试将 DataGrid 绑定到 DataTable(参见清单 3 中的 XAML),但性能很差(grdOut 是嵌套的 Grid ,用作表格数据的容器;_dataGridDataGrid 的数据感知对象类型。

清单 3DataGrid 绑定到 DataTable:性能很差,所以我删除了 ScrollViewer,但它运行不正常。

<ScrollViewer ScrollViewer.CanContentScroll="True" VerticalScrollBarVisibility="Auto" >
    <Grid Name="grdOut">
            <DataGrid AutoGenerateColumns="True" Name="_dataGrid" ItemsSource="{Binding Path=.}" />
    </Grid>
</ScrollViewer>

【问题讨论】:

  • 您没有性能问题。你有一个设计问题。你绝对不应该在 WPF 中这样做。使用具有内置 UI 虚拟化的 ItemsControl。发布您需要的屏幕截图,我可以告诉您在 WPF 中执行此操作的正确方法。请忘记 winforms 的心态。
  • @HighCore:感谢您的回复。我非常了解还有许多其他方法可以通过其他控件来实现此功能,例如ListView 或 DataGrid 等。在我的问题中明确指出,我正在寻找与 Grid 控件使用相关的解决方案。仅当发现与 WPF Grid 相关的所有其他选项都不足时,我才会考虑涉及任何其他控件的解决方案。最好的问候,
  • 同样,Grid 是与Layout 相关的 UI 元素。我与显示数据无关。我正在准备一个使用 ItemsControl 和动态定义的列和内容的示例。
  • @HighCore:再次感谢。作为仅供参考,我已经看到了很多关于其他 WPF 数据感知控件(如 ListView 或 DataGrid)性能的关键 cmets,关键问题是实现 UI 虚拟化,这(可能)提供了巨大的性能提升。如果这确实是一个关键点,那么请详细说明它提供详细的解决方案,或者只是链接到现有的解决方案。问候,

标签: wpf wpf-controls wpfdatagrid wpf-4.0 wpf-4.5


【解决方案1】:

好的。删除所有代码并重新开始。

这是我对 Labels 的“动态网格”的看法,其中 X 行和 Y 列基于二维字符串数组:

<Window x:Class="MiscSamples.LabelsGrid"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="LabelsGrid" Height="300" Width="300">
    <DockPanel>

        <Button DockPanel.Dock="Top" Content="Fill" Click="Fill"/>

        <ItemsControl ItemsSource="{Binding Items}"
                      ScrollViewer.HorizontalScrollBarVisibility="Auto"
                      ScrollViewer.VerticalScrollBarVisibility="Auto"
                      ScrollViewer.CanContentScroll="true"
                      ScrollViewer.PanningMode="Both">
            <ItemsControl.Template>
                <ControlTemplate>
                    <ScrollViewer>
                        <ItemsPresenter/>
                    </ScrollViewer>
                </ControlTemplate>
            </ItemsControl.Template>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <ItemsControl ItemsSource="{Binding Items}">
                        <ItemsControl.ItemTemplate>
                            <DataTemplate>
                                <Label Content="{Binding}"/>
                            </DataTemplate>
                        </ItemsControl.ItemTemplate>
                        <ItemsControl.ItemsPanel>
                            <ItemsPanelTemplate>
                                <UniformGrid Rows="1"/>
                            </ItemsPanelTemplate>
                        </ItemsControl.ItemsPanel>
                    </ItemsControl>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <VirtualizingStackPanel VirtualizationMode="Recycling" IsVirtualizing="True"/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>
    </DockPanel>
</Window>

代码背后:

public partial class LabelsGrid : Window
{
    private LabelsGridViewModel ViewModel { get; set; }

    public LabelsGrid()
    {
        InitializeComponent();
        DataContext = ViewModel = new LabelsGridViewModel();
    }

    private void Fill(object sender, RoutedEventArgs e)
    {
        var array = new string[1600,20];

        for (int i = 0; i < 1600; i++)
        {
            for (int j = 0; j < 20; j++)
            {
                array[i, j] = "Item" + i + "-" + j;
            }
        }

        ViewModel.PopulateGrid(array);
    }
}

视图模型:

public class LabelsGridViewModel: PropertyChangedBase
{
    public ObservableCollection<LabelGridItem> Items { get; set; } 

    public LabelsGridViewModel()
    {
        Items = new ObservableCollection<LabelGridItem>();
    }

    public void PopulateGrid(string[,] values)
    {
        Items.Clear();

        var cols = values.GetUpperBound(1) + 1;
        int rows = values.GetUpperBound(0) + 1;

        for (int i = 0; i < rows; i++)
        {
            var item = new LabelGridItem();

            for (int j = 0; j < cols; j++)
            {
                item.Items.Add(values[i, j]);
            }

            Items.Add(item);
        }
    }
}

数据项:

public class LabelGridItem: PropertyChangedBase
{
    public ObservableCollection<string> Items { get; set; }

    public LabelGridItem()
    {
        Items = new ObservableCollection<string>();
    }
}

PropertyChangedBase 类(MVVM 助手)

public class PropertyChangedBase:INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
        Application.Current.Dispatcher.BeginInvoke((Action) (() =>
                                                                 {
                                                                     PropertyChangedEventHandler handler = PropertyChanged;
                                                                     if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
                                                                 }));
    }
}

结果:

  • 性能很棒。请注意,我使用的是 20 列而不是您建议的 10 列。当您单击按钮时,网格的填充是立即的。由于内置 UI 虚拟化,我确信性能比蹩脚的恐龙 Winform 好得多。

  • UI 是在 XAML 中定义的,而不是在过程代码中创建 UI 元素,这是一种不好的做法。

  • UI 和数据保持分离,从而提高了可维护性、可扩展性和简洁性。

  • 复制我的代码并将其粘贴到 File -&gt; New -&gt; WPF Application 中,然后亲自查看结果。

  • 另外,请记住,如果您只打算显示文本,最好使用 TextBlock 而不是 Label,后者是一个非常轻量级的文本元素。

  • WPF 非常出色,即使在边缘情况下它可能会导致性能下降,它仍然比目前存在的任何东西都要好 12837091723。

编辑:

我继续在行数 (160000) 中添加了 0 个零。性能还是可以接受的。填充网格的时间不到 1 秒。

请注意,在我的示例中,“列”没有被虚拟化。如果数量很多,这可能会导致性能问题,但这不是您所描述的。

编辑2:

根据您的 cmets 和说明,我制作了一个新示例,这次基于 System.Data.DataTable。没有 ObservableCollections,没有异步的东西(无论如何在我之前的示例中没有任何异步)。而且只有 10 列。由于窗口太小 (Width="300") 并且不足以显示数据,因此存在水平滚动条。 WPF 是独立于分辨率的,不像恐龙框架,它会在需要时显示滚动条,但也会将内容拉伸到可用空间(您可以通过调整窗口大小等来看到这一点)。

我还将数组初始化代码放在 Window 的构造函数中(以处理缺少 INotifyPropertyChanged 的问题),因此加载和显示它需要更多时间,我注意到这个使用 System.Data.DataTable 的示例是比上一个稍慢。

但是,我必须警告你Binding to Non-INotifyPropertyChanged objects may cause a Memory Leak

不过,您将无法使用简单的Grid 控件,因为它不进行 UI 虚拟化。如果你想要一个虚拟化网格,你必须自己实现它。

您也无法使用 winforms 方法来解决此问题。它在 WPF 中简直是无关紧要和无用的。

    <ItemsControl ItemsSource="{Binding Rows}"
                  ScrollViewer.HorizontalScrollBarVisibility="Auto"
                  ScrollViewer.VerticalScrollBarVisibility="Auto"
                  ScrollViewer.CanContentScroll="true"
                  ScrollViewer.PanningMode="Both">
        <ItemsControl.Template>
            <ControlTemplate>
                <ScrollViewer>
                    <ItemsPresenter/>
                </ScrollViewer>
            </ControlTemplate>
        </ItemsControl.Template>
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <ItemsControl ItemsSource="{Binding ItemArray}">
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding}"/>
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                    <ItemsControl.ItemsPanel>
                        <ItemsPanelTemplate>
                            <UniformGrid Rows="1"/>
                        </ItemsPanelTemplate>
                    </ItemsControl.ItemsPanel>
                </ItemsControl>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <VirtualizingStackPanel VirtualizationMode="Recycling" IsVirtualizing="True"/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
    </ItemsControl>

代码背后:

public partial class LabelsGrid : Window
{
    public LabelsGrid()
    {
        var array = new string[160000, 10];

        for (int i = 0; i < 160000; i++)
        {
            for (int j = 0; j < 10; j++)
            {
                array[i, j] = "Item" + i + "-" + j;
            }
        }

        DataContext = Array2DataTable(array);
        InitializeComponent();
    }

    internal static DataTable Array2DataTable(string[,] arrString)
    {
        //... Your same exact code here
    }
}

底线是在 WPF 中做某事,你必须以 WPF 的方式来做。它不仅仅是一个 UI 框架,它本身更像是一个应用程序框架。

编辑3

<DataGrid AutoGenerateColumns="True" ItemsSource="{Binding}"/>

 DataContext = Array2DataTable(array).DefaultView;

对我来说非常好用。 160000 行的加载时间并不明显。您使用的是哪个 .Net 框架版本?

【讨论】:

  • 再次感谢您的意见,虽然这不是我要找的。让我明确一点:整个想法是最小化所涉及技术的表面积,因此没有可观察的集合,没有异步的东西(阻塞执行就可以了),没有水平滚动条(正如我所说,它必须是 10列或更少)。为了简化任务,我只添加了一个额外的数据对象:DataTable dt,因此用于显示表格数据的控件只需绑定到 dt.DefaultView。我将 string[,] 的代码 sn-p 发布到 DataTable 转换。问候,
  • @AlexBell `“最小化所涉及技术的表面积”是什么意思? WPF 不接受(也不关心)其他技术的方式。如果你要在 WPF 中做某事,那就是WPF 方式或高速公路”。否则你会吃很多苦头,并且不会获得任何有用的东西。WPF 并不真正关心“你是什么”正在寻找”……再考虑一下。
  • @AlexBell 要明确这一点,WPF 方式实际上是 XAML 方式。这意味着我在这里谈论的所有内容也适用于所有其他基于 XAML 的技术(WinRT、Silverlight、Windows Phone 等)。您最初发布的代码(在程序代码中创建 Label 元素)类似于旧的、不可维护的 winforms 方法。 WPF 不支持。
  • TextBlock vs. Label 是一个次要/次要问题(标签控件包含在 VS 2010 中,因此它不完全是(引用)“旧的、不可维护的 winforms 方法”......顺便说一句,那种旧式几十年来,这种方法工作得很好,数据绑定可以在两行代码中完成,其性能明显优于 WPF 数据感知控件中所见的性能,具有相关的大量代码,而不是完成时只有两行数据绑定行之前。无论如何,将不胜感激有关 WPF DataGrid 到 DataView 绑定的实用解决方案,并具有良好的性能。Rgds,
  • @AlexBell winforms 方法“运行良好”,因为开发人员习惯于编写(并因此阅读)糟糕的代码。我无法忍受任何 winforms 代码而不感到头晕和呕吐。而winforms数据绑定的故事就是个笑话。 “数据感知控件” - 我不知道那是什么,WPF 中的一切都是“数据感知”。即使BordersBrushesTransformsItemsControls,你显然不知道你在说什么。此外,winforms 在 windows 95 上运行良好,但现在是 2013 年,我们不能再做 windows-95 之类的应用程序了。
猜你喜欢
  • 2012-05-10
  • 2015-11-25
  • 2012-05-16
  • 2019-08-23
  • 2012-11-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-19
相关资源
最近更新 更多