none
WPF скорость обновления UI RRS feed

  • Общие обсуждения

  • Есть биндинг Datagrid и ObservableCollection. Как правильно отображать данные, если имеются проблемы с производительностью (видимый грид большой, источник данных тоже большой, часто и помногу меняется)?

     Получив данные для отображения, я пробовал:

    1. при каждом изменении данных просматривать коллекцию, находить изменившиеся данные, и просто менять их (получалось, как будто замена содержимого изменившихся ячеек грида). (первый вариант)

    2. при каждом изменении данных создавать новую коллекцию, и затем биндить ее (второй вариант)

    3. при каждом изменении данных делать очистку коллекции, и потом заполнять ее заново данными (полностью) (третий вариант)

    Я сделал тестовое приложение, в котором я реализовал все перечисленные варианты, и которое показывает время выполнения. Проект можно скачать тут http://zalil.ru/31580086. Для тестирования различных вариантов, нужно раскомментировать соответствующие строки.

    Если что, вот код файлов. MainWindow.xaml:

    <Window x:Class="WpfApplication1.MainWindow"
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     Title="MainWindow" Height="Auto" Width="Auto" SizeToContent="WidthAndHeight" mc:Ignorable="d" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" d:DesignHeight="211" d:DesignWidth="343">
    
     <DockPanel Height="Auto" HorizontalAlignment="Left" Name="DockPanel" VerticalAlignment="Top" Width="Auto">
     <DataGrid Name="dgB"
      ItemsSource="{Binding}"
      DockPanel.Dock="Left"
      VirtualizingStackPanel.IsVirtualizing="True"
      VirtualizingStackPanel.VirtualizationMode="Recycling"
      Width="Auto"
      AutoGenerateColumns="False">
     <DataGrid.Columns>
      <DataGridTextColumn Header="One"
      Binding="{Binding Path=One}"
      Width="60"/>
      <DataGridTextColumn Header="Two"
      Binding="{Binding Path=Two}"
      Width="60"/>
      <DataGridTextColumn Header="Three"
      Binding="{Binding Path=Three}"
      Width="60"/>
      <DataGridTextColumn Header="Four"
      Binding="{Binding Path=Four}"
      Width="60"/>
     </DataGrid.Columns>
     </DataGrid>
     <TextBox Height="Auto" DockPanel.Dock="Right" HorizontalAlignment="Left" Name="textBox" VerticalAlignment="Top" Width="Auto" />
     </DockPanel>
    </Window>
    
    

    MainWindow.сs:

     

     

    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Diagnostics;
    using System.Runtime.CompilerServices;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Windows;
    using System.Windows.Controls;
    
    namespace WpfApplication1
    {
     /// <summary>
     /// Логика взаимодействия для MainWindow.xaml
     /// </summary>
     public partial class MainWindow : Window
     {
     public ObservableCollection<Record> Obc { get; set; } //для отображение в UI
    
     public MainWindow()
     {
     InitializeComponent();
    
     Obc = new ObservableCollection<Record>();
     dgB.ItemsSource = Obc;
    
     Task.Factory.StartNew(() => Run());
     }
    
     private int _temp = 1000;
    
     public void Run()
     {
     while (true)
     {
     //заполнение списка
     var list = new List<Record>();
     for (var i = 1000; i < 1050; i++)
     {
      list.Add(new Record { One = _temp + i, Two = _temp + i, Three = _temp + i, Four = _temp + i });
      _temp = _temp + i;
     }
    
     ////второй вариант с созданием новой obc вне потока UI и биндингом в потоке UI (первая часть - вне потока UI)
     //var newObc = new ObservableCollection<Record>(list);
    
     //перенос изменений из списка в obc
     DockPanel.Dispatcher.BeginInvoke(new ThreadStart(() =>
     {
      if (list.Count != Obc.Count) //если obc не инициализирован или произошло изменение количества строк
      {
      textBox.AppendText(Time.Get() + " очистка" + "\r\n");
      Obc.Clear();
      foreach (var item in list)
      Obc.Add(new Record());
      }
    
      var stopwatch = Stopwatch.StartNew();
    
      //первый и самый быстрый вариант с ручной заменой изменившихся ячеек в потоке UI
      //у меня занимает примерно 15 мс, плюс будет бонус по времени, если какая-то ячейка не изменилась
      for (var i = 0; i < list.Count; i++)
      {
      if (list[i].One != Obc[i].One) Obc[i].One = list[i].One;
      if (list[i].Two != Obc[i].Two) Obc[i].Two = list[i].Two;
      if (list[i].Three != Obc[i].Three) Obc[i].Three = list[i].Three;
      if (list[i].Four != Obc[i].Four) Obc[i].Four = list[i].Four;
      }
    
      ////второй вариант с созданием новой obc вне потока UI и биндингом в потоке UI (вторая часть - в потоке UI)
      ////у меня занимает примерно 35 мс
      //dgB.ItemsSource = newObc;
    
      ////третий вариант с полной очисткой и последующим заполнением Obc
      ////у меня занимает примерно 42 мс, что немного медленнее второго варианта
      //Obc.Clear();
      //foreach (var t in list)
      // Obc.Add(new Record { One = t.One, Two = t.Two, Three = t.Three, Four = t.Four });
    
      textBox.AppendText(Time.Get() + " отрисовка заняла, мс: " + stopwatch.ElapsedTicks * Time.MillisecPerTick + "\r\n");
     }));
    
     Thread.Sleep(2000);
     }
     }
     }
    
     public class Record : INotifyPropertyChanged
     {
     public event PropertyChangedEventHandler PropertyChanged;
    
     private decimal _one;
     private int _two;
     private int _three;
     private int _four;
    
     private void NotifyPropertyChanged(String info)
     {
     if (PropertyChanged != null)
     PropertyChanged(this, new PropertyChangedEventArgs(info));
     }
    
    
     #region свойства
     public decimal One
     {
     get { return _one; }
     set
     {
     if (value != _one)
     {
      _one = value;
      NotifyPropertyChanged("One");
     }
     }
     }
    
     public int Two
     {
     get { return _two; }
     set
     {
     if (value != _two)
     {
      _two = value;
      NotifyPropertyChanged("Two");
     }
     }
     }
    
     public int Three
     {
     get { return _three; }
     set
     {
     if (value != _three)
     {
      _three = value;
      NotifyPropertyChanged("Three");
     }
     }
     }
    
    
     public int Four
     {
     get { return _four; }
     set
     {
     if (value != _four)
     {
      _four = value;
      NotifyPropertyChanged("Four");
     }
     }
     }
     #endregion
    
     }
    
     public static class Time
     {
     public static readonly double MillisecPerTick = (1000L * 1000L * 1000L) / Stopwatch.Frequency / Math.Pow(10, 6);
    
     [MethodImpl(MethodImplOptions.Synchronized)]
     public static string Get()
     {
     return String.Format("{0:HH:mm:ss:ffff}", DateTime.Now);
     }
     }
    }
    
    
    


    Проблема в том, что даже первый вариант является для меня слишком медленным. Отрисовка занимает больше половины ресурсов приложения, и в разы больше ресурсов, расходуемых на логику. Какие есть варианты ускорения отрисовки? Виртуализация, как я понял, тут не причем, так как я всегда вижу весь грид, у него нет невидимой части.

    В рабочем приложении и гридов больше, и изменение идет чаще (примерно 5-10 раз в секунду). В итоге, поток UI просто "забивается" и UI висит.

     

     

     




    19 августа 2011 г. 23:35

