web-dev-qa-db-fra.com

Implémentation d'un visualiseur de journal avec WPF

Je cherche des conseils sur la meilleure approche pour implémenter un visualiseur de journal de console avec WPF.

Il devrait correspondre aux critères suivants:

  • défilement rapide avec plus de 100 000 lignes
  • Certaines entrées (comme les stacktraces) doivent être pliables
  • articles longs envelopper
  • la liste peut être filtrée selon différents critères (recherche, tags, etc.)
  • quand à la fin, il devrait continuer à défiler lorsque de nouveaux éléments sont ajoutés
  • Les éléments de ligne peuvent contenir une sorte de mise en forme supplémentaire, telle que des hyperliens et un compteur d'occurrences.

En général, je pense à quelque chose comme la fenêtre de la console de FireBug et Chrome.

J'ai joué avec this mais je n'ai pas fait beaucoup de progrès, car ... - la grille de données ne peut pas gérer différentes hauteurs d'élément - la position de défilement est mise à jour uniquement inacceptable).

Je suis sûr que j'ai besoin d'une certaine forme de virtualisation et j'aimerais suivre le modèle MVVM.

Toute aide ou pointeur est le bienvenu.

70
pixtur

Je devrais commencer à vendre ces échantillons WPF au lieu de les distribuer gratuitement. = P

enter image description here

  • Interface utilisateur virtualisée (en utilisant VirtualizingStackPanel) qui offre des performances incroyablement bonnes (même avec plus de 200 000 éléments)
  • Complètement compatible avec MVVM.
  • DataTemplates pour chaque type de LogEntry type. Celles-ci vous permettent de personnaliser autant que vous le souhaitez. Je n'ai implémenté que 2 types de LogEntries (de base et imbriqué), mais vous voyez l'idée. Vous pouvez sous-classer LogEntry autant que vous le souhaitez. Vous pouvez même prendre en charge du texte enrichi ou des images.
  • Articles extensibles (imbriqués).
  • Word Wrap.
  • Vous pouvez implémenter le filtrage, etc. en utilisant un CollectionView.
  • WPF Rocks, copiez et collez mon code dans un fichier File -> New -> WPF Application et voyez les résultats par vous-même.

    <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>
    

Code Derrière: (Notez que la majeure partie est simplement boileplate pour supporter l'exemple (générer des entrées aléatoires))

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

        }
    }

Éléments de données:

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

La réponse HighCore est parfaite, mais je suppose qu’il manque cette exigence: "à la fin, elle devrait continuer à défiler lorsque de nouveaux éléments sont ajoutés".

Selon this réponse, vous pouvez le faire:

Dans le ScrollViewer principal (à l'intérieur du DockPanel), ajoutez l'événement:

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

Transmettez la source de l'événement pour faire le défilement automatique:

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