Dan moet je meten waar 't wél zit (en bepalen wanneer / hoe je die tijd wil betalen; stukje-bij-beetje bij elke stukje werk dat je verzet, of in 1 klap nadat alle werk is verzet).
Het is dat ik even niks anders te doen heb, maar allow me to demonstrate. We gaan even uit van een Customer en een CustomerListviewItem:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| public class Customer
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime DateOfBirth { get; set; }
private static readonly Random _rng = new Random();
public static Customer CreateRandomCustomer(int id)
{
Thread.Sleep(1); // Fake some work
return new Customer
{
Id = id,
DateOfBirth = new DateTime(1900, 1, 1).AddDays(_rng.Next(365 * 100)),
FirstName = "Bob" + _rng.Next(99999),
LastName = "Doe" + _rng.Next(99999)
};
}
}
public class CustomerListViewItem : ListViewItem
{
public Customer Customer { get; set; }
public CustomerListViewItem(Customer customer)
: base(new[] { customer?.FirstName, customer?.LastName, customer?.DateOfBirth.ToString("yyyy-MM-dd") })
{
Customer = customer ?? throw new ArgumentNullException(nameof(customer));
}
} |
Maak een form en mik daar 2 buttons op: "AddButton" en "AbortButton". Tevens een ListView en een ProgressBar: "MainListView" en "MainProgressBar". Zet de ListView View op Details en geef 'm 3 kolommen.
Vervolgens plakken we in 't form:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows.Forms;
namespace WindowsFormsApp1
{
public partial class MainForm : Form
{
private BackgroundWorker _bgw;
public MainForm()
{
InitializeComponent();
}
private void AddButton_Click(object sender, EventArgs e)
{
AddCustomers(2500);
}
private void AbortButton_Click(object sender, EventArgs e)
{
_bgw?.CancelAsync();
}
}
} |
De _bgw is onze backgroundworker die we straks aan 't werk gaan zetten. Voeg een methode AddCustomers toe; deze doet niets anders dan een nieuwe BGW instantiëren (als er niet al 1 bezig is), 'configureert' deze en trapt 'm vervolgens aan om aan 't werk te gaan.
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| private void AddCustomers(int count)
{
// If we currently have no backgroundworker
if (_bgw == null)
{
// Create a backgroundworker, set it up and kick it off
using (_bgw = new BackgroundWorker() { WorkerReportsProgress = true, WorkerSupportsCancellation = true })
{
_bgw.DoWork += DoWork;
_bgw.RunWorkerCompleted += WorkCompleted;
_bgw.ProgressChanged += (s, pe) => { MainProgressBar.Value = pe.ProgressPercentage; };
_bgw.RunWorkerAsync(count);
}
}
else
{
throw new Exception("Already in progress");
}
} |
Ok; so far so good. Nu de code waar we mee gaan stoeien: DoWork() en WorkCompleted():
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| private void DoWork(object sender, DoWorkEventArgs e)
{
// Get reference to ourself and prepare a list of results
var self = (BackgroundWorker)sender;
var custcount = (int)e.Argument;
var results = new List<Customer>(custcount);
// Keep track of last reported progress
var lastreportedprogress = 0;
// Add "count" customers while not cancellation pending
for (var i = 0; i < custcount && !self.CancellationPending; i++)
{
// Do actual work
results.Add(Customer.CreateRandomCustomer(i));
// Keep track of progress
var progress = (int)((double)i / custcount * 100);
// If the actual percentage changed, report it
if (progress > lastreportedprogress)
{
self.ReportProgress(progress);
lastreportedprogress = progress;
}
}
// If not cancelled, report last status as 100
if (!self.CancellationPending)
self.ReportProgress(100);
// Return whether the action was cancelled
e.Cancel = self.CancellationPending;
// Return results
e.Result = results;
}
private void WorkCompleted(object sender, RunWorkerCompletedEventArgs e)
{
// Populate our listview with results (if any)
MainListView.BeginUpdate();
MainListView.Items.Clear();
if (!e.Cancelled)
{
var results = (IEnumerable<Customer>)e.Result;
MainListView.Items.AddRange(results.Select(r => new CustomerListViewItem(r)).ToArray());
}
MainListView.EndUpdate();
// Reset progressbar
MainProgressBar.Value = 0;
// Clear backgroundworker so we're ready for a new run
_bgw = null;
} |
Zoals je ziet vullen we hier in DoWork() alleen een list met puur de resultaten, niets anders. Dan, in de WorkCompleted() pakken we die resultaten en transformeren die (dat heet 'projecteren' in LINQ termen) naar onze ListViewItems en mikken die in 1 klap in de listview. Nu gebeurt er op 't moment vrij weinig in de WorkCompleted. Als we echter "veel werk" hebben om de listviewitems te maken (omdat er bijv. nog e.e.a. uitgeplozen moet worden) dan heb je dus alsnog dat je UI "hangt". Je kunt dus de "projectie" ook nog naar de DoWork() halen. Daarvoor verander je in bovenstaande code regel 6 naar:
C#:
1
| var results = new List<CustomerListViewItem>(custcount); |
en regel 13 naar:
C#:
1
| results.Add(new CustomerListViewItem(Customer.CreateRandomCustomer(i))); |
(waar je dan eventueel de rest van het werk nog verzet om het listviewitem te maken etc). Regels 41 & 42 kun je dan veranderen naar:
C#:
1
| MainListView.Items.AddRange(((IEnumerable<CustomerListViewItem>)e.Result).ToArray()); |
offtopic:
Helaas wil WinForms altijd arrays of andere vage collecties hebben i.p.v. dat het een IEnumerable<T> accepteert

