c# wpf performance user-interface scroll

c# - Implementación de un visor de registro con WPF



performance user-interface (2)

Debería comenzar a vender estas muestras de WPF en lugar de darlas gratis. = P

  • Interfaz de usuario virtualizada (usando VirtualizingStackPanel ) que proporciona un rendimiento increíblemente bueno (incluso con más de 200 000 elementos)
  • Totalmente compatible con MVVM.
  • DataTemplate s para cada clase de tipo LogEntry . Estos te dan la posibilidad de personalizar todo lo que quieras. Solo implementé 2 tipos de LogEntries (básico y anidado), pero se entiende la idea. Puede subclase LogEntry tanto como lo necesite. Incluso puede admitir texto o imágenes enriquecidas.
  • Artículos ampliables (anidados).
  • Ajuste de línea.
  • Puede implementar el filtrado, etc. mediante el uso de un CollectionView .
  • WPF Rocks, solo copie y pegue mi código en un File -> New -> WPF Application y vea los resultados usted mismo.

    <Window x:Class="MiscSamples.LogViewer" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:MiscSamples" Title="LogViewer" Height="500" Width="800"> <Window.Resources> <Style TargetType="ItemsControl" x:Key="LogViewerStyle"> <Setter Property="Template"> <Setter.Value> <ControlTemplate> <ScrollViewer CanContentScroll="True"> <ItemsPresenter/> </ScrollViewer> </ControlTemplate> </Setter.Value> </Setter> <Setter Property="ItemsPanel"> <Setter.Value> <ItemsPanelTemplate> <VirtualizingStackPanel IsItemsHost="True"/> </ItemsPanelTemplate> </Setter.Value> </Setter> </Style> <DataTemplate DataType="{x:Type local:LogEntry}"> <Grid IsSharedSizeScope="True"> <Grid.ColumnDefinitions> <ColumnDefinition SharedSizeGroup="Index" Width="Auto"/> <ColumnDefinition SharedSizeGroup="Date" Width="Auto"/> <ColumnDefinition/> </Grid.ColumnDefinitions> <TextBlock Text="{Binding DateTime}" Grid.Column="0" FontWeight="Bold" Margin="5,0,5,0"/> <TextBlock Text="{Binding Index}" Grid.Column="1" FontWeight="Bold" Margin="0,0,2,0" /> <TextBlock Text="{Binding Message}" Grid.Column="2" TextWrapping="Wrap"/> </Grid> </DataTemplate> <DataTemplate DataType="{x:Type local:CollapsibleLogEntry}"> <Grid IsSharedSizeScope="True"> <Grid.ColumnDefinitions> <ColumnDefinition SharedSizeGroup="Index" Width="Auto"/> <ColumnDefinition SharedSizeGroup="Date" Width="Auto"/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition/> </Grid.RowDefinitions> <TextBlock Text="{Binding DateTime}" Grid.Column="0" FontWeight="Bold" Margin="5,0,5,0"/> <TextBlock Text="{Binding Index}" Grid.Column="1" FontWeight="Bold" Margin="0,0,2,0" /> <TextBlock Text="{Binding Message}" Grid.Column="2" TextWrapping="Wrap"/> <ToggleButton x:Name="Expander" Grid.Row="1" Grid.Column="0" VerticalAlignment="Top" Content="+" HorizontalAlignment="Right"/> <ItemsControl ItemsSource="{Binding Contents}" Style="{StaticResource LogViewerStyle}" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" x:Name="Contents" Visibility="Collapsed"/> </Grid> <DataTemplate.Triggers> <Trigger SourceName="Expander" Property="IsChecked" Value="True"> <Setter TargetName="Contents" Property="Visibility" Value="Visible"/> <Setter TargetName="Expander" Property="Content" Value="-"/> </Trigger> </DataTemplate.Triggers> </DataTemplate> </Window.Resources> <DockPanel> <TextBlock Text="{Binding Count, StringFormat=''{}{0} Items''}" DockPanel.Dock="Top"/> <ItemsControl ItemsSource="{Binding}" Style="{StaticResource LogViewerStyle}"> <ItemsControl.Template> <ControlTemplate> <ScrollViewer CanContentScroll="True"> <ItemsPresenter/> </ScrollViewer> </ControlTemplate> </ItemsControl.Template> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <VirtualizingStackPanel IsItemsHost="True"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ItemsControl> </DockPanel> </Window>

