Showing posts with label raw data. Show all posts
Showing posts with label raw data. Show all posts

Sunday, 15 January 2023

Working-With-Byte-Arrays

In this post I will show how to read/write different data type values to and from a byte buffer. This should prove useful when working with block data or pages. In this scenario, pages describes database pages or similar block data mechanisms.


Top

In this post...

Top

How To Handle Typed Values

The aim is to read/write different typed values from a byte array. In C++ one can achieve this by using a union. In C# we achieve this by using a struct/class using explicit layout. Explicit layout is the key here as it ensures types are aligned correctly.

The basic idea is to have a number of byte values offset from zero to maximum bytes required. Then add basic types, such as int 16, int 32, int 64, float, etc all at offset zero. Then one can set a float value and read of the four byte values. The same can be done for most basic types, set the value and the read the corresponding byte values. To read typed values, simply populate the byte values accordingly then read the basic type info (int 16, int 32, int float, etc).

For those with a COM background this approach is similar to the VARIANT type.

Top

The Union Value Type


using System;
using System.Runtime.InteropServices;
namespace Blog
{
  [StructLayout(LayoutKind.Explicit)]
  public struct Value
  {
    [FieldOffset(0)] public byte B0;
    [FieldOffset(1)] public byte B1;
    [FieldOffset(2)] public byte B2;
    [FieldOffset(3)] public byte B3;
    [FieldOffset(4)] public byte B4;
    [FieldOffset(5)] public byte B5;
    [FieldOffset(6)] public byte B6;
    [FieldOffset(7)] public byte B7;

    [FieldOffset(0)] public float Float;
    [FieldOffset(0)] public double Double;
    [FieldOffset(0)] public Int16 Int16;
    [FieldOffset(0)] public Int32 Int32;
    [FieldOffset(0)] public Int64 Int64;
  }
}

The above struct declaration shows eight bytes, B0-B7 for data transfer. If using the decimal type this will need to be expanded to include 16 bytes! As can be seen, B0-B7 is byte aligned and offset accordingly.

Note also, that basic types float, double, Int16, etc all start at offset zero. So, if one sets the Float field, the corresponding 4 bytes can be read from B0-B3. To reverse the action, set fields B0-B3, then read the float value.

It is imperative that one knows how many bytes are required for a typed value. The above structure should cover most data types bar decimal. DateTime values can also be stored by first converting to long and using the Int64 field or bytes B0-B7 to recreate.

Top

Testing The Union Value Type

The above can be tested using the following code...

static void RawTest()
{
  Value value = new Value();
  byte[] data = new byte[128];

  int cursor = 0;
  
  // *********************************************************************************************
  // Writing values to the data byte array.
  // *********************************************************************************************
  Int16 v1 = Int16.MaxValue - 1067;
  Int32 v2 = Int32.MaxValue - 10067;
  float v3 = float.MaxValue - 1.07896f;

  // Write an Int16 value to the data byte array.
  // First set the Value's Int16 field, then read two Value bytes.
  value.Int16 = v1;
  data[cursor++] = value.B0;
  data[cursor++] = value.B1;

  // Write an Int32 value to the data byte array.
  // First set the Value's Int32 field, then read four Value bytes.
  value.Int32 = v2;
  data[cursor++] = value.B0;
  data[cursor++] = value.B1;
  data[cursor++] = value.B2;
  data[cursor++] = value.B3;

  // Write a float value to the data byte array.
  // First set the Value's Float field, then read four Value bytes.
  value.Float = v3;
  data[cursor++] = value.B0;
  data[cursor++] = value.B1;
  data[cursor++] = value.B2;
  data[cursor++] = value.B3;

  
  // *********************************************************************************************
  // Reading values from the data byte array.
  // *********************************************************************************************
  cursor = 0;
  
  // Read an Int16 from the data byte array.
  // Set Value's first two byte fields, then read the Value's Int16 field.
  value.B0 = data[cursor++];
  value.B1 = data[cursor++];
  var v1Result = value.Int16;

  // Read an Int32 from the data byte array.
  // Set Value's first four byte fields, then read the Value's Int32 field.
  value.B0 = data[cursor++];
  value.B1 = data[cursor++];
  value.B2 = data[cursor++];
  value.B3 = data[cursor++];
  var v2Result = value.Int32;

  // Read a float from the data byte array.
  // Set Value's first four byte fields, then read the Value's Float field.
  value.B0 = data[cursor++];
  value.B1 = data[cursor++];
  value.B2 = data[cursor++];
  value.B3 = data[cursor++];
  var v3Result = value.Float;

  Console.WriteLine(v1);
  Console.WriteLine(v1Result);
  Console.WriteLine(v2);
  Console.WriteLine(v2Result);
  Console.WriteLine(v3);
  Console.WriteLine(v3Result);
}
Top

