This is caused by the following problem rendering the generated table using TableLayoutPanel, which ends up taking too long . There are other SO posts regarding WPF tabular data, but I don't think they cover this case (although How to display real table data with WPF is closer). The problem is interesting in that both the rows and columns are dynamic, and the view should not only display the data first, but also respond to addition / deletion (both rows and columns) and updates. I will introduce the WF path (because I have experience there) and would like to see and compare it with WPF methods.
Firstly, here is an example model that will be used in both cases:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
namespace Models
{
abstract class Entity
{
public readonly int Id;
protected Entity(int id) { Id = id; }
}
class EntitySet<T> : IReadOnlyCollection<T> where T : Entity
{
Dictionary<int, T> items = new Dictionary<int, T>();
public int Count { get { return items.Count; } }
public IEnumerator<T> GetEnumerator() { return items.Values.GetEnumerator(); }
IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }
public void Add(T item) { items.Add(item.Id, item); }
public bool Remove(int id) { return items.Remove(id); }
}
class Player : Entity
{
public string Name;
public Player(int id) : base(id) { }
}
class Game : Entity
{
public string Name;
public Game(int id) : base(id) { }
}
class ScoreBoard
{
EntitySet<Player> players = new EntitySet<Player>();
EntitySet<Game> games = new EntitySet<Game>();
Dictionary<int, Dictionary<int, int>> gameScores = new Dictionary<int, Dictionary<int, int>>();
public ScoreBoard() { Load(); }
public IReadOnlyCollection<Player> Players { get { return players; } }
public IReadOnlyCollection<Game> Games { get { return games; } }
public int GetScore(Player player, Game game)
{
Dictionary<int, int> playerScores;
int score;
return gameScores.TryGetValue(game.Id, out playerScores) && playerScores.TryGetValue(player.Id, out score) ? score : 0;
}
public event EventHandler<ScoreBoardChangeEventArgs> Changed;
#region Test
private void Load()
{
for (int i = 0; i < 20; i++) AddNewPlayer();
for (int i = 0; i < 10; i++) AddNewGame();
foreach (var game in games)
foreach (var player in players)
if (RandomBool()) SetScore(player, game, random.Next(1000));
}
public void StartUpdate()
{
var syncContext = SynchronizationContext.Current;
var updateThread = new Thread(() =>
{
while (true) { Thread.Sleep(100); Update(syncContext); }
});
updateThread.IsBackground = true;
updateThread.Start();
}
private void Update(SynchronizationContext syncContext)
{
var addedPlayers = new List<Player>();
var removedPlayers = new List<Player>();
var addedGames = new List<Game>();
var removedGames = new List<Game>();
var changedScores = new List<ScoreKey>();
if (RandomBool())
foreach (var player in players)
if (RandomBool()) { removedPlayers.Add(player); if (removedPlayers.Count == 10) break; }
if (RandomBool())
foreach (var game in games)
if (RandomBool()) { removedGames.Add(game); if (removedGames.Count == 5) break; }
foreach (var game in removedGames)
games.Remove(game.Id);
foreach (var player in removedPlayers)
{
players.Remove(player.Id);
foreach (var item in gameScores)
item.Value.Remove(player.Id);
}
foreach (var game in games)
{
foreach (var player in players)
{
if (!RandomBool()) continue;
int oldScore = GetScore(player, game);
int newScore = Math.Min(oldScore + random.Next(100), 1000000);
if (oldScore == newScore) continue;
SetScore(player, game, newScore);
changedScores.Add(new ScoreKey { Player = player, Game = game });
}
}
if (RandomBool())
for (int i = 0, count = random.Next(10); i < count; i++)
addedPlayers.Add(AddNewPlayer());
if (RandomBool())
for (int i = 0, count = random.Next(5); i < count; i++)
addedGames.Add(AddNewGame());
foreach (var game in addedGames)
foreach (var player in addedPlayers)
SetScore(player, game, random.Next(1000));
var handler = Changed;
if (handler != null && (long)addedGames.Count + removedGames.Count + addedPlayers.Count + removedPlayers.Count + changedScores.Count > 0)
{
var e = new ScoreBoardChangeEventArgs { AddedPlayers = addedPlayers, RemovedPlayers = removedPlayers, AddedGames = addedGames, RemovedGames = removedGames, ChangedScores = changedScores };
syncContext.Send(_ => handler(this, e), null);
}
}
Random random = new Random();
int playerId, gameId;
bool RandomBool() { return (random.Next() % 5) == 0; }
Player AddNewPlayer()
{
int id = ++playerId;
var item = new Player(id) { Name = "P" + id };
players.Add(item);
return item;
}
Game AddNewGame()
{
int id = ++gameId;
var item = new Game(id) { Name = "G" + id };
games.Add(item);
return item;
}
void SetScore(Player player, Game game, int score)
{
Dictionary<int, int> playerScores;
if (!gameScores.TryGetValue(game.Id, out playerScores))
gameScores.Add(game.Id, playerScores = new Dictionary<int, int>());
playerScores[player.Id] = score;
}
#endregion
}
struct ScoreKey
{
public Player Player;
public Game Game;
}
class ScoreBoardChangeEventArgs
{
public IReadOnlyList<Player> AddedPlayers, RemovedPlayers;
public IReadOnlyList<Game> AddedGames, RemovedGames;
public IReadOnlyList<ScoreKey> ChangedScores;
public long Count { get { return (long)AddedPlayers.Count + RemovedPlayers.Count + AddedGames.Count + RemovedGames.Count + ChangedScores.Count; } }
}
}
StoreBoard. , GetScore (, ) . , , , - . ( - ).
WF:
: IList , ITypedList PropertyDescriptor - IBindingList.ListChanged - .
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
namespace WfViewModels
{
using Models;
class ScoreBoardItemViewModel : CustomTypeDescriptor
{
ScoreBoardViewModel container;
protected ScoreBoard source { get { return container.source; } }
Player player;
Dictionary<int, int> playerScores;
public ScoreBoardItemViewModel(ScoreBoardViewModel container, Player player)
{
this.container = container;
this.player = player;
playerScores = new Dictionary<int, int>(source.Games.Count);
foreach (var game in source.Games) AddScore(game);
}
public Player Player { get { return player; } }
public int GetScore(Game game) { int value; return playerScores.TryGetValue(game.Id, out value) ? value : 0; }
internal void AddScore(Game game) { playerScores.Add(game.Id, source.GetScore(player, game)); }
internal bool RemoveScore(Game game) { return playerScores.Remove(game.Id); }
internal bool UpdateScore(Game game)
{
int oldScore = GetScore(game), newScore = source.GetScore(player, game);
if (oldScore == newScore) return false;
playerScores[game.Id] = newScore;
return true;
}
public override PropertyDescriptorCollection GetProperties()
{
return container.properties;
}
}
class ScoreBoardViewModel : BindingList<ScoreBoardItemViewModel>, ITypedList
{
internal ScoreBoard source;
internal PropertyDescriptorCollection properties;
public ScoreBoardViewModel(ScoreBoard source)
{
this.source = source;
properties = new PropertyDescriptorCollection(
new[] { CreateProperty("PlayerName", item => item.Player.Name, "Player") }
.Concat(source.Games.Select(CreateScoreProperty))
.ToArray()
);
source.Changed += OnSourceChanged;
}
public void Load()
{
Items.Clear();
foreach (var player in source.Players)
Items.Add(new ScoreBoardItemViewModel(this, player));
ResetBindings();
}
void OnSourceChanged(object sender, ScoreBoardChangeEventArgs e)
{
var count = e.Count;
if (count == 0) return;
RaiseListChangedEvents = count < 2;
foreach (var player in e.RemovedPlayers) OnRemoved(player);
foreach (var game in e.RemovedGames) OnRemoved(game);
foreach (var game in e.AddedGames) OnAdded(game);
foreach (var player in e.AddedPlayers) OnAdded(player);
foreach (var group in e.ChangedScores.GroupBy(item => item.Player))
{
int index = IndexOf(group.Key);
if (index < 0) continue;
bool changed = false;
foreach (var item in group) changed |= Items[index].UpdateScore(item.Game);
if (changed) ResetItem(index);
}
if (RaiseListChangedEvents) return;
RaiseListChangedEvents = true;
if (e.AddedGames.Count + e.RemovedGames.Count > 0)
OnListChanged(new ListChangedEventArgs(ListChangedType.PropertyDescriptorChanged, null));
if ((long)e.AddedPlayers.Count + e.RemovedPlayers.Count + e.ChangedScores.Count > 0)
ResetBindings();
}
void OnAdded(Player player)
{
if (IndexOf(player) >= 0) return;
Add(new ScoreBoardItemViewModel(this, player));
}
void OnRemoved(Player player)
{
int index = IndexOf(player);
if (index < 0) return;
RemoveAt(index);
}
void OnAdded(Game game)
{
if (IndexOf(game) >= 0) return;
var property = CreateScoreProperty(game);
properties.Add(property);
foreach (var item in Items)
item.AddScore(game);
if (RaiseListChangedEvents)
OnListChanged(new ListChangedEventArgs(ListChangedType.PropertyDescriptorAdded, property));
}
void OnRemoved(Game game)
{
int index = IndexOf(game);
if (index < 0) return;
var property = properties[index];
properties.RemoveAt(index);
foreach (var item in Items)
item.RemoveScore(game);
if (RaiseListChangedEvents)
OnListChanged(new ListChangedEventArgs(ListChangedType.PropertyDescriptorDeleted, property));
}
int IndexOf(Player player)
{
for (int i = 0; i < Count; i++)
if (this[i].Player == player) return i;
return -1;
}
int IndexOf(Game game)
{
var propertyName = ScorePropertyName(game);
for (int i = properties.Count - 1; i >= 0; i--)
if (properties[i].Name == propertyName) return i;
return -1;
}
string ITypedList.GetListName(PropertyDescriptor[] listAccessors) { return null; }
PropertyDescriptorCollection ITypedList.GetItemProperties(PropertyDescriptor[] listAccessors) { return properties; }
static string ScorePropertyName(Game game) { return "Game_" + game.Id; }
static PropertyDescriptor CreateScoreProperty(Game game) { return CreateProperty(ScorePropertyName(game), item => item.GetScore(game), game.Name); }
static PropertyDescriptor CreateProperty<T>(string name, Func<ScoreBoardItemViewModel, T> getValue, string displayName = null)
{
return new ScorePropertyDescriptor<T>(name, getValue, displayName);
}
class ScorePropertyDescriptor<T> : PropertyDescriptor
{
string displayName;
Func<ScoreBoardItemViewModel, T> getValue;
public ScorePropertyDescriptor(string name, Func<ScoreBoardItemViewModel, T> getValue, string displayName = null) : base(name, null)
{
this.getValue = getValue;
this.displayName = displayName ?? name;
}
public override string DisplayName { get { return displayName; } }
public override Type ComponentType { get { return typeof(ScoreBoardItemViewModel); } }
public override bool IsReadOnly { get { return true; } }
public override Type PropertyType { get { return typeof(T); } }
public override bool CanResetValue(object component) { return false; }
public override object GetValue(object component) { return getValue((ScoreBoardItemViewModel)component); }
public override void ResetValue(object component) { throw new NotSupportedException(); }
public override void SetValue(object component, object value) { throw new NotSupportedException(); }
public override bool ShouldSerializeValue(object component) { return false; }
}
}
}
: WF- - , , Reset , .
:
using System;
using System.Drawing;
using System.Windows.Forms;
namespace Views
{
using Models;
using ViewModels;
class ScoreBoardView : Form
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new ScoreBoardView { WindowState = FormWindowState.Maximized });
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
var source = new ScoreBoard();
viewModel = new ScoreBoardViewModel(source);
InitView();
viewModel.Load();
source.StartUpdate();
}
ScoreBoardViewModel viewModel;
DataGridView view;
void InitView()
{
view = new DataGridView { Dock = DockStyle.Fill, Parent = this };
view.Font = new Font("Microsoft Sans Serif", 25, FontStyle.Bold);
view.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
view.MultiSelect = false;
view.CellBorderStyle = DataGridViewCellBorderStyle.None;
view.ForeColor = Color.Black;
view.AllowUserToAddRows = view.AllowUserToDeleteRows = view.AllowUserToOrderColumns = view.AllowUserToResizeRows = false;
view.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
view.RowHeadersVisible = false;
view.EnableHeadersVisualStyles = false;
var style = view.DefaultCellStyle;
style.SelectionForeColor = style.SelectionBackColor = Color.Empty;
style = view.ColumnHeadersDefaultCellStyle;
style.SelectionForeColor = style.SelectionBackColor = Color.Empty;
style.BackColor = Color.Navy;
style.ForeColor = Color.White;
style = view.RowHeadersDefaultCellStyle;
style.SelectionForeColor = style.SelectionBackColor = Color.Empty;
style = view.RowsDefaultCellStyle;
style.SelectionForeColor = style.ForeColor = Color.Black;
style.SelectionBackColor = style.BackColor = Color.AliceBlue;
style = view.AlternatingRowsDefaultCellStyle;
style.SelectionForeColor = style.ForeColor = Color.Black;
style.SelectionBackColor = style.BackColor = Color.LightSteelBlue;
view.ColumnAdded += OnViewColumnAdded;
view.DataSource = viewModel;
view.AutoResizeColumnHeadersHeight();
view.RowTemplate.MinimumHeight = view.ColumnHeadersHeight;
}
private void OnViewColumnAdded(object sender, DataGridViewColumnEventArgs e)
{
var column = e.Column;
if (column.ValueType == typeof(int))
{
var style = column.DefaultCellStyle;
style.Alignment = DataGridViewContentAlignment.MiddleRight;
style.Format = "n0";
}
}
}
}
.
WPF. , " " WF WPF - () WPF.
EDIT: . " " WF. ( ICustomTypeDescriptor), WF, WPF.