I have a WinForms display of some captured network data (binary). I'm trying to move to WPF as a learning exercise, and because it should be more maintainable going forward.
As you may have seen, the most common display format for binary data is a dual-column layout showing both the numeric value of each value on the left and string display of octets corresponding to printable ASCII characters on the right.
I also want to be able to independently change the color of each value to highlight detected changes.
So far I've tried with a RichTextBox
, where the FlowDocument
is populated by a Table
. There's a single TableRowGroup
and two columns, one for numeric display and one for string display.
A loop recreates the TableRowGroup
each time the data changes. Every 16 bytes of data forms a new TableRow
containing a single Paragraph
. The values within the TableCell
are managed as a Run
each. Run
objects corresponding to values requiring coloring are further nested in a Span
before being placed into the Paragraph
.
This works, but is noticeably slower than a WinForms RichTextBox
for moderate data sizes. I suppose WPF is not really designed to have ~4000 Run
instances in a single FlowDocument
.
Also, I'm told that imperative creation of so many TableCell
and Run
objects is not the intended approach for WPF. But I'm not quite sure how to map this to a viewmodel either, how to trigger property changes for new data as well as changed display options.
Any suggestions would be appreciated. Support for copy-to-clipboard is highly desirable.
internal void RenderMessageDiff(GameMessage oldMsg, GameMessage newMsg)
{
if (newMsg == null) return;
var tablerows = new TableRowGroup();
if (oldMsg == null) oldMsg = newMsg;
var aby = newMsg.payload;
for (int offset = 0; offset < aby.Length - 1; offset += 16)
{
var leftCol = new Paragraph();
var rightCol = new Paragraph();
int blocklen = Math.Min(16, aby.Length - offset);
switch (selectDisplayFormat.SelectedValue as string)
{
case "u1":
for (int i = 0; i < blocklen; ++i)
{
if (i > 0) leftCol.Inlines.Add(new Run(","));
var v = new Run(aby[offset + i].ToString());
if (aby[offset + i] == oldMsg.payload[offset + i])
leftCol.Inlines.Add(v);
else
leftCol.Inlines.Add(new Span(v) { Foreground = Brushes.Red });
}
break;
case "u2":
for (int i = 0; i + 1 < blocklen; i += 2)
{
if (i > 0) leftCol.Inlines.Add(new Run(","));
var v = new Run(newMsg.ConvertUInt16LE(offset + i).ToString());
if (newMsg.ConvertUInt16LE(offset + i) == oldMsg.ConvertUInt16LE(offset + i))
leftCol.Inlines.Add(v);
else
leftCol.Inlines.Add(new Span(v) { Foreground = Brushes.Red });
}
break;
case "u4":
for (int i = 0; i + 3 < blocklen; i += 4)
{
if (i > 0) leftCol.Inlines.Add(new Run(","));
var v = new Run(newMsg.ConvertUInt32LE(offset + i).ToString());
if (newMsg.ConvertUInt32LE(offset + i) == oldMsg.ConvertUInt32LE(offset + i))
leftCol.Inlines.Add(v);
else
leftCol.Inlines.Add(new Span(v) { Foreground = Brushes.Red });
}
break;
case "x1":
default:
for (int i = 0; i < blocklen; ++i)
{
if (i > 0) leftCol.Inlines.Add(new Run(" "));
var v = new Run(aby[offset + i].ToString("X2"));
if (aby[offset + i] == oldMsg.payload[offset + i])
leftCol.Inlines.Add(v);
else
leftCol.Inlines.Add(new Span(v) { Foreground = Brushes.Red });
}
break;
}
Span changeRun = null;
for (int i = 0; i < blocklen; ++i)
{
char c = (char)aby[offset + i];
if (c < 32 || c >= 127) c = '.';
var v = new Run(c.ToString());
if (aby[offset + i] == oldMsg.payload[offset + i])
{
rightCol.Inlines.Add(v);
changeRun = null;
}
else
{
if (changeRun == null)
rightCol.Inlines.Add(changeRun = new Span(v) { Foreground = Brushes.Red });
else
changeRun.Inlines.Add(v);
}
}
tablerows.Rows.Add(new TableRow()
{
Cells = { new TableCell(leftCol), new TableCell(rightCol) }
});
}
textMessageDiff.RowGroups.Clear();
textMessageDiff.RowGroups.Add(tablerows);
}