跨表格数据绑定WPF [英] Cross tabular data binding in WPF

查看:233
本文介绍了跨表格数据绑定WPF的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

这是由以下问题<一个启发href=\"http://stackoverflow.com/questions/32072960/rendering-a-generated-table-with-tablelayoutpanel-taking-too-long-to-finish/32084782#32084782\">Rendering生成的表TableLayoutPanel中的时间太长完成。还有其他关于WPF表格数据SO职位,但我不认为他们盖这种情况下(虽然的如何显示与WPF真正的表格数据?是更接近)。这个问题是有趣的,因为这两个行和列是动态的,并且该视图应不仅最初显示的数据,而且还对添加/删除(行和列),并更新发生反应。我会present的WF方式(因为我有经验,有),并希望看到和比较的WPF的方式(S)。

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

 使用系统;
System.Collections中使用;
使用System.Collections.Generic;
使用的System.Threading;
命名空间模型
{
    抽象类实体
    {
        公共只读INT标识;
        保护实体(INT ID){ID = ID; }
    }
    一流的EntitySet&LT; T&GT; :IReadOnlyCollection&LT; T&GT;其中T:实体
    {
        字典&LT; INT,T&GT;项目=新词典与LT; INT,T&GT;();
        公众诠释计数{{返回items.Count; }}
        公众的IEnumerator&LT; T&GT;的GetEnumerator(){返回items.Values​​.GetEnumerator(); }
        的IEnumerator IEnumerable.GetEnumerator(){返回的GetEnumerator(); }
        公共无效添加(T项目){items.Add(item.Id,项目); }
        公共BOOL删除(INT ID){返回items.Remove(ID); }
    }
    一流的球员:实体
    {
        公共字符串名称;
        公共播放器(INT标识):碱(ID){}
    }
    类游戏:实体
    {
        公共字符串名称;
        公共游戏(INT ID):基地(ID){}
    }
    记分牌类
    {
        EntitySet的&LT;玩家GT;玩家=新的EntitySet&LT;播放器及GT;();
        EntitySet的&LT;游戏及GT;游戏=新的EntitySet&LT;游戏及GT;();
        字典&LT; INT,字典&LT; INT,INT&GT;&GT; gameScores =新词典&LT; INT,字典&LT; INT,INT&GT;&GT;();
        公共记分牌(){负荷(); }
        公共IReadOnlyCollection&LT;玩家GT;播放器{{返回的球员; }}
        公共IReadOnlyCollection&LT;游戏及GT;游戏{{返回游戏; }}
        公众诠释GetScore(Player播放器,游戏机游戏)
        {
            字典&LT; INT,INT&GT; playerScores;
            INT分;
            返回gameScores.TryGetValue(game.Id,出playerScores)及和放大器; playerScores.TryGetValue(player.Id,出成绩)?评分:0;
        }
        公共事件的EventHandler&LT; ScoreBoardChangeEventArgs&GT;改变;
        #区域测试
        私人无效负载()
        {
            的for(int i = 0; I&LT; 20;我++)AddNewPlayer();
            对(INT I = 0; I&小于10;我++)AddNewGame();
            的foreach(在游戏中VAR游戏)
                的foreach(在玩家VAR播放器)
                    如果(RandomBool())SetScore(播放器,游戏,random.Next(1000));
        }
        公共无效StartUpdate()
        {
            VAR syncContext = SynchronizationContext.Current;
            VAR updateThread =新的Thread(()=&GT;
            {
                而(真){Thread.sleep代码(100);更新(syncContext); }
            });
            updateThread.IsBackground = TRUE;
            updateThread.Start();
        }
        私人无效更新(的SynchronizationContext syncContext)
        {
            VAR addedPlayers =新的List&LT;播放器及GT;();
            VAR removedPlayers =新的List&LT;播放器及GT;();
            VAR addedGames =新的List&LT;游戏及GT;();
            VAR removedGames =新的List&LT;游戏及GT;();
            VAR changedScores =新的List&LT; ScoreKey&GT;();
            //移除
            如果(RandomBool())
                的foreach(在玩家VAR播放器)
                    如果(RandomBool()){removedPlayers.Add(播放器);如果(removedPlayers.Count == 10)破; }
            如果(RandomBool())
                的foreach(在游戏中VAR游戏)
                    如果(RandomBool()){removedGames.Add(游戏);如果(removedGames.Count == 5)打破; }
            的foreach(在removedGames VAR游戏)
                games.Remove(game.Id);
            的foreach(在removedPlayers VAR播放器)
            {
                players.Remove(player.Id);
                的foreach(在gameScores VAR项)
                    item.Value.Remove(player.Id);
            }
            // 更新
            的foreach(在游戏中VAR游戏)
            {
                的foreach(在玩家VAR播放器)
                {
                    如果继续(RandomBool()!);
                    INT oldScore = GetScore(播放器,游戏);
                    INT newScore = Math.Min(oldScore + random.Next(100),1000000);
                    如果(oldScore == newScore)继续;
                    SetScore(播放器,游戏,newScore);
                    changedScores.Add(新ScoreKey {球员=播放器,游戏=游戏});
                }
            }
            //附加
            如果(RandomBool())
                对于(INT I = 0,计数= random.Next(10); I&LT;计数;我++)
                    addedPlayers.Add(AddNewPlayer());
            如果(RandomBool())
                对于(INT I = 0,计数= random.Next(5); I&LT;计数;我++)
                    addedGames.Add(AddNewGame());
            的foreach(在addedGames VAR游戏)
                的foreach(在addedPlayers VAR播放器)
                    SetScore(播放器,游戏,random.Next(1000));
            //通知
            VAR处理器=更改;
            如果(处理器= NULL&放大器;!及(长)addedGames.Count + removedGames.Count + addedPlayers.Count + removedPlayers.Count + changedScores.Count大于0)
            {
                变种E =新ScoreBoardChangeEventArgs {AddedPlayers = addedPlayers,RemovedPlayers = removedPlayers,AddedGames = addedGames,RemovedGames = removedGames,ChangedScores = changedScores};
                syncContext.Send(_ =&GT;处理程序(这一点,E),NULL);
            }
        }
        随机随机=新的随机();
        INT playerId,游戏ID;
        布尔RandomBool(){回报(random.Next()%5)== 0; }
        球员AddNewPlayer()
        {
            INT ID = ++ playerId;
            VAR项目=新的玩家(ID){名称=P+ ID};
            players.Add(项目);
            归还物品;
        }
        游戏AddNewGame()
        {
            INT ID = ++游戏ID;
            VAR项目=新游戏(ID){名称=G+ ID};
            games.Add(项目);
            归还物品;
        }
        无效SetScore(Player播放器,游戏的游戏,INT分)
        {
            字典&LT; INT,INT&GT; playerScores;
            如果(!gameScores.TryGetValue(game.Id,出playerScores))
                gameScores.Add(game.Id,playerScores =新词典&LT; INT,INT&GT;());
            playerScores [player.Id] =得分;
        }
        #endregion
    }
    结构ScoreKey
    {
        公共Player播放器;
        公共游戏游戏;
    }
    类ScoreBoardChangeEventArgs
    {
        公共IReadOnlyList&LT;玩家GT; AddedPlayers,RemovedPlayers;
        公共IReadOnlyList&LT;游戏及GT; AddedGames,RemovedGames;
        公共IReadOnlyList&LT; ScoreKey&GT; ChangedScores;
        众长计数{{返回(长)AddedPlayers.Count + RemovedPlayers.Count + AddedGames.Count + RemovedGames.Count + ChangedScores.Count; }}
    }
}

