如何提高WPF Grid控件的性能(.NET 4.0/4.5)?

3

定义: 有一个由字符串组成的二维数组(大约10列,1600行,长度固定为7个字符),作为WPF .NET 4.0网格控件的数据源。下面的代码片段用于填充显示来自数组值的标签的网格。注意:网格被添加到XAML并传递给函数PopulateGrid(参见Listing 1)。视觉输出实质上是一个表格化的数据表示,并处于只读模式(不需要双向绑定)。

问题: 性能是一个关键问题。使用强大的Intel-i3/8GB-DDR3 PC运行时,完成此操作需要令人难以置信的3...5秒;因此,IMHO,这个WPF网格性能至少比预期慢一个数量级,与例如常规WinForm数据感知控件或甚至Excel工作表中的类似控件/任务相比。

问题1: 如果有一种方法可以改进上述情况下的WPF网格的性能?请将您的答案/潜在改进指向下面提供的Listing 1和Listing 2中的代码片段。

问题1a: 提出的解决方案可能会实现对其他数据感知控件(例如DataGridDataTable)的数据绑定。我在Listing 2中添加了string[,] DataTable dt转换器,以便其他控件的DataContext(或ItemsSource等)属性可以绑定到dt.DefaultView。因此,在最简单的形式下,您能否提供一个简洁(理想情况下大约是几行代码,就像旧式数据感知控件那样)和有效(性能方面)的解决方案,实现WPF DataGridDataTable对象的数据绑定

非常感谢。

Listing 1. 用于从二维string[,]值填充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

清单2. string[,] 转换为 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:建议使用TextBlock控件替换Label控件,并使用其Text属性而非Content属性,这样可以加快执行速度,而且代码片段将与不包括Label的Win 8中的VS 2012向前兼容。

注意 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的思维方式。 - Federico Berasategui
@HighCore:感谢您的回复。我非常了解使用其他控件(例如ListView或DataGrid等)实现此功能的许多其他方法。但是在我的问题中,明确说明我正在寻找与Grid控件使用相关的解决方案。只有当所有其他仅涉及WPF Grid的选项都被认为不足时,我才会考虑涉及任何其他控件的解决方案。最好的问候。 - Alexander Bell
再次强调,Grid是与布局相关的UI元素,与数据显示无关。我正在准备一个示例,使用动态定义列等内容的ItemsControl - Federico Berasategui
@HighCore:再次感谢。顺便说一下,我看到了很多关于其他WPF数据感知控件(如ListView或DataGrid)性能的批评评论,而关键问题是实现UI虚拟化,这(据说)可以大大提高性能。如果这确实是一个关键点,那么请详细说明解决方案,或者只是链接到现有的解决方案。问候, - Alexander Bell
1个回答

6
好的。删除你所有的代码,重新开始。
这是我对基于2D字符串数组的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 Helper)

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

结果:

enter image description here

  • 性能非常出色。请注意,我使用的是20列而不是你建议的10列。单击按钮时,网格的填充是立即完成的。由于内置的UI虚拟化,我相信性能比恶劣的恐龙winforms要好得多。

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

  • UI和数据被分开,从而增加了可维护性、可扩展性和清晰度。

  • 复制并粘贴我的代码到“文件-新建-WPF应用程序”中,看看结果。

  • 此外,请记住,如果您只需要显示文本,则最好使用而不是

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

编辑:

我将行数(160000)增加了0个零。性能仍然可以接受。填充网格不到1秒。
请注意,我的示例中“列”没有被虚拟化。如果有很多列,则可能会导致性能问题,但这不是您所描述的情况。
编辑2:
根据您的评论和澄清,我制作了一个新示例,这次基于System.Data.DataTable。没有ObservableCollections,没有异步操作(我的上一个示例中也没有异步操作)。只有10列。由于窗口太小(Width="300"),水平滚动条存在且不足以显示数据。WPF是与分辨率无关的,不像恐龙般的框架,当需要时它会显示滚动条,但也会将内容拉伸到可用空间(可以通过调整窗口大小等来看到这一点)。
我还将数组初始化代码放在了Window的构造函数中(以处理缺少INotifyPropertyChanged),因此加载和显示它需要更长的时间,并且我注意到使用System.Data.DataTable的此示例比先前的示例略慢。

然而,我必须警告你,绑定到非INotifyPropertyChanged对象可能会导致内存泄漏

但是,你将无法使用简单的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框架版本?


@AlexBell,“最小化所涉及技术的表面积”是什么意思?WPF不接受(也不关心)其他技术的方式。如果你要在WPF中做某些事情,就必须按照WPF的方式去做,否则你会遭受很多苦难,并且无法实现任何有用的东西。WPF并不真正关心“你正在寻找什么”……再想一想吧。 - Federico Berasategui
TextBlock和Label之间的区别只是一个小问题(Label控件已经包含在VS 2010中,所以它不是(引用)“旧的、难以维护的winforms方法”……顺便说一句,那种旧式方法在过去的几十年里运行得非常好,数据绑定只需要两行代码就可以完成,性能比WPF数据感知控件要好得多,而且还有大量的代码与之相关,而不仅仅是两个数据绑定行。无论如何,关于WPF DataGrid到DataView绑定的实用解决方案,我们将不胜感激。祝好! - Alexander Bell
@AlexBell,WinForms方法“运作良好”,因为开发人员习惯于编写(因此阅读)糟糕的代码。我无法忍受任何WinForms代码,感觉头晕目眩,恶心难受。而且WinForms数据绑定故事简直是一个笑话。“数据感知控件”-我不知道那是什么,WPF中的所有内容都是“数据感知”的。甚至包括BordersBrushesTransformsItemsControls,你显然不知道自己在说什么。此外,WinForms适用于Windows 95,但现在已经是2013年了,我们不能再做类似Windows 95的应用程序了。 - Federico Berasategui
@AlexBell我不是在谈论LabelsTextBlocks的区别。我在谈论以编程方式创建UI元素,这是我见过的最糟糕的方法。 - Federico Berasategui
@AlexBell,你可以在网上找到成千上万的DataGrid与DataTable示例。你最初的问题是不同的,所以我给出了一个适合你最初问题的答案,你说你不想使用DataGrid。 - Federico Berasategui
显示剩余10条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接