Как улучшить производительность WPF Grid control (.NET 4.0/4.5)?

Определение. Поскольку двумерный массив строк (около 10 столбцов, 1600 строк, фиксированная длина 7 символов) служит источником данных для элемента управления WPF .NET 4.0 Grid, для заполнения таблицы используется следующий фрагмент кода: метки, отображающие значения из массива. Примечание. Сетка была добавлена ​​в XAML и передана в функцию PopulateGrid (см. Листинг 1.). Визуальный вывод по сути представляет собой табличное представление данных в режиме только для чтения (нет необходимости в двустороннем связывании).

Проблема: производительность является ключевым вопросом. Для выполнения этой операции на мощном ПК Intel-i3/8GB-DDR3 потребовалось ошеломляющее 3... 5 секунд; следовательно, эта производительность WPF Grid, по крайней мере, на порядок ниже ожидаемой, если сравнивать с аналогичными элементами управления / задачами, например, с обычными элементами управления WinForm, поддерживающими данные, или даже с таблицей Excel.

Вопрос 1: есть ли способ улучшить производительность WPF Grid в сценарии, описанном выше? Пожалуйста, направьте ваш ответ / потенциальное улучшение на фрагмент кода, приведенный ниже в листинге 1 и листинге 2.

Вопрос 1a: предлагаемое решение может реализовать привязку данных к дополнительному управлению с учетом данных, как, например, DataGrid в DataTable, я добавил string[,] в DataTable dt преобразователь в листинге 2, так что дополнительные элементы управления DataContext (или же ItemsSource что угодно) собственность может быть связана с dt.DefaultView, Итак, в простейшей форме, не могли бы вы предоставить компактное (желательно примерно несколько строк кода, как это было сделано в элементах управления с учетом данных старого стиля) и эффективное (с точки зрения производительности) решение для привязки данных в WPF DataGrid в DataTable объект?

Большое спасибо.

Листинг 1 Процедура для заполнения WPF Grid GridOut из 2D string[,] Values

#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 Рекомендуется заменить Label управление ж / TextBlock используя его свойство Text вместо Content, как в случае Label, Это немного ускорит выполнение, плюс фрагмент кода будет напрямую совместим с VS 2012 для Win 8, которая не включает Label,

Примечание 3: пока что я пробовал связывать DataGrid в DataTable (см. XAML в листинге 3), но производительность очень низкая (grdOut является вложенным Grid, который был использован в качестве контейнера для табличных данных; _ dataGrid тип данных с учетом данных DataGrid).

Листинг 3 DataGrid привязка к DataTable: производительность была плохой, поэтому я убрал это ScrollViewer и не работает нормально.

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

1 ответ

Решение

Хорошо. Удалите весь свой код и начните все сначала.

Это мой взгляд на "Динамическую сетку" Labels с числом строк X и числом столбцов Y на основе массива 2D строк:

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

ViewModel:

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, которые вы предложили. Заполнение сетки НЕМЕДЛЕННО, когда вы нажимаете кнопку. Я уверен, что производительность намного лучше, чем дрянной формы динозавров, благодаря встроенной виртуализации пользовательского интерфейса.

  • Пользовательский интерфейс определен в XAML, в отличие от создания элементов пользовательского интерфейса в процедурном коде, что является плохой практикой.

  • Пользовательский интерфейс и данные хранятся отдельно, что повышает удобство обслуживания, масштабируемость и чистоту.

  • Скопируйте и вставьте мой код в File -> New -> WPF Application и посмотрите результаты для себя.

  • Кроме того, имейте в виду, что если вы собираетесь отображать только текст, лучше использовать TextBlock вместо Label, который является очень легким текстовым элементом.

  • WPF рушится, даже если в некоторых случаях это может привести к снижению производительности, он все равно на 12837091723 лучше, чем что-либо существующее в настоящее время.

Редактировать:

Я пошел дальше и добавил 0 нулей к числу строк (160000). Производительность по-прежнему приемлема. Это заняло менее 1 секунды, чтобы заполнить сетку.

Обратите внимание, что в моем примере "столбцы" НЕ виртуализируются. Это может привести к проблемам с производительностью, если их много, но это не то, что вы описали.

Edit2:

Основываясь на ваших комментариях и разъяснениях, я сделал новый пример, на этот раз в System.Data.DataTable, Нет ObservableCollections, нет асинхронных вещей (в моем предыдущем примере все равно ничего не было асинхронно). И всего 10 столбцов. Горизонтальная полоса прокрутки оказалась там из-за того, что окно было слишком маленьким (Width="300") и было недостаточно, чтобы показать данные. WPF не зависит от разрешения, в отличие от каркасов динозавров, он показывает полосы прокрутки, когда это необходимо, но также растягивает содержимое до доступного пространства (это можно увидеть, изменив размер окна и т. Д.).

Я также поместил код инициализации массива в конструктор Window (чтобы справиться с отсутствием INotifyPropertyChanged) так что это займет немного больше, чтобы загрузить и показать его, и я заметил, что этот пример с помощью System.Data.DataTable немного медленнее, чем предыдущий.

Однако я должен предупредить вас о том, что INotifyPropertyChanged объекты могут вызвать утечку памяти.

Тем не менее, вы не сможете использовать простой Grid контроль, потому что он не делает виртуализацию пользовательского интерфейса. Если вам нужна виртуализирующая сетка, вам придется реализовать ее самостоятельно.

Вы также НЕ сможете использовать подход 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. Это не просто инфраструктура пользовательского интерфейса, это сама по себе прикладная среда.

Edit3:

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

 DataContext = Array2DataTable(array).DefaultView;

Работает отлично для меня. Время загрузки не заметно с 160000 строк. Какую версию.Net Framework вы используете?

Другие вопросы по тегам