WPF中的跨表格数据绑定

这是受以下问题启发的。 使用TableLayoutPanel渲染生成的表需要很长时间才能完成 。 关于WPF表格数据还有其他SOpost,但我不认为它们涵盖了这种情况(尽管如何用WPF显示真正的表格数据?更接近)。 问题很有趣,因为行和列都是动态的,视图不应该最初显示数据,还应该对添加/删除(行和列)和更新做出反应。 我将呈现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 : IReadOnlyCollection where T : Entity { Dictionary items = new Dictionary(); public int Count { get { return items.Count; } } public IEnumerator 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 players = new EntitySet(); EntitySet games = new EntitySet(); Dictionary<int, Dictionary> gameScores = new Dictionary<int, Dictionary>(); public ScoreBoard() { Load(); } public IReadOnlyCollection Players { get { return players; } } public IReadOnlyCollection Games { get { return games; } } public int GetScore(Player player, Game game) { Dictionary playerScores; int score; return gameScores.TryGetValue(game.Id, out playerScores) && playerScores.TryGetValue(player.Id, out score) ? score : 0; } public event EventHandler Changed; #region Test private void Load() { for (int i = 0; i < 20; i++) AddNewPlayer(); for (int i = 0; i  { while (true) { Thread.Sleep(100); Update(syncContext); } }); updateThread.IsBackground = true; updateThread.Start(); } private void Update(SynchronizationContext syncContext) { var addedPlayers = new List(); var removedPlayers = new List(); var addedGames = new List(); var removedGames = new List(); var changedScores = new List(); // 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  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 playerScores; if (!gameScores.TryGetValue(game.Id, out playerScores)) gameScores.Add(game.Id, playerScores = new Dictionary()); playerScores[player.Id] = score; } #endregion } struct ScoreKey { public Player Player; public Game Game; } class ScoreBoardChangeEventArgs { public IReadOnlyList AddedPlayers, RemovedPlayers; public IReadOnlyList AddedGames, RemovedGames; public IReadOnlyList ChangedScores; public long Count { get { return (long)AddedPlayers.Count + RemovedPlayers.Count + AddedGames.Count + RemovedGames.Count + ChangedScores.Count; } } } } 

感兴趣的课程是StoreBoard 。 基本上它有玩家和游戏列表,GetScorefunction(玩家,游戏)和多用途批量更改通知。 我希望它以表格forms呈现,其中行是玩家,列 – 游戏,以及他们的交叉点 – 得分。 此外,所有更新都应以结构化方式完成(使用某种数据绑定)。

WF具体解决方案

视图模型IList将处理行部分, ITypedList具有自定义PropertyDescriptor s – 列部分,以及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 playerScores; public ScoreBoardItemViewModel(ScoreBoardViewModel container, Player player) { this.container = container; this.player = player; playerScores = new Dictionary(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, 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  item.Player)) { int index = IndexOf(group.Key); if (index  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; 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 = 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(string name, Func getValue, string displayName = null) { return new ScorePropertyDescriptor(name, getValue, displayName); } class ScorePropertyDescriptor : PropertyDescriptor { string displayName; Func getValue; public ScorePropertyDescriptor(string name, Func 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解决方案的问题感兴趣。

编辑:事实上,我错了。 我的“视图模型”不是特定于WF的。 我用化妆品更改(使用ICustomTypeDescriptor )更新了它,现在它可以在WF和WPF中使用。

所以,你的解决方案是非常复杂的,并采用reflection这样的黑客攻击,这并不会让我感到惊讶,因为winforms是一种非常过时的技术,需要对所有东西进行攻击。

WPF是一个现代的UI框架,不需要任何这样的框架。

这是一个非常天真的解决方案,我在15分钟内放在一起。 请注意,它绝对没有性能考虑因素(因为我基本上是在丢弃并不断重新创建所有行和列),但UI在运行时仍保持完全响应。

首先是对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会自动将该样板放置到位。

然后,使用您提供的相同Model类,我将这个ViewModel放在一起:

 public class ViewModel : PropertyChangedBase { private readonly ScoreBoard board; public ObservableCollection Columns { get; private set; } public ObservableCollection Games { get; private set; } public ObservableCollection 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 columns) { this.Columns = new ObservableCollection(columns); this.Columns.Insert(0, "Player"); this.OnPropertyChanged("Columns"); } private void UpdateRows(IEnumerable players, IEnumerable 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(scores) } select row; this.Rows = new ObservableCollection(rows); this.OnPropertyChanged("Rows"); } } public class RowViewModel { public string Player { get; set; } public ObservableCollection Scores { get; set; } } 

然后一些XAML:

                         

请注意,虽然这看起来像很多XAML,但我没有使用内置的DataGrid或任何其他内置控件,而是使用嵌套的ItemsControl自己将它放在一起。

最后, Window的代码背后,它只是实例化VM并设置DataContext:

 public partial class Window3 : Window { public Window3() { InitializeComponent(); var board = new ScoreBoard(); this.DataContext = new ViewModel(board); } } 

结果:

在此处输入图像描述

  • 第一个ItemsControl在顶部显示Columns集合(列名称)。
  • ListBox显示Rows ,每行包含播放器名称的单个单元格,然后是数字单元格的水平ItemsControl 。 请注意,与winforms的对应物相比,WPF ListBox实际上很有用。
  • 请注意,我的解决方案支持行选择,就像标准的DataGrid一样,除了因为我丢弃并不断重新创建整个数据集,所以不会始终保持选择。 我可以在VM中添加SelectedRow属性来解决这个问题。
  • 请注意,我没有任何优化的完全天真的例子能够处理你的100毫秒更新周期。 如果数据更大,性能肯定会开始降低,并且需要更好的解决方案,例如实际删除需要删除的内容并添加需要添加的内容。 请注意,即使使用更复杂的解决方案,我仍然不需要使用reflection或任何其他黑客。
  • 还要注意我的ViewModel代码要短得多(95左右对比你的154个)而且我没有采用删除所有空行来使它看起来更短。