Все ответы

  • в цикле for (var i = 0; i < list.Count; i++) повторяются вызовы Obc[i] и list[i]. лучше не полагаться на оптимизатор и сделать так: 

    for (var i = 0; i < list.Count; i++)
    {
     var lv = list[i];
     var ov = Obc[i];
     if (lv.One != ov.One) ov.One = lv.One;
     if (lv.Two != ov.Two) ov.Two = lv.Two;
     if (lv.Three != ov.Three) ov.Three = lv.Three;
     if (lv.Four != ov.Four) ov.Four = lv.Four;
    }
    
    



     

    20 августа 2011 г. 7:26
  • использовать ObservableCollection - обязательно? если нет, то можно еще немного выиграть, т.к. в методе ObservableCollection.Add->InsertItem есть несколько проверок, лишних в данной ситуации.

    P.S.
    см. Find Application Bottlenecks with Visual Studio Profiler - http://msdn.microsoft.com/en-us/magazine/cc337887.aspx

     

    20 августа 2011 г. 7:42
  • Спасибо. В тестовом стало быстрее на 35%. примерно 10,5 мс.

    А чем заменить ObservableCollection. Если сделать List<>, то не будет отрисовки. Кстати, время с List<> без отрисовки будет 3,5 мс.



    • Изменено Qwester33 22 августа 2011 г. 20:39
    20 августа 2011 г. 11:52
  • На скрине результат показа Hot Spot для тестового приложения. Мне понятно только наличие Sleep вверху. Как найти узкое место среди кода, который я не писал, я не понимаю)

    20 августа 2011 г. 13:05
  • ускорить отрисовку можно, если формировать DataGrid до вывода на экран.
    ниже переделанный Run:
     
    delegate void Method(object data);
    public void Run()
    {
     Method draw = data => 
      {
       var grid = new DataGrid();
       grid.ItemsSource = (IEnumerable) data;
       grid.Width = 350;
       var old = host.Children[0];
       var stopwatch = Stopwatch.StartNew();
       old.Visibility = System.Windows.Visibility.Hidden;
       host.Children.Add(grid);
       textBox.AppendText(Time.Get() + " отрисовка заняла, мс: " + stopwatch.ElapsedTicks * Time.MillisecPerTick + "\r\n");
       host.Children.Remove(old);
      };
     while (true)
     {
      var data = CreateData();
      DockPanel.Dispatcher.BeginInvoke(draw, data);
      Thread.Sleep(2000);
     }
    }
    IEnumerable CreateData()
    {
      var ret = new ObservableCollection();
      for (var i = 1000; i < 1050; i++)
      {
        ret.Add(new Record { One = _temp + i, Two = _temp + i, Three = _temp + i, Four = _temp + i });
        _temp = _temp + i;
      }
    
      return ret;
    }
    
    в xaml заменить DataGrid:
    <DockPanel Height="Auto" HorizontalAlignment="Left" Name="DockPanel" VerticalAlignment="Top" Width="Auto">
     <Grid x:Name="host"><DataGrid /></Grid>
     <TextBox Height="Auto" DockPanel.Dock="Right" HorizontalAlignment="Left" Name="textBox" VerticalAlignment="Top" Width="Auto" />
    </DockPanel>
    



    20 августа 2011 г. 13:53
  • То есть, датагрид будет в разы быстрее заполнен, если он до этого ни разу не отображался? 

    (потому что пробовал просто прятать датагрид, но показало плохое время, значит,видимо, должен ни разу до этого не отображаться?:

      dgB.Visibility = Visibility.Hidden;
      dgB.ItemsSource = (IEnumerable)list;
      dgB.Visibility = Visibility.Visible;
    

    )

    • Изменено Qwester33 22 августа 2011 г. 20:40
    20 августа 2011 г. 15:55
  • если вся таблица и ячейки должны выводиться целиком, то ...
    public partial class MainWindow : Window
    {
      public MainWindow()
      {
        InitializeComponent();
    
        _Cells = new List>();
        for (int i = 0; i < 50; i++)
        {
          var row = new List();
          for (int j = 0; j < 4; j++)
          {
            var r = new TextBlock();
            Canvas.SetLeft(r, j * 70 + 1);
            Canvas.SetTop(r, (i * 20) + 1);
            host.Children.Add(r);
            row.Add(r);
          }
          _Cells.Add(row);
        }
        Task.Factory.StartNew(() => Run());
      }
    
      List> _Cells;
      private int _temp = 1000;
    
      delegate void Method(object data);
      public void Run()
      {
        Method draw = data =>
        {
          var stopwatch = Stopwatch.StartNew();
          var records = (ObservableCollection)data;
          for (int i = 0; i < records.Count; i++)
          {
            var r = records[i];
            var row = _Cells[i];
            row[0].Text = r.One;
            row[1].Text = r.Two;
            row[2].Text = r.Three;
            row[3].Text = r.Four;
          }
          textBox.AppendText(Time.Get() + " отрисовка заняла, мс: " + stopwatch.ElapsedTicks * Time.MillisecPerTick + "\r\n");
        };
        while (true)
        {
          var data = CreateData();
          Dispatcher.BeginInvoke(draw, data);
          Thread.Sleep(200);
        }
      }
    
      IEnumerable CreateData()
      {
        var ret = new ObservableCollection();
        for (var i = 1000; i < 1050; i++)
        {
          ret.Add(new Record { One = (_temp + i).ToString(), Two = (_temp + i).ToString(), Three = (_temp + i).ToString(), Four = (_temp + i).ToString() });
          _temp = _temp + i;
        }
        return ret;
      }
      public class Record
      {
        public string One { get; set; }
        public string Two { get; set; }
        public string Three { get; set; }
        public string Four { get; set; }
      }
    }
    
    <DockPanel Height="Auto" HorizontalAlignment="Left" Name="DockPanel" VerticalAlignment="Top" Width="Auto">
      <Canvas x:Name="host" Width="350"></Canvas>
      <TextBox Height="Auto" DockPanel.Dock="Right" HorizontalAlignment="Left" Name="textBox" VerticalAlignment="Top" Width="Auto" />
    </DockPanel>
    
    20 августа 2011 г. 21:57
  • Мне надо датагрид, у меня на нем основано оформление и клики по ячейкам

    21 августа 2011 г. 12:58
  • Уважаемый пользователь!

    В вашей теме отсутствует активность в течение последних 5 дней. При отсутствии каких-либо действий в течение 2 последующих дней, тема будет переведена в разряд обсуждений. Вы можете возобновить дискуссию, просто оставив сообщение в данной теме


    Для связи [mail]
    31 августа 2011 г. 9:47