在感兴趣的类是 StoreBoard 。基本上它有球员和比赛的名单,由(播放器,游戏)GetScore功能,多用途批量更改通知。我希望它成为以表格格式psented与行是玩家,列$ P $ - 游戏,它们的交集 - 分数。此外所有的更新应该以结构化的方式来完成(使用某种形式的数据绑定)。

WF具体的解决方案

视图模式的IList 将处理该行的一部分, ITypedList 自定义的PropertyDescriptor 秒 - 柱部分, IBindingList.ListChanged 事件 - 所有的修改。

 使用系统;
使用System.Collections.Generic;
使用System.Collections.Specialized;
使用System.ComponentModel;
使用System.Linq的;
命名空间WfViewModels
{
    使用模型;    类ScoreBoardItemViewModel:CustomTypeDescriptor
    {
        ScoreBoardViewModel容器;
        保护记分牌源{{返回container.source; }}
        Player播放器;
        字典&LT; INT,INT&GT; playerScores;
        公共ScoreBoardItemViewModel(ScoreBoardViewModel容器Player播放器)
        {
            this.container =容器;
            this.player =玩家;
            playerScores =新词典&LT; INT,INT&GT;(source.Games.Count);
            的foreach(在source.Games VAR游戏)AddScore(游戏);
        }
        公共Player播放器{{返回的球员; }}
        公众诠释GetScore(Game游戏){int值;返回playerScores.TryGetValue(game.Id,出来的值)?值:0; }
        内部空隙AddScore(游戏游戏){playerScores.Add(game.Id,source.GetScore(播放器,游戏)); }
        内部布尔RemoveScore(游戏游戏){返回playerScores.Remove(game.Id); }
        内部布尔UpdateScore(游戏游戏)
        {
            INT oldScore = GetScore(游戏),newScore = source.GetScore(播放器,游戏);
            如果(oldScore == newScore)返回false;
            playerScores [game.Id] = newScore;
            返回true;
        }
        公众覆盖PropertyDescriptorCollection的GetProperties()
        {
            返回container.properties;
        }
    }
    类ScoreBoardViewModel:&的BindingList LT; ScoreBoardItemViewModel&gt;中ITypedList
    {
        内部记分牌来源;
        内部PropertyDescriptorCollection性能;
        公共ScoreBoardViewModel(记分牌源)
        {
            this.source =来源;
            性能=新PropertyDescriptorCollection(
                新的[] {CreateProperty(PlayerName项目= GT; item.Player.Name,玩家)}
                .Concat(source.Games.Select(CreateScoreProperty))
                .ToArray()
            );
            source.Changed + = OnSourceChanged;
        }
        公共无效负载()
        {
            Items.Clear();
            的foreach(在source.Players VAR播放器)
                Items.Add(新ScoreBoardItemViewModel(这一点,玩家));
            ResetBindings();
        }
        无效OnSourceChanged(对象发件人,ScoreBoardChangeEventArgs E)
        {
            变种数= e.Count;
            如果(计数== 0)回报;
            RaiseListChangedEvents = COUNT&LT; 2;
            的foreach(在e.RemovedPlayers VAR播放器)OnRemoved(播放器);
            的foreach(在e.RemovedGames VAR游戏)OnRemoved(游戏);
            的foreach(在e.AddedGames VAR游戏)onAdded回调函数里(游戏);
            的foreach(在e.AddedPlayers VAR播放器)onAdded回调函数里(播放器);
            的foreach(在e.ChangedScores.GroupBy(项目=&GT VAR组; item.Player))
            {
                INT指数=的IndexOf(group.Key);
                如果(指数℃下)继续;
                布尔改变= FALSE;
                改变的foreach(组VAR项)| =项目[指数] .UpdateScore(item.Game);
                如果(改)ResetItem(指数);
            }
            如果(RaiseListChangedEvents)回报;
            RaiseListChangedEvents = TRUE;
            如果(e.AddedGames.Count + e.RemovedGames.Count大于0)
                OnListChanged(新ListChangedEventArgs(ListChangedType.PropertyDescriptorChanged,NULL));
            如果((长)e.AddedPlayers.Count + e.RemovedPlayers.Count + e.ChangedScores.Count大于0)
                ResetBindings();
        }
        无效onAdded回调函数里(Player播放器)
        {
            如果(的IndexOf(播放器)GT; = 0)回报;
            加入(新ScoreBoardItemViewModel(这一点,玩家));
        }
        无效OnRemoved(Player播放器)
        {
            INT指数=的IndexOf(播放器);
            如果(指数℃下)返回;
            RemoveAt移除(索引);
        }
        无效onAdded回调函数里(游戏游戏)
        {
            如果(的IndexOf(游戏)&GT; = 0)回报;
            VAR财产= CreateScoreProperty(游戏);
            properties.Add(财产);
            的foreach(在项目VAR项)
                item.AddScore(游戏);
            如果(RaiseListChangedEvents)
                OnListChanged(新ListChangedEventArgs(ListChangedType.PropertyDescriptorAdded,财产));
        }
        无效OnRemoved(游戏游戏)
        {
            INT指数=的IndexOf(游戏);
            如果(指数℃下)返回;
            VAR属性=属性[指数]
            properties.RemoveAt(索引);
            的foreach(在项目VAR项)
                item.RemoveScore(游戏);
            如果(RaiseListChangedEvents)
                OnListChanged(新ListChangedEventArgs(ListChangedType.PropertyDescriptorDeleted,财产));
        }
        INT的IndexOf(Player播放器)
        {
            的for(int i = 0; I&LT;计数;我++)
                如果(本[I] .Player ==播放器)返回我;
            返回-1;
        }
        INT的IndexOf(游戏游戏)
        {
            VAR propertyName的= ScorePropertyName(游戏);
            的for(int i = properties.Count - 1; I&GT; = 0;我 - )
                如果(属性[I] .Name点== propertyName的)回报我;
            返回-1;
        }
        字符串ITypedList.GetListName(PropertyDescriptor的[] listAccessors){返回NULL; }
        PropertyDescriptorCollection ITypedList.GetItemProperties(PropertyDescriptor的[] listAccessors){返回属性; }
        静态字符串ScorePropertyName(游戏游戏){返回Game_+ game.Id; }
        静态的PropertyDescriptor CreateScoreProperty(游戏游戏){返回CreateProperty(ScorePropertyName(游戏),项目= GT; item.GetScore(游戏),game.Name); }
        静态的PropertyDescriptor CreateProperty&LT; T&GT;(字符串名称,函数功能与LT; ScoreBoardItemViewModel,T&GT;的getValue,字符串显示名= NULL)
        {
            返回新ScorePropertyDescriptor&LT; T&GT;(姓名,的getValue,显示名);
        }
        类ScorePropertyDescriptor&LT; T&GT; :的PropertyDescriptor
        {
            字符串显示名;
            FUNC&LT; ScoreBoardItemViewModel,T&GT;的getValue;
            公共ScorePropertyDescriptor(字符串名称,函数功能与LT; ScoreBoardItemViewModel,T&GT;的getValue,字符串显示名= NULL):基地(姓名,NULL)
            {
                this.getValue =的getValue;
                this.displayName =显示名?名称;
            }
            公众覆盖字符串显示名称{{返回显示名; }}
            公众覆盖类型COMPONENTTYPE {{返回的typeof(ScoreBoardItemViewModel); }}
            公众覆盖BOOL IsReadOnly {获得{返回true; }}
            公众覆盖类型属性类型{{返回的typeof(T); }}
            公众覆盖布尔CanResetValue(对象组件){返回false; }
            公众覆盖对象的GetValue(对象组件){返回的getValue((ScoreBoardItemViewModel)组件); }
            公共覆盖无效ResetValue(对象组件){抛出新NotSupportedException异常(); }
            公共覆盖无效的SetValue(对象组件,对象的值){抛出新NotSupportedException异常(); }
            公众覆盖布尔ShouldSerializeValue(对象组件){返回false; }
        }
    }
}