Writing Values To A Byte Array

Figure 2 illustrates a byte array filled with values from the previous test code. As the diagram illustrates, data values are byte aligned accordingly. That is, a 16 bit-integer requires 2 bytes, a float or 32-bit integer requires 4 bytes. This approach works well for data blocks comprising of byte arrays. Simply create a byte array, write values and save to disk. Conversely, load a byte array block from disk into memory, then proceed to read actual values.

Top

Improving The Value Interface

The current method of reading/writing values is somewhat verbose. One needs to track the byte array offset (cursor) and the offset to add following a read or write. In addition, explicitly reading and writing to the Value's byte fields (B0...BN) is tiresome and error-prone. The following should help alleviate these problems...

  • Specify the byte array and initial offset.
  • Specify a cursor that is relative to the specified initial offset.
  • Update the cursor accordingly following a read or write operation.
  • Allow the cursor's position to be set manually, the cursor will always be relative to the specified offset.

The following class, ByteBuffer, implements the above features.

using System;

namespace Blog
{
  /// <summary>
  /// Allows values to read/written to/from a byte array.
  /// Specify a byte buffer and initial offset in the constructor.
  /// Data will be read/written at this offset.
  /// The class uses a cursor to indicate current read/write position.
  /// The cursor is always offset by the offset specified in the constructor.
  /// </summary>
  public class ByteBuffer
  {
    private Value _value = new Value();
    private int _internalCursor;
    private int _offset;
    private readonly byte[] _buffer;

    public int Cursor => _internalCursor - _offset;

    public ByteBuffer(byte[] buffer, int offset)
    {
      _buffer = buffer;
      _offset = offset;
      _internalCursor = offset;
    }

    public ByteBuffer SetCursor(int position)
    {
      _internalCursor = _offset + position;
      return this;
    }

    public ByteBuffer Int16(Int16 value)
    {
      _value.Int16 = value;
      _buffer[_internalCursor++] = _value.B0;
      _buffer[_internalCursor++] = _value.B1;
      return this;
    }

    public ByteBuffer Int16(out Int16 result)
    {
      _value.B0 = _buffer[_internalCursor++];
      _value.B1 = _buffer[_internalCursor++];
      result = _value.Int16;
      return this;
    }

    public ByteBuffer Int32(Int32 value)
    {
      _value.Int32 = value;
      _buffer[_internalCursor++] = _value.B0;
      _buffer[_internalCursor++] = _value.B1;
      _buffer[_internalCursor++] = _value.B2;
      _buffer[_internalCursor++] = _value.B3;
      return this;
    }

    public ByteBuffer Int32(out Int32 result)
    {
      _value.B0 = _buffer[_internalCursor++];
      _value.B1 = _buffer[_internalCursor++];
      _value.B2 = _buffer[_internalCursor++];
      _value.B3 = _buffer[_internalCursor++];

      result = _value.Int32;
      return this;
    }

    public ByteBuffer Float(float value)
    {
      _value.Float = value;
      _buffer[_internalCursor++] = _value.B0;
      _buffer[_internalCursor++] = _value.B1;
      _buffer[_internalCursor++] = _value.B2;
      _buffer[_internalCursor++] = _value.B3;
      return this;
    }

    public ByteBuffer Float(out float result)
    {
      _value.B0 = _buffer[_internalCursor++];
      _value.B1 = _buffer[_internalCursor++];
      _value.B2 = _buffer[_internalCursor++];
      _value.B3 = _buffer[_internalCursor++];
      result = _value.Float;
      return this;
    }
  }
}
Top

Using The ByteBuffer Class

Using the ByteBuffer class is fairly straightforward. Simply call the constructor with a byte array and offset. No data can be read or written before the offset.

A sample test/driver program now follows...

