Cross-reference table data in WPF

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>();
            // Removes
            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);
            }
            // Updates
            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 });
                }
            }
            // Additions
            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));
            // Notify
            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.

+2
1

, , , , winforms - .

WPF - .

, 15 . , ( ), .

, DataBinding:

public abstract class PropertyChangedBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        var handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

, Ctrl + Enter, ReSharper .

, , , ViewModel:

public class ViewModel : PropertyChangedBase
{
    private readonly ScoreBoard board;

    public ObservableCollection<string> Columns { get; private set; }

    public ObservableCollection<Game> Games { get; private set; } 

    public ObservableCollection<RowViewModel> Rows { get; private set; } 

    public ViewModel(ScoreBoard board)
    {
        this.board = board;
        this.board.Changed += OnBoardChanged;

        UpdateColumns(this.board.Games.Select(x => x.Name));
        UpdateRows(this.board.Players, this.board.Games);

        this.board.StartUpdate();
    }

    private void OnBoardChanged(object sender, ScoreBoardChangeEventArgs e)
    {
        var games = 
            this.board.Games
                      .Except(e.RemovedGames)
                      .Concat(e.AddedGames)
                      .ToList();

        this.UpdateColumns(games.Select(x => x.Name));

        var players =
            this.board.Players
                      .Except(e.RemovedPlayers)
                      .Concat(e.AddedPlayers)
                      .ToList();

        this.UpdateRows(players, games);
    }

    private void UpdateColumns(IEnumerable<string> columns)
    {
        this.Columns = new ObservableCollection<string>(columns);
        this.Columns.Insert(0, "Player");

        this.OnPropertyChanged("Columns");
    }

    private void UpdateRows(IEnumerable<Player> players, IEnumerable<Game> games)
    {
        var rows =
            from p in players
            let scores =
                from g in games
                select this.board.GetScore(p, g)
            let row = 
                new RowViewModel
                {
                    Player = p.Name,
                    Scores = new ObservableCollection<int>(scores)
                }
            select row;

        this.Rows = new ObservableCollection<RowViewModel>(rows);
        this.OnPropertyChanged("Rows");
    }
}

public class RowViewModel
{
    public string Player { get; set; }

    public ObservableCollection<int> Scores { get; set; }
}

XAML:

<Window x:Class="WpfApplication31.Window3"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window3" Height="300" Width="300">
    <Window.Resources>
        <Style TargetType="ItemsControl" x:Key="Horizontal">
            <Setter Property="ItemsPanel">
                <Setter.Value>
                    <ItemsPanelTemplate>
                        <StackPanel Orientation="Horizontal"/>
                    </ItemsPanelTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <Style TargetType="ListBoxItem">
            <Setter Property="Padding" Value="0"/>
        </Style>

        <DataTemplate x:Key="CellTemplate">
            <Border BorderBrush="Black" BorderThickness="1" Padding="5" Width="60">
                <TextBlock Text="{Binding}"
                           VerticalAlignment="Center"
                           HorizontalAlignment="Center"/>
            </Border>
        </DataTemplate>
    </Window.Resources>

    <DockPanel>
        <ItemsControl ItemsSource="{Binding Columns}"
                      Style="{StaticResource Horizontal}"
                      Margin="3,0,0,0"
                      ItemTemplate="{StaticResource CellTemplate}"
                      DockPanel.Dock="Top"/>

        <ListBox ItemsSource="{Binding Rows}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <ContentPresenter Content="{Binding Player}"
                                          ContentTemplate="{StaticResource CellTemplate}"/>

                        <ItemsControl ItemsSource="{Binding Scores}"
                                  Style="{StaticResource Horizontal}"
                                  ItemTemplate="{StaticResource CellTemplate}"/>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </DockPanel>
</Window>

, , XAML, DataGrid - , , ItemsControl s.

, Window, DataContext:

public partial class Window3 : Window
{
    public Window3()
    {
        InitializeComponent();

        var board = new ScoreBoard();
        this.DataContext = new ViewModel(board);
    }
}

:

enter image description here

  • ItemsControl Columns ( ) .
  • ListBox Rows, , ItemsControl . , winforms, WPF ListBox .
  • , , DataGrid, , , . SelectedRow VM, .
  • , - 100 . , , , , , , , , , . , .
  • , ViewModel (95 LOC 154 ), , .
+3

All Articles