Afhankelijk van waar je "werk" zit en hoe je je applicatie wil opzetten kun je er dus voor kiezen pure results te returnen en die te transformeren naar listviewitems naderhand
of meteen listviewitems te returnen.
In beide gevallen zie je je listview (wegens de BeginUpdate en EndUpdate) in 1 keer vol "ploepen". Totaal:
99 regels, waarvan
47 daadwerkelijke code.
Met een virtual listview ziet 't er heel anders uit. Althans: je kunt dan nog steeds een backgroundworker gebruiken en de lijst vullen zoals hierboven gedaan wordt maar dan "op de virtual manier". ECHTER; als je maar 25 listviewitems in beeld hebt, waarom dan wel 't werk van alle 2500 items verzetten? Je kunt op dat moment véél beter het werk pas verzetten op 't moment dat je de listviewitems op je scherm moet zetten wanneer men ze in beeld scrolt. Je hebt in dat geval niet eens een progressbar nodig (of het moet écht heel "duur" zijn om die 25 listviewitems op je scherm te krijgen).
De code uit
de link die ik je gaf vind ik zelf
vrij omslachtig. Ik zou 't zelf eerder op ongeveer deze manier schrijven (heel de code van MainForm kun je trashen):
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| using System;
using System.Collections.Concurrent;
using System.Windows.Forms;
namespace WindowsFormsApp1
{
public partial class MainForm : Form
{
// Cache items; key = index in listview, value = item
private ConcurrentDictionary<int, CustomerListViewItem> _cache;
public MainForm()
{
InitializeComponent();
// Set up retrieval of listviewitems
MainListView.RetrieveVirtualItem += (s, e) =>
{
// Check cache, if not in cache create item on demand
e.Item = _cache.GetOrAdd(e.ItemIndex, (i) =>
{
// Do the work for 1 item here...
return new CustomerListViewItem(Customer.CreateRandomCustomer(i));
});
};
}
private void AddButton_Click(object sender, EventArgs e)
{
AddCustomers(2500);
}
public void AddCustomers(int count)
{
_cache = new ConcurrentDictionary<int, CustomerListViewItem>(Environment.ProcessorCount, count);
MainListView.VirtualListSize = count;
MainListView.Refresh();
}
}
} |
Meer heb je niet nodig;
40 regels, waarvan
17 met daadwerkelijke code. Het neerzetten van een enkel item (of een "hele pagina" van 25 items) is dan, wellicht, wat trager maar je betaalt wél alleen maar voor de items die je ziet. Heel veel eleganter dan dat krijg je 't niet. Probeer 't eens met 1.000.000 customers
Wil je overigens niet alle items in-memory houden (in het geval van een miljoen items bijvoorbeeld) dan kun je natuurlijk iets anders (bijvoorbeeld een MemoryCache) gebruiken met een limit van, zeg, 100MB of een soortgelijke structuur en daar gewoon een FIFO-achtig iets op los laten of een andere eviction strategie. Maar we gaan te ver offtopic
Vervolgens kun je dit overigens weer makkelijk uitbreiden met een backgroundworker die in de achtergrond alvast "andere pagina's" gaat ophalen en "vooruit werken" (maar dat vereist een beetje nadenkwerk... en de kans dat 't de moeite loont is vrij klein).
Code
Zipfile
En dan tot slot, over een compleet andere boeg: op het moment dat je zoveel items in een listview zet moet je je ook even afvragen of er echt wel noodzaak is om de gebruiker te 'overstelpen' met zoveel data. Ik heb daar ooit
een stukkie over geschreven 
[Voor 14% gewijzigd door RobIII op 20-09-2018 02:14]
There are only two hard problems in distributed systems: 2. Exactly-once delivery 1. Guaranteed order of messages 2. Exactly-once delivery.
Roses are red Violets are blue, Unexpected ‘{‘ on line 32.
Over mij