Código detrás: (Tenga en cuenta que la mayor parte es solo una placa de punta para apoyar el ejemplo (generar entradas aleatorias)

public partial class LogViewer : Window { private string TestData = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum"; private List<string> words; private int maxword; private int index; public ObservableCollection<LogEntry> LogEntries { get; set; } public LogViewer() { InitializeComponent(); random = new Random(); words = TestData.Split('' '').ToList(); maxword = words.Count - 1; DataContext = LogEntries = new ObservableCollection<LogEntry>(); Enumerable.Range(0, 200000) .ToList() .ForEach(x => LogEntries.Add(GetRandomEntry())); Timer = new Timer(x => AddRandomEntry(), null, 1000, 10); } private System.Threading.Timer Timer; private System.Random random; private void AddRandomEntry() { Dispatcher.BeginInvoke((Action) (() => LogEntries.Add(GetRandomEntry()))); } private LogEntry GetRandomEntry() { if (random.Next(1,10) > 1) { return new LogEntry() { Index = index++, DateTime = DateTime.Now, Message = string.Join(" ", Enumerable.Range(5, random.Next(10, 50)) .Select(x => words[random.Next(0, maxword)])), }; } return new CollapsibleLogEntry() { Index = index++, DateTime = DateTime.Now, Message = string.Join(" ", Enumerable.Range(5, random.Next(10, 50)) .Select(x => words[random.Next(0, maxword)])), Contents = Enumerable.Range(5, random.Next(5, 10)) .Select(i => GetRandomEntry()) .ToList() }; } }

Elementos de datos:

public class LogEntry: PropertyChangedBase { public DateTime DateTime { get; set; } public int Index { get; set; } public string Message { get; set; } } public class CollapsibleLogEntry: LogEntry { public List<LogEntry> Contents { get; set; } }

PropertyChangedBase:

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

Busco asesoramiento sobre el mejor enfoque para implementar un visor de registro de consola con WPF.

Debe coincidir con los siguientes criterios:

  • desplazamiento rápido con más de 100.000 líneas
  • Algunas entradas (como stacktraces) deberían ser plegables
  • artículos largos envuelven
  • la lista se puede filtrar por diferentes criterios (búsqueda, etiquetas, etc.)
  • cuando al final, debe seguir desplazándose cuando se agregan nuevos elementos
  • Los elementos de línea pueden contener algún tipo de formato de adición como hipervínculos y contador de ocurrencias

En general, tengo algo en mente como la ventana de la consola de FireBug y Chrome.

Jugué con this pero no hice mucho progreso, porque ... - la cuadrícula de datos no puede manejar diferentes alturas de elementos - la posición de desplazamiento solo se actualiza después de soltar la barra de desplazamiento (lo cual es completamente inaceptable).

Estoy bastante seguro, necesito alguna forma de virtualización y me encantaría seguir el patrón MVVM.

Cualquier ayuda o punteros son bienvenidos.


La respuesta de HighCore es perfecta, pero supongo que falta este requisito: "cuando al final, debe seguir desplazándose cuando se agregan nuevos elementos".

De acuerdo con this respuesta, puedes hacer esto:

En el ScrollViewer principal (dentro del DockPanel), agregue el evento:

<ScrollViewer CanContentScroll="True" ScrollChanged="ScrollViewer_ScrollChanged">

Emitir el origen del evento para hacer el desplazamiento automático:

private bool AutoScroll = true; private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e) { // User scroll event : set or unset autoscroll mode if (e.ExtentHeightChange == 0) { // Content unchanged : user scroll event if ((e.Source as ScrollViewer).VerticalOffset == (e.Source as ScrollViewer).ScrollableHeight) { // Scroll bar is in bottom // Set autoscroll mode AutoScroll = true; } else { // Scroll bar isn''t in bottom // Unset autoscroll mode AutoScroll = false; } } // Content scroll event : autoscroll eventually if (AutoScroll && e.ExtentHeightChange != 0) { // Content changed and autoscroll mode set // Autoscroll (e.Source as ScrollViewer).ScrollToVerticalOffset((e.Source as ScrollViewer).ExtentHeight); } } }