static void Main(string[] args)
{
  byte[] data = new byte[128];
  ByteBuffer buffer = new ByteBuffer(data, 5);

  Int16 v1 = Int16.MaxValue - 1067;
  Int32 v2 = Int32.MaxValue - 10067;
  float v3 = float.MaxValue - 1.07896f;

  int bytesWritten = buffer
    .Int16(v1)
    .Int32(v2)
    .Float(v3)
    .Cursor;
  Console.WriteLine($"{bytesWritten} bytes written to buffer.");

  int bytesRead = buffer
    .SetCursor(0)
    .Int16(out var v1Read)
    .Int32(out var v2Read)
    .Float(out var v3Read)
    .Cursor;

  Console.WriteLine($"{bytesRead} bytes read from buffer.");
  Console.WriteLine($"v1Write:{v1} - v1Read:{v1Read}");
  Console.WriteLine($"v2Write:{v2} - v2Read:{v2Read}");
  Console.WriteLine($"v3Write:{v3} - v3Read:{v3Read}");
}

The above code wites the following to the console.

Notice how the test program uses an offset of 5 when constructing the ByteBuffer. The correct bytes read/wrriten of ten is still returned. The first five bytes in this example will be zeroed.

Top

Friday, 23 September 2022

Displaying Raw Data In A Grid - Winforms

