В окне отображаю лог довольно длительного процесса (5000 операций в среднем по 10 строчек каждая, потом будет и больше вплоть до 500k строчек)
Как известно, plain text убог :) например, на MacOS в стандартном блокноте только rich text.
вот и у меня для удобства чтения и для красоты часть строчек имеет свой цвет, часть кусков текста выделяется желтым маркером, возможно даже будут ссылки или и того круче - спойлеры из UIElement.
Но после того, как строк накопится много, лог тормозит и на выделение его кусков мышью (причем кусков совсем маленьких) и - что самое неприятное - на само добавление строк.
Есть ли что-то шустрее?
Решил попробовать три варианта - Syncfusion, Telerik и ComponentOne - и замерить. И ни один не заработал вообще.
Видно, не мой день.
(С первым разобрался, сделал все по аналогии с оригиналом, все работает, но строки добавленной не видно.
Второй вылетает при простом добавлении самого rtb в пустое окно свежесозданного проекта точь-в-точь по мануалу Getting started.
Со третьим разобрался, сделал все по аналогии с оригиналом, все работает, но строки добавленной не видно.
Пишу в саппорты...)
Единственным заработал AvalonEdit, но он заточен под подсветку синтаксиса, и я не понял, как в нем просто взять и задать шрифт для такого-то участка. И примитивный он какой-то по виду.
Интересно имеет ли вообще смысл что-то искать?
Вы не должны держать в памяти громадные массивы UI-контролов. Это неправильно и не нужно. Для отображения логов прекрасно подходит виртуализированный список. А для отображения — легковесный TextBlock
. Я обойдусь стоковыми средствами.
Дополнительные преимущества списка — вы легко можете устроить фильтрацию, сортировку и прочие плюшки, чего не так-то просто добиться в чисто текстовом формате.
Давайте засучим рукава и напишем немного кода.
Создадим простейший класс для одного элемента лога.
class LogEntry
{
public DateTime Time { get; }
public int Severity { get; }
public string ModuleName { get; }
public string Text { get; }
public LogEntry(DateTime time, int severity, string moduleName, string text)
{
Time = time;
Severity = severity;
ModuleName = moduleName;
Text = text;
}
}
Создадим 500K таких элементов, и положим их список в DataContext
. Отображение — в XAML:
<ListView ItemsSource="{Binding}" ScrollViewer.CanContentScroll="True">
<ListView.Resources>
<local:SeverityConverter x:Key="SevConv"/>
</ListView.Resources>
<ListView.ItemTemplate>
<DataTemplate DataType="{x:Type local:LogEntry}">
<TextBlock>
<Run Text="{Binding Time, Mode=OneWay}"/>
<Run Text="{Binding ModuleName, Mode=OneWay}"
Background="{Binding Severity, Converter={StaticResource SevConv}}"/>
<Run Text="{Binding Text, Mode=OneWay}"/>
</TextBlock>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Я использую самописный конвертер Severity в цвет:
class SeverityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object p, CultureInfo ci)
{
switch ((int)value)
{
case 1: return Brushes.Red;
case 2: return Brushes.Yellow;
case 3: return Brushes.Green;
default: return Brushes.Black;
}
}
public object ConvertBack(object value, Type targetType, object p, CultureInfo ci)
{
throw new NotSupportedException();
}
}
Да, давайте создадим случайные данные.
static class LogEntryFactory
{
static Random r = new Random();
static string[] Modules = { "Main", "VM", "Model", "DataBase", "Connectivity" };
public static LogEntry CreateRandom()
{
var time = DateTime.Now.AddDays(r.NextDouble() * 10);
var severity = r.Next(1, 4);
var moduleName = Modules[r.Next(Modules.Length)];
var text = string.Join(" ", Enumerable.Range(0, r.Next(2, 10))
.Select(_ => CreateRandomWord()));
return new LogEntry(time, severity, moduleName, text);
}
static char[] allowedChars = Enumerable.Range('a', 26).Select(Convert.ToChar)
.Concat(Enumerable.Range('A', 26).Select(Convert.ToChar)).ToArray();
private static string CreateRandomWord() =>
new string(Enumerable.Range(0, r.Next(3, 12))
.Select(_ => allowedChars[r.Next(allowedChars.Length)])
.ToArray());
}
Запускаем. Выводим надпись «Creating» на время создания в фоновом потоке 500K элементов.
Если вы хотите, чтобы можно было выделять текст, придётся использовать RichTextBox
в каждой строке. Получается как-то так:
<ListView ItemsSource="{Binding}" ScrollViewer.CanContentScroll="True"
HorizontalContentAlignment="Stretch"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListView.Resources>
<local:SeverityConverter x:Key="SevConv"/>
</ListView.Resources>
<ListView.ItemTemplate>
<DataTemplate DataType="{x:Type local:LogEntry}">
<RichTextBox IsReadOnly="True" HorizontalContentAlignment="Stretch" >
<FlowDocument>
<Paragraph>
<Run Text="{Binding Time, Mode=OneWay}"/>
<Run Text="{Binding ModuleName, Mode=OneWay}"
Background="{Binding Severity,
Converter={StaticResource SevConv}}"/>
<Run Text="{Binding Text, Mode=OneWay}"/>
</Paragraph>
</FlowDocument>
</RichTextBox>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Получается так:
Выделять текст через границу ячеек нельзя, можно только в одной.
Дополнение. Наверняка вы хотите выделять не внутри одной строки, а несколько строк, используя Ctrl-C и контекстное меню. Это тоже можно сделать, мы ж программисты!
Возвращаемся к TextBlock
'ам, добавляем Multiselect, привязку команды ApplicationCommands.Copy
и контекстное меню:
<ListView ItemsSource="{Binding Entries}" ScrollViewer.CanContentScroll="True"
SelectionMode="Extended">
<ListView.Resources>
<local:SeverityConverter x:Key="SevConv"/>
</ListView.Resources>
<ListView.ContextMenu>
<ContextMenu>
<MenuItem Header="Copy" Command="Copy"/>
</ContextMenu>
</ListView.ContextMenu>
<ListView.CommandBindings>
<CommandBinding Command="Copy" Executed="OnListCopy"/>
</ListView.CommandBindings>
<ListView.ItemTemplate>
<DataTemplate DataType="{x:Type local:LogEntry}">
<TextBlock>
<Run Text="{Binding Time, Mode=OneWay}"/>
<Run Text="{Binding ModuleName, Mode=OneWay}"
Background="{Binding Severity, Converter={StaticResource SevConv}}"/>
<Run Text="{Binding Text, Mode=OneWay}"/>
</TextBlock>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
В code-behind:
void OnListCopy(object sender, ExecutedRoutedEventArgs e)
{
var lv = (ListView)sender;
var text = string.Join(Environment.NewLine, lv.SelectedItems.Cast<LogEntry>());
Clipboard.SetText(text);
}
В LogEntry
добавляем
public override string ToString() => $"{Time} {ModuleName} {Text}";
Пробуем:
Кофе для программистов: как напиток влияет на продуктивность кодеров?
Рекламные вывески: как привлечь внимание и увеличить продажи
Стратегії та тренди в SMM - Технології, що формують майбутнє сьогодні
Выделенный сервер, что это, для чего нужен и какие характеристики важны?
Современные решения для бизнеса: как облачные и виртуальные технологии меняют рынок
Пошерстил интернет на данную тему и нашел примерно такую реализацию:
Мне нужно чтобы в массив urls добавлялись строки из hrefvalue, но вместо этого все строки добавляются в 0 элемент, метод ToArray() не помог
До сего момента не сильно сталкивался с WCFСегодня, разбирая чужой код, наткнулся на объявление свойства и присвоение ему пустого делегата: