Подтвердить что ты не робот

Внедрение средства просмотра журналов с помощью WPF

Я обращаюсь за советом к лучшему подходу к реализации средства просмотра консольных журналов с помощью WPF.

Он должен соответствовать следующим критериям:

  • быстрая прокрутка со 100 000 строк
  • Некоторые записи (например, stacktraces) должны быть складными.
  • перенос длинных элементов
  • список можно фильтровать по различным критериям (поиск, теги и т.д.).
  • когда в конце он должен сохранять прокрутку при добавлении новых элементов.
  • Линейные элементы могут содержать какое-то форматирование добавления, например гиперссылки и счетчик встречаемости.

В общем, я имею в виду что-то вроде консольного окна FireBug и Chrome.

Я играл с этим, но я не сделал большого прогресса, потому что... - datagrid не может обрабатывать разные высоты элементов - положение прокрутки обновляется только после отпускания полосы прокрутки (что совершенно неприемлемо).

Я уверен, мне нужна форма виртуализации, и мне бы хотелось следовать шаблону MVVM.

Любая помощь или указатели приветствуются.

4b9b3361

Ответ 1

Я должен начать продавать эти образцы WPF, а не давать их бесплатно. = P

enter image description here

  • Виртуализированный пользовательский интерфейс (с использованием VirtualizingStackPanel), который обеспечивает невероятно хорошую производительность (даже с 200000 + элементами)
  • Полностью совместим с MVVM.
  • DataTemplate для каждого типа типа LogEntry. Это дает вам возможность как хотите. Я реализовал только 2 вида LogEntries (базовые и вложенные), но вы получили идею. Вы можете подклассифицировать LogEntry столько, сколько вам нужно. Вы даже можете поддерживать богатый текст или изображения.
  • Расширяемые (вложенные) элементы.
  • Word Wrap.
  • Вы можете реализовать фильтрацию и т.д. с помощью CollectionView.
  • WPF Rocks, просто скопируйте и вставьте мой код в File -> New -> WPF Application и посмотрите результаты для себя.

    <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 Behind: (Обратите внимание, что большая часть из них просто boileplate для поддержки примера (генерация случайных записей)

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

        }
    }

Элементы данных:

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

Ответ 2

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

В соответствии с ответом this вы можете сделать это:

В главном ScrollViewer (внутри DockPanel) добавьте событие:

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

Передайте источник события для автоматического прокрутки:

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