0

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.

enter image description here

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);
    }
Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • It is this just simply for display? is there any reason the values would need to be edited from the UI? – kmacdonald Apr 05 '14 at 20:36
  • @kmacdonald: No editing. But other non-trivial interactions are helpful. Copy to clipboard. Selection of a subset for further processing. Searching. – Ben Voigt Apr 05 '14 at 20:37
  • @kmacdonald: Since colorization doesn't affect positioning, is there a way I could do this with a single `Run` per paragraph and just override the rendering? Should I be using a `Grid` or `ListView` instead? – Ben Voigt Apr 05 '14 at 20:40
  • 1
    If you really want to get into WPF i would suggest taking a look at how to really take advantage of the MVVM approach. Your example is a prime candidate for using An ItemsControl with a Datatemplate that will represent Each Row visually. Your datatemplate could consist of a single textblock. By simply binding to your Row Collection you could get a uniform set of rows to display... This is getting long winded. I will write up something as an answer to the best of my knowledge. – kmacdonald Apr 05 '14 at 20:45
  • @kmacdonald: Even though my data is allocated multiple values per row? I suppose I can break it into slices that map to rows, if that makes MVVM work better. – Ben Voigt Apr 05 '14 at 20:46
  • yes you could even have a itemControl to display each single byte value. so basically you would end up with a "Double array" of ItemControls. – kmacdonald Apr 05 '14 at 20:51
  • Can you post a sample screenshot of your intended result? – Federico Berasategui Apr 05 '14 at 21:04
  • Also I don't know if this makes any difference, but there will never be items changed individually. Changing either the source `byte[]` or the format selection will affect all these cells. – Ben Voigt Apr 05 '14 at 21:14

1 Answers1

1

Ok here is a crude version of what you could do in WPF.

    <ItemsControl ItemsSource="{Binding YourBindingRows}">
    <ItemsControl.Resources>
        <DataTemplate DataType="{x:Type Row}">
            <StackPanel Orientation="Horizontal">
                <TextBlock>Text Representation Here</TextBlock>
                <ItemsControl ItemsSource="{Binding YourBindingCells}">
                    <ItemsControl.Resources>
                        <DataTemplate DataType="{x:Type Cell}">
                            <TextBlock Text="{Binding HexValue}"></TextBlock>
                        </DataTemplate>
                    </ItemsControl.Resources>
                </ItemsControl>
            </StackPanel>
        </DataTemplate>
    </ItemsControl.Resources>
</ItemsControl>

You can achieve changing the background color by using ValueConverters and property Binding. This is another crude example of how to achieve this:

<TextBlock Text="{Binding HexValue}"  Background="{Binding ValueChanged, Converter={StaticResource ValueChangedBackgroundConverter}}">FF</TextBlock>

These are crude examples and i think you will have to do some more research into how to really take advantage of MVVM. However, your project sounds cool and i think it would work really well in the MVVM environment. For further research look into: INotifiedPropertyChanged for binding and IValueConverter for changing your color and other UI updates.

kmacdonald
  • 3,381
  • 2
  • 18
  • 22