c# - WPF 中的跨表格数据绑定(bind)

标签 c# wpf winforms

这受到以下问题的启发 Rendering a generated table with TableLayoutPanel taking too long to finish .还有其他关于 WPF 表格数据的 SO 帖子,但我认为它们没有涵盖这种情况(尽管 How to display real tabular data with WPF? 更接近)。这个问题很有趣,因为行和列都是动态的, View 不仅应该最初显示数据,还应该对添加/删除(行和列)和更新使用react。我将介绍 WF 方式(因为我有这方面的经验)并希望看到它并将其与 WPF 方式进行比较。

首先,这是在这两种情况下使用的示例模型:

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 功能(玩家、游戏)和多用途批量更改通知。我希望它以表格格式呈现,行是玩家,列是游戏,它们的交集是分数。此外,所有更新都应以结构化方式完成(使用某种数据绑定(bind))。

WF具体解决方案:

View 模型: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 数据绑定(bind)缺陷之一 - 我们卡在了单个项目列表更改通知中,如果要应用大量更改或暴力破解,这是无效的 Reset 任何列表数据呈现器都无法有效处理的通知。

View :

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 解决方案非常感兴趣。

编辑:事实上,我错了。我的“ View 模型”不是特定于 WF 的。我对它进行了更新(使用 ICustomTypeDescriptor),现在它可以在 WF 和 WPF 中使用。

最佳答案

因此,您的解决方案非常复杂,并且诉诸于诸如使用反射之类的 hack,这并不让我感到惊讶,因为 winforms 是一种非常过时的技术,并且所有事情都需要这样的 hack。

WPF 是现代 UI 框架,不需要任何这些。

这是一个非常幼稚的解决方案,我在 15 分钟内就整理好了。请注意,它对性能的考虑绝对为零(因为我基本上是不断地丢弃和重新创建所有行和列),但 UI 在运行时仍然保持完全响应。

首先是对数据绑定(bind)的一些基本支持:

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));
    }
}

Notice that this class requires nothing more than literally Ctrl+Enter since ReSharper puts that boilerplate in place automatically.

然后,使用您提供的相同模型类,我将这个 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>

Notice that, while this looks like a lot of XAML, I'm not using the built-in DataGrid or any other built-in control, but rather putting it together myself using nested ItemsControls.

最后是 Window 的代码,它简单地实例化了 VM 并设置了 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 显示,每行包含玩家名称的单个单元格,然后是数字单元格的水平 ItemsControl .请注意,与 winforms 的对应物相比,WPF ListBox 实际上很有用。
  • 请注意,我的解决方案支持像标准 DataGrid 一样的行选择,只是因为我不断地丢弃并重新创建整个数据集,所以选择不会始终保持不变。我可以在 VM 中添加一个 SelectedRow 属性来解决这个问题。
  • 请注意,我的示例完全没有任何优化,完全可以处理 100 毫秒的更新周期。如果数据更大,性能肯定会开始下降,并且需要更好的解决方案,例如实际删除需要删除的内容并添加需要添加的内容。请注意,即使使用更复杂的解决方案,我仍然不需要使用反射或任何其他技巧。
  • 另请注意,我的 ViewModel 代码要短得多(95 LOC 与你的 154 LOC)并且我没有采取删除所有空行的方法来使其看起来更短。

关于c# - WPF 中的跨表格数据绑定(bind),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32169375/

相关文章:

wpf - 在 winform 或 wpf 中拖放

c# - datagridview 中多列的搜索栏

c# - Windows 窗体线程到底发生了什么?

javascript - 将 JavaScript 转换为 C# - 构造函数和数组的困难

c# - 访问azure存储服务

c# - LINQ 中的 GroupBy 是如何工作的?

c# - 使用 WinForms 进行线程化?

c# - Entity Framework Code First 多对多创建重复行

wpf - 菜单打开时,XAML ResourceDictionary中的图像在工具栏上消失

c# - WPF 文本覆盖网格