边注:在code以上可以看出,WF数据绑定的一个缺陷 - 我们坚持单张项目列表更改通知,如果有一个很大的变化,以适用的是无效的,或蛮力复位不能被任何列表数据presenter有效地处理的通知。

视图的:

 使用系统;
使用System.Drawing中;
使用System.Windows.Forms的;
命名空间访问量
{
    使用模型;
    使用的ViewModels;
    类ScoreBoardView:表单
    {
        [STAThread]
        静态无效的主要()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(假);
            Application.Run(新ScoreBoardView {=的WindowState FormWindowState.Maximized});
        }
        保护覆盖无效的OnLoad(EventArgs的发送)
        {
            base.OnLoad(E);
            无功源=新的记分牌();
            视图模型=新ScoreBoardViewModel(源);
            InitView();
            viewModel.Load();
            source.StartUpdate();
        }
        ScoreBoardViewModel视图模型;
        DataGridView的观点;
        无效InitView()
        {
            鉴于=新的DataGridView {码头= DockStyle.Fill,父=这};
            view.Font =新的字体(Microsoft无衬线,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风格= view.DefaultCellStyle;
            style.SelectionForeColor = style.SelectionBackColor = Color.Empty;
            风格= view.ColumnHeadersDefaultCellStyle;
            style.SelectionForeColor = style.SelectionBackColor = Color.Empty;
            style.BackColor = Color.Navy;
            style.ForeColor = Color.White;
            风格= view.RowHeadersDefaultCellStyle;
            style.SelectionForeColor = style.SelectionBackColor = Color.Empty;
            风格= view.RowsDefaultCellStyle;
            style.SelectionForeColor = style.ForeColor = Color.Black;
            style.SelectionBackColor = style.BackColor = Color.AliceBlue;
            风格= view.AlternatingRowsDefaultCellStyle;
            style.SelectionForeColor = style.ForeColor = Color.Black;
            style.SelectionBackColor = style.BackColor = Color.LightSteelBlue;
            view.ColumnAdded + = OnViewColumnAdded;
            view.DataSource =视图模型;
            view.AutoResizeColumnHeadersHeight();
            view.RowTemplate.MinimumHeight = view.ColumnHeadersHeight;
        }
        私人无效OnViewColumnAdded(对象发件人,DataGridViewColumnEventArgs E)
        {
            无功柱= e.Column;
            如果(column.ValueType == typeof运算(INT))
            {
                VAR风格= column.DefaultCellStyle;
                style.Alignment = DataGridViewContentAlignment.MiddleRight;
                style.Format =NO;
            }
        }
    }
}

就是这样。

展望为WPF方式。而且请注意,这个问题的不是作为哪个更好WF和WPF之间的比较 - 我在这个问题的解决方案,WPF(S)很感兴趣。

编辑:其实,我错了。我的视图模型是不是WF具体。我曾与化妆品的变化更新,它(使用 ICustomTypeDescriptor ),现在它在这两个WF和WPF是可用的。


解决方案

所以,你的解决方案是非常令人费解,度假村到黑客,如使用反射,它并没有真正让我感到吃惊,因为的WinForms是一个非常过时的技术,需要这样的黑客做的一切。

WPF是一个现代化的UI框架,并且不需要任何的。

这是一个很幼稚的解决方案,我放在一起在15分钟内。请注意,它有绝对的零性能方面的考虑(因为我基本上扔掉并重新创建所有的行和列不断),但用户界面仍然在运行完全响应。

所有的数据绑定一些基本的支持第一:

 公共抽象类PropertyChangedBase:INotifyPropertyChanged的
{
    公共事件PropertyChangedEventHandler的PropertyChanged;    受保护的虚拟无效OnPropertyChanged([CallerMemberName]字符串参数propertyName = NULL)
    {
        VAR处理器=的PropertyChanged;
        如果(!=处理空值)处理器(这一点,新PropertyChangedEventArgs(propertyName的));
    }
}


  

注意,这个类需要什么比字面上Ctrl + Enter因为ReSharper的提出了样板到位自动。

更多

然后,用你提供的同一型号课,我放在一起这视图模型:

 公共类视图模型:PropertyChangedBase
{
    私人只读记分牌板;    公众的ObservableCollection&LT;串GT;列{搞定;私人集; }    公众的ObservableCollection&LT;游戏及GT;游戏{搞定;私人集; }    公众的ObservableCollection&LT; RowViewModel&GT;行{搞定;私人集; }    公共视图模型(记分板)
    {
        this.board =板;
        this.board.Changed + = OnBoardChanged;        UpdateColumns(this.board.Games.Select(X =&GT; x.Name));
        UpdateRows(this.board.Players,this.board.Games);        this.board.StartUpdate();
    }    私人无效OnBoardChanged(对象发件人,ScoreBoardChangeEventArgs E)
    {
        VAR游戏=
            this.board.Games
                      。除(e.RemovedGames)
                      .Concat(e.AddedGames)
                      .ToList();        this.UpdateColumns(games.Select(X =&GT; x.Name));        VAR玩家=
            this.board.Players
                      。除(e.RemovedPlayers)
                      .Concat(e.AddedPlayers)
                      .ToList();        this.UpdateRows(播放器,游戏);
    }    私人无效UpdateColumns(IEnumerable的&LT;串GT;列)
    {
        this.Columns =新的ObservableCollection&LT;串GT;(列);
        this.Columns.Insert(0,播放器);        this.OnPropertyChanged(列);
    }    私人无效UpdateRows(IEnumerable的&LT;玩家GT;的球员,IEnumerable的&LT;游戏及GT;游戏)
    {
        VAR行=
            从运动员P
            让分数=
                从游戏摹
                选择this.board.GetScore(P,G)
            让行=
                新RowViewModel
                {
                    玩家= p.Name,
                    分数=新的ObservableCollection&LT; INT&GT;(分)
                }
            选择行;        this.Rows =新的ObservableCollection&LT; RowViewModel&GT;(行);
        this.OnPropertyChanged(行);
    }
}公共类RowViewModel
{
    公共字符串球员得到{;组; }    公众的ObservableCollection&LT; INT&GT;分数{搞定;组; }
}

然后一些XAML:

 &LT;窗​​口x:类=WpfApplication31.Window3
        的xmlns =htt​​p://schemas.microsoft.com/winfx/2006/xaml/$p$psentation
        的xmlns:X =htt​​p://schemas.microsoft.com/winfx/2006/xaml
        标题=Window3HEIGHT =300WIDTH =300&GT;
    &LT; Window.Resources&GT;
        &LT;风格的TargetType =ItemsControl的X:键=水平&GT;
            &LT; setter属性=ItemsPanel&GT;
                &LT; Setter.Value&GT;
                    &LT; ItemsPanelTemplate&GT;
                        &LT; StackPanel的方向=横向/&GT;
                    &LT; / ItemsPanelTemplate&GT;
                &LT; /Setter.Value>
            &LT; /二传手&GT;
        &LT; /样式和GT;        &LT;风格的TargetType =ListBoxItem的&GT;
            &LT; setter属性=填充VALUE =0/&GT;
        &LT; /样式和GT;        &LT;的DataTemplate X:键=CellTemplate&GT;
            &LT; BORDER = BorderBrush黑了borderThickness =1填充=5WIDTH =60&GT;
                &LT; TextBlock的文本={结合}
                           VerticalAlignment =中心
                           的Horizo​​ntalAlignment =中心/&GT;
            &LT; /边框&GT;
        &LT; / DataTemplate中&GT;
    &LT; /Window.Resources>    &LT;&DockPanel中GT;
        &LT;的ItemsControl的ItemsSource ={绑定列}
                      风格={StaticResource的水平}
                      保证金=3,0,0,0
                      的ItemTemplate ={StaticResource的CellTemplate}
                      DockPanel.Dock =顶部/&GT;        &LT; ListBox中的ItemsSource ={结合行}&GT;
            &LT; ListBox.ItemTemplate&GT;
                &LT;&DataTemplate的GT;
                    &LT; StackPanel的方向=横向&GT;
                        &lt;内容presenter CONTENT ={结合玩家}
                                          的ContentTemplate ={StaticResource的CellTemplate}/&GT;                        &LT;的ItemsControl的ItemsSource ={结合分数}
                                  风格={StaticResource的水平}
                                  的ItemTemplate ={StaticResource的CellTemplate}/&GT;
                    &LT; / StackPanel的&GT;
                &LT; / DataTemplate中&GT;
            &LT; /ListBox.ItemTemplate>
        &LT; /列表框&GT;
    &LT; / DockPanel中&GT;
&LT; /窗GT;


  

注意,虽然这貌似很多XAML的,我不使用内置的的DataGrid 或任何其他内置的控制,而是把它自己一起使用嵌套的ItemsControl 秒。


最后,窗口的code的后面,它只是实例化VM,并设置的DataContext:

 公共部分类Window3:窗口
{
    公共Window3()
    {
        的InitializeComponent();        VAR板=新的记分牌();
        this.DataContext =新的视图模型(板);
    }
}

结果:

在这里输入的形象描述


  • 第一个的ItemsControl 显示集合在顶部(列名)。

  • 的ListBox 显示,每行包含玩家姓名的单个单元格,然后一个水平的ItemsControl 的数字单元。请注意,在对比的是的WinForms'对应的WPF 的ListBox 实际上是有用的。

  • 请注意,我的解决方案支持像一个标准的行选择的DataGrid 会,只是因为我扔掉,不断重新创建整个数据集,选择不会保持整个。我可以在虚拟机中添加 SelectedRow 属性来解决这个问题。

  • 请注意,我没有完全优化,例如幼稚任何比能够处理你的100毫秒更新周期的多。如果数据是较大的性能必然会开始降解,和更好的解决方案将是需要的,如实际删除需要删除什么和添加需要添加什么。请注意,即使有更复杂的解决方案,我仍然不会需要使用反射或任何其他黑客。

  • 另请注意,我的视图模型code是短得多(95 LOC对你的154),我没有诉诸删除所有空行,使它看起来更短。

This is inspired from the following issue Rendering a generated table with TableLayoutPanel taking too long to finish. There are other SO posts regarding WPF tabular data, but I don't think they cover this case (although How to display real tabular data with WPF? is closer). The issue is interesting because both rows and columns are dynamic, and the view should not only display the data initially, but also react on add/remove (both rows and columns) and updates. I'll present the WF way (because I have experience there) and would like to see and compare it to the WPF way(s).

First, here is the sample model to 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; } }
    }
}  

The class in interest is StoreBoard. Basically it has Players and Games lists, GetScore function by (player, game), and multipurpose batch change notification. I want it to be presented in a tabular format with rows being players, columns - games, and their intersection - scores. Also all the updating should be done in a structured way (using some sort of data binding).

WF specific solution:

the view model: IList will handle the row part, ITypedList with custom PropertyDescriptors - column part, and IBindingList.ListChanged event - all modifications.

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

Side note: in the code above can be seen one of the WF databinding flaws - we are stuck with a singe item list change notifications, which is ineffective if there are a lot of changes to apply, or brute force Reset notification which cannot be handled effectively by any list data presenter.

the 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";
            }
        }
    }
}  

And that's it.

Looking forward for the WPF way. And please note that this question is not for "which is better" comparison between WF and WPF - I'm really interested in WPF solution(s) of the problem.

EDIT: In fact, I was wrong. My "view model" is not WF specific. I've updated it with a cosmetic change (using ICustomTypeDescriptor) and now it's usable in both WF and WPF.

解决方案

So, your solution is extremely convoluted and resorts to hacks such as using reflection, which doesn't really surprise me since winforms is a very outdated technology and requires such hacks for everything.

WPF is a modern UI framework and does not need any of that.

This is a very naive solution that I put together in 15 minutes. Notice that it has absolutely zero performance considerations (since I'm basically throwing away and recreating all the rows and columns constantly) and yet the UI remains totally responsive while running.

First of all some base support for 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));
    }
}

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

Then, using the same Model classes you provided, I put together this 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; }
}

Then some 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.

Finally, the Window's code behind, which simply instantiates the VM and sets the DataContext:

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

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

Result:

  • The first ItemsControl shows the Columns collection (the column names) on top.
  • The ListBox shows the Rows, each row containing a single cell for the player name, and then an horizontal ItemsControl for the numeric cells. Notice that in contrast to the winforms' counterpart, the WPF ListBox is actually useful.
  • Notice that my solution supports row selection like a standard DataGrid would, except that because I'm throwing away and recreating the entire dataset constantly, the selection is not maintained throughout. I could add a SelectedRow property in the VM to fix this.
  • Notice that my totally naive example with no optimizations whatsoever is more than capable of dealing with your 100 ms update cycle. If data was larger performance would surely start to degrade, and a better solution would be required, such as actually deleting what needs to be deleted and adding what needs to be added. Notice that even with a more complex solution I still wouldn't need to use reflection or any other hacks.
  • Also notice that my ViewModel code is much shorter (95 LOC versus 154 of yours) and I did not resort to deleting all blank lines to make it look shorter.

这篇关于跨表格数据绑定WPF的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