I'm working on an application that has a list view that is used as a Hex Viewer, but there are important performance issues. I'm not a very experienced programmer, so I don't know how to optimize the code.
The idea is to avoid the application freezes when adding the items into the list view. I was thinking adding the items in groups of 100 items more or less, but I don't know how to deal with scroll up and down, and I'm not sure if this would solve these performance issues.
The control will always have the same height, 795px. Here's the code I'm using:
private void Button_SearchFile_Click(object sender, EventArgs e)
{
//Open files explorer.
OpenFileDialog_SearchFile.Filter = "SFX Files (*.sfx)|*.sfx";
DialogResult openEuroSoundFileDlg = OpenFileDialog_SearchFile.ShowDialog();
if (openEuroSoundFileDlg == DialogResult.OK)
{
//Get the selected file
string filePath = OpenFileDialog_SearchFile.FileName;
Textbox_FilePath.Text = filePath;
//Clear list.
ListView_HexEditor.Items.Clear();
//Start Reading.
using (BinaryReader BReader = new BinaryReader(File.OpenRead(filePath)))
{
//Each line will contain 16 bits.
byte[] bytesRow = new byte[16];
while (BReader.BaseStream.Position != BReader.BaseStream.Length)
{
//Get current offset.
long offset = BReader.BaseStream.Position;
//Read 16 bytes.
byte[] readedBytes = BReader.ReadBytes(16);
//Sometimes the last read could not contain 16 bits, with this we ensure to have a 16 bits array.
Buffer.BlockCopy(readedBytes, 0, bytesRow, 0, readedBytes.Length);
//Add item to the list.
ListView_HexEditor.Items.Add(new ListViewItem(new[]
{
//Print offset
offset.ToString("X8"),
//Merge bits
((bytesRow[0] << 8) | bytesRow[1]).ToString("X4"),
((bytesRow[2] << 8) | bytesRow[3]).ToString("X4"),
((bytesRow[4] << 8) | bytesRow[5]).ToString("X4"),
((bytesRow[6] << 8) | bytesRow[7]).ToString("X4"),
((bytesRow[8] << 8) | bytesRow[9]).ToString("X4"),
((bytesRow[10] << 8) | bytesRow[11]).ToString("X4"),
((bytesRow[12] << 8) | bytesRow[13]).ToString("X4"),
((bytesRow[14] << 8) | bytesRow[15]).ToString("X4"),
//Get hex ASCII representation
GetHexStringFormat(bytesRow)
}));
}
}
}
}
private string GetHexStringFormat(byte[] inputBytes)
{
//Get char in ascii encoding
char[] arrayChars = Encoding.ASCII.GetChars(inputBytes);
for (int i = 0; i < inputBytes.Length; i )
{
//Replace no printable chars with a dot.
if (char.IsControl(arrayChars[i]) || (arrayChars[i] == '?' && inputBytes[i] != 63))
{
arrayChars[i] = '.';
}
}
return new string(arrayChars);
}
And here is an image of the program:
CodePudding user response:
This is a common problem, where any long-running action on the main UI thread freezes the application. The normal solutions are either to run the operation on a background thread or as an asynchronous method... or more commonly as a hybrid of the two.
The main problem here though is that ListView
is quite slow when loading large amounts of data. Even the fastest method - bulk loading the entire collection of ListViewItem
s - is slow. A quick test with a 340KB file took
~0.18s to load the items, then ~2.3s to add the items to the control. And since the last part has to happen on the UI thread, that's ~2.3s of dead time.
The smoothest solution with ListView
working on large lists is to use virtual list mode. In this mode the ListView
requests the visible items whenever the list scrolls rather than maintaining its' own list of items.
To implement a virtual ListView
you provide a handler for the RetrieveVirtualItem
event, set the VirtualListSize
property to the length of the list, then set VirtualMode
to true. The ListView
will call your RetrieveVirtualItem
handler whenever it needs an item for display.
Here's my PoC code:
// Loaded data rows.
ListViewItem[]? _rows = null;
// Load data and setup ListView for virtual list.
// Uses Task.Run() to (hopefully) get off the UI thread.
private Task LoadData(string filename)
=> Task.Run(() =>
{
// Clear current virtual list
// NB: Invoke() makes this run on the main UI thread
Invoke((Action)(() =>
{
listView1.BeginUpdate();
listView1.VirtualMode = false;
listView1.VirtualListSize = 0;
listView1.EndUpdate();
}));
// Read data into '_rows' field.
using (var stream = File.OpenRead(filename))
{
var buffer = new byte[16];
var rows = new List<ListViewItem>();
int rc;
while ((rc = stream.Read(buffer, 0, buffer.Length)) > 0)
{
var items = new[]
{
(stream.Position - rc).ToString("X8"),
string.Join(" ", buffer.Take(rc).Select(b => $"{b:X2}")),
string.Join("", buffer.Take(rc).Select(b => (char)b).Select(b => char.IsControl(b) ? '.' : b)),
};
rows.Add(new ListViewItem(items));
}
_rows = rows.ToArray();
}
// Enable virtual list mode
Invoke((Action)(() =>
{
listView1.BeginUpdate();
listView1.VirtualListSize = _rows?.Length ?? 0;
listView1.VirtualMode = _rows?.Length > 0;
listView1.EndUpdate();
}));
});
// Fetch rows from loaded data.
private void ListView1_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
{
if (_rows is not null && e.ItemIndex >= 0 && e.ItemIndex < _rows.Length)
e.Item = _rows[e.ItemIndex];
}
(Insert usual disclaimers about lack of error handling and so on.)
Hopefully that's fairly self-explanatory. I took some LINQ shortcuts in the content generation, and cut the column count down. More columns means slower refresh times during scrolling, whether you're using a virtual list or letting the control handle the item collection.
For really large files this method can still be an issue. If you're routinely loading files in the 10s of MB in size or larger then you need to get a bit more creative about loading chunks of data at a time, and maybe creating and caching the ListViewItem
s on-the-fly rather than during the initial load phase. Have a look at the ListView.VirtualMode
documentation. The example code shows the full functionality... although their caching strategy is a little rudimentary.