So, I wanted a way to quickly display data in Winforms (C#). Granted, we have ListView, but I hate the ceremony involved to populate a ListView. So, I decided to develop something similar to a ListView, but with a more econimcal API. I think I achieved that. Not quite as hard as you may think. I called my new control View, whch exists within the UI.UIGrid namespace.

In this post...

Top

1. Grid View Goals

  • Simple API
  • Fast
  • Extendible

My first take meets the first two criteria.

Top

2. Test Data

As with all things software, testing is key. For this particular solution a list of something is required. I opted for a simple list of customers. where the list count may be specified. The code for the test suite follows.

Top

2.1 Customer

using System;

namespace UI.App.DataAccess
{
  public class Customer
  {
    public string FirstName { get; }
    public string LastName { get; }
    public DateTime DOB { get; }

    public Customer(
      string firstName,
      string lastName,
      DateTime dob)
    {
      this.FirstName = firstName;
      this.LastName = lastName;
      this.DOB = dob;
    }
  }
}

Top

2.2. Customer Mock Data

using System;
using System.Collections.Generic;

namespace UI.App.DataAccess
{
  public static class MockData
  {
    public static IEnumerable<Customer> Random(int count)
    {
      DateTime start = new DateTime(1995, 1, 1);
      int range = (DateTime.Today - start).Days;

      for (int i=0; i<count; i++)
      {
        yield return new Customer(
          $"First{i}",
          $"Last{i}",
          start.AddDays(i));
      }
    }
  }
}
Top

3. Grid View As A User Control

Figure 1: Grid View

The above image illustrates the components of the grid view. The grid view consists of a User Control that contains the following...

  1. A header (UIColumns in this case)
    UIColumns is a Panel control that is used to display individual coumns. UIColumns overrides the Paint event.
  2. A rows view (a Panel derived view)
    No surprises here, the rows view is responsible for painting row data.
Top

3.1. Using the Grid View

To use my grid control, from say, a form, I used the following code...

protected override void OnLoad(EventArgs e)
{
  new UIGrid.View()
  {
    Parent = this,
    Dock = DockStyle.Fill,
  }
  .WithColumn("First Name", 80)
  .WithColumn("Last Name", 80)
  .WithColumn("DOB", 80)
  .WithData(
  DataAccess.MockData.Random(50000),
  (c, cells) =>
  {
    cells[0] = c.FirstName;
    cells[1] = c.LastName;
    cells[2] = c.DOB.ToString("dd/MM/yyyy");
  });
}
Top

3.2. Grid View Output

So, the expected output of my grid view is as follows...

Let's introduce some concepts...

  • The view will have columns.
  • The view will have rows where each row's width is specified by its column.
  • Each row will contain one or more cells, essentially strings for now.

4. Implementation

In this section I will show/discuss the implementation used to realise my original concept. Here goes...

Top

4.1. Column

using System;

namespace UI.UIGrid
{
  public class Column
  {
    public string Title { get; }
    public int Width { get; }
    
    public Column(
      string title,
      int width)
    {
      this.Title = title;
      this.Width = width;
    }

    public override string ToString() =>
      $"{Title}, {Width}";
  }
}

The column class is simple, it simply stores the column title and the column width.

Top

4.2. Rows

namespace UI.UIGrid
{
  public class Row
  {
    public View View { get; }
    public string[] Cells { get; }

    public Row(
      View view,
      string[] cells)
    {
      this.View = view;
      this.Cells = cells;
    }
  }
}

The row class is also simple. It maintains a back pointer to the View, and a collection of cells (strings). An array of strings is used to optimise lookup.

Top

4.3. Rows View

using System.Windows.Forms;

namespace UI.UIGrid
{
  public partial class RowsView : Panel
  {
    public RowsView()
    {
      InitializeComponent();
      DoubleBuffered = true;
      ResizeRedraw = true;
    }
  }
}
Top

4.4. View

The view class is reponsible for drawing columns and rows, and processing events. One could create separate, panel-derived classes, one for columns and one for rows. The problem with this approach is sharing data between the different controls. My approach simplifies data exchange (all in one class, the View) at the expense of slightly more verbose code. Software, always trade offs!

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;

namespace UI.UIGrid
{
  public partial class View : UserControl
  {
    /// <summary>
    /// Two controls, one for columns, one for rows.
    /// Allow easy access to scroll info, given a control.
    /// </summary>
    public struct ScrollInfo
    {
      public int X;
      public int Y;

      public ScrollInfo(int x, int y)
      {
        this.X = x;
        this.Y = y;
      }

      public static ScrollInfo FromControl(Panel c)
      {
        return new ScrollInfo(
          -c.AutoScrollPosition.X,
          -c.AutoScrollPosition.Y);
      }
    }

    private readonly List<Column>; _columns = new List<Column>();
    private readonly List<Row> _rows = new List<Row>();
    private readonly Color _gridColor = Color.Gainsboro;
    private int _rowHeight;

    public View()
    {
      InitializeComponent();
      UIColumnsInit();
      UIRowsInit();
      _rowHeight = Font.Height + 4;
    }

    public View WithColumn(string title, int width)
    {
      _columns.Add(new Column(title, width));
      return this;
    }

    public View WithData<T>(
      IEnumerable<T> data,
      Action<T, string[]> mapRowCells)
    {
      _rows = data
        .Select(r =>
        {
          string[] row = new string[_columns.Count];
          mapRowCells(r, row);
          return new Row(this, row);
        }).ToList();
      return this;
    }

    private void UIColumnsInit()
    {
      UIColumns.Height = Font.Height;
      UIColumns.Paint += UIColumns_Paint;
    }

    private void ForceColumnsRepaint()
    {
      UIColumns.Invalidate();
      UIColumns.Update();
    }

    private void UIColumns_Paint(object sender, PaintEventArgs e)
    {
      e.Graphics.TranslateTransform(
        UIRows.AutoScrollPosition.X,
        0);

      Rectangle rcCol = new Rectangle(
        UIColumns.ClientRectangle.Left,
        UIColumns.ClientRectangle.Top,
        0,
        UIColumns.ClientRectangle.Height);

      _columns.ForEach(col =>
      {
        rcCol.Width = col.Width;
        e.Graphics.DrawString(col.Title, Font, Brushes.White, rcCol);
        e.Graphics.DrawLine(Pens.Gainsboro, rcCol.Right - 1, rcCol.Top, rcCol.Right - 1, rcCol.Bottom);
        rcCol.X = rcCol.Right;
      });
    }

    private void UIRowsInit()
    {
      UIRows.Paint += UIRows_PaintNaive;
      UIRows.Scroll += UIRows_Scroll;
      UIRows.Resize += (e, s) => ForceColumnsRepaint();
    }

    private void UIRows_Scroll(object sender, ScrollEventArgs e)
    {
      // Force columns to repaint upon a horizontal scroll event.
      if (e.ScrollOrientation == ScrollOrientation.HorizontalScroll)
        ForceColumnsRepaint();
    }

    /// <summary>
    /// Paint vertical grid lines in UIRows control.
    /// </summary>
    /// <param name="g"></param>
    /// <returns>Total columns width.</returns>
    private int PaintVerticalGridLines(
      Graphics g,
      Color lineColor,
      int yScroll)
    {
      int right = 0;
      using (Pen pen = new Pen(lineColor, 1))
      {
        _columns.ForEach(c =>
        {
          right += c.Width;
          g.DrawLine(
            pen,
            right - 1,
            UIRows.DisplayRectangle.Top,
            right - 1,
            UIRows.DisplayRectangle.Bottom + yScroll);
        });
      }
      >return right;
    }

    private void PaintRow(
      Graphics g,
      Pen gridPen,
      Font font,
      Rectangle rcRow,
      int xScroll,
      Row row)
    {
      Rectangle rcCell = rcRow;
      rcCell.X = 0;
      for (int cell = 0; cell < _columns.Count; cell++)
      {
        rcCell.Width = _columns[cell].Width;
        g.DrawString(row.Cells[cell], font, Brushes.Black, rcCell);
        rcCell.X = rcCell.Right;
      }
      g.DrawLine(gridPen, rcRow.Left, rcCell.Bottom, rcRow.Right + xScroll, rcCell.Bottom);
    }

    private void UIRows_PaintNaive(object sender, PaintEventArgs e)
    {
      e.Graphics.TranslateTransform(
        UIRows.AutoScrollPosition.X,
        UIRows.AutoScrollPosition.Y);

      ScrollInfo si = ScrollInfo.FromControl(UIRows);
      Rectangle rcDisp = UIRows.ClientRectangle;
      Rectangle rcRow = new Rectangle(0, 0, DisplayRectangle.Width + si.X, _rowHeight);
      rcDisp.Offset(-UIRows.AutoScrollPosition.X, -UIRows.AutoScrollPosition.Y);

      int yPos = 0;
      using (Pen penGrid = new Pen(_gridColor, 1))
      {
        for (int row = 0; row < _rows.Count; row++)
        {
          rcRow.Y = yPos;
          PaintRow(e.Graphics, penGrid, Font, rcRow, si.X, _rows[row]);
          yPos += _rowHeight;
        }
      }

      int right = PaintVerticalGridLines(e.Graphics, Color.Gainsboro, si.X);
      UIRows.AutoScrollMinSize = new Size(right, _rows.Count * _rowHeight);
    }

    private void UIRows_Paint(object sender, PaintEventArgs e)
    {      
      e.Graphics.TranslateTransform(
        UIRows.AutoScrollPosition.X,
        UIRows.AutoScrollPosition.Y);

      ScrollInfo si = ScrollInfo.FromControl(UIRows);      
      Rectangle rcDisp = UIRows.ClientRectangle;
      Rectangle rcRow = new Rectangle(0,0,DisplayRectangle.Width + si.X,_rowHeight);
      rcDisp.Offset(-UIRows.AutoScrollPosition.X, -UIRows.AutoScrollPosition.Y);      

      int yPos = 0;
      using (Pen penGrid = new Pen(_gridColor, 1))
      {
        for (int row = 0; row < _rows.Count; row++)
        {
          rcRow.Y = yPos;
          if (rcRow.IntersectsWith(rcDisp))
            PaintRow(e.Graphics, penGrid, Font, rcRow, si.X, _rows[row]);
          yPos += _rowHeight;
        }
      }

      int right = PaintVerticalGridLines(e.Graphics, Color.Gainsboro, si.X);
      UIRows.AutoScrollMinSize = new Size(right, _rows.Count * _rowHeight);
    }
  }
}
Top

Code Analysis

Most of the code should be easy enough to follow. However, some may have noticed that I have two row paint methods, namely, UIRows_PaintNaive and UIRows_Paint. One is optimal and the other is sub-optimal and, for large row counts will appear to update slowly. Of course, one could argue that displaying more than, say, a 1000 rows is not ideal. It is probably better to offer a search or paging mechanism. Still, sometimes, for debugging purposes, algorithmic purposes, displaying a large row count might be useful.

In both cases, the document size is calculated. The UIRows control's AutoScrollMinSize is updated to reflect the document size. Note, while the AutoScrollMinSize is updated, I refrain from setting AutoScroll to true. I noticed, during development and testing that AutoScroll set to true can cause problems. The header is repainted if the user performs a horizontal scroll.

One can view the overall painting process as a view within a larger view. This is typically known as a viewport. A viewport is simply the visible area within a document that is too big to display in its entirety. One can actually envisage a viewport as an actual window. If you look straight ahead out of a window you will get one perspective. If you now move your head, and say, bend at you your knees, you will see an entirely different perspective. The point is, you cannot see all there is too see out of your window. To see more you must reorient yourself, or invest in a larger window.

The above diagram illustrates considerations required when painting a document that is much larger than the available display size.

The following analyses the two different row paint approaches.

Naive Paint Method

The naive paint method iterates and draws all rows. The Operating System will clip accordingly. However, calculations are still performed for each row. Also, clipping will increase the time required to process each row.

As one might expect, this is the least performant approach, as we attemptto calculate, and paint all rows, regardless of whether or not they fall within the viewport area.