- c - 在位数组中找到第一个零
- linux - Unix 显示有关匹配两种模式之一的文件的信息
- 正则表达式替换多个文件
- linux - 隐藏来自 xtrace 的命令
这受到以下问题的启发 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 nestedItemsControl
s.
最后是 Window
的代码,它简单地实例化了 VM 并设置了 DataContext:
public partial class Window3 : Window
{
public Window3()
{
InitializeComponent();
var board = new ScoreBoard();
this.DataContext = new ViewModel(board);
}
}
结果:
ItemsControl
在顶部显示 Columns
集合(列名称)。ListBox
显示行
,每行包含玩家名称的单个单元格,然后是数字单元格的水平 ItemsControl
.请注意,与 winforms 的对应物相比,WPF ListBox
实际上很有用。DataGrid
一样的行选择,只是因为我不断地丢弃并重新创建整个数据集,所以选择不会始终保持不变。我可以在 VM 中添加一个 SelectedRow
属性来解决这个问题。关于c# - WPF 中的跨表格数据绑定(bind),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32169375/
我想做的是让 JTextPane 在 JPanel 中占用尽可能多的空间。对于我使用的 UpdateInfoPanel: public class UpdateInfoPanel extends JP
我在 JPanel 中有一个 JTextArea,我想将其与 JScrollPane 一起使用。我正在使用 GridBagLayout。当我运行它时,框架似乎为 JScrollPane 腾出了空间,但
我想在 xcode 中实现以下功能。 我有一个 View Controller 。在这个 UIViewController 中,我有一个 UITabBar。它们下面是一个 UIView。将 UITab
有谁知道Firebird 2.5有没有类似于SQL中“STUFF”函数的功能? 我有一个包含父用户记录的表,另一个表包含与父相关的子用户记录。我希望能够提取用户拥有的“ROLES”的逗号分隔字符串,而
我想使用 JSON 作为 mirth channel 的输入和输出,例如详细信息保存在数据库中或创建 HL7 消息。 简而言之,输入为 JSON 解析它并输出为任何格式。 最佳答案 var objec
通常我会使用 R 并执行 merge.by,但这个文件似乎太大了,部门中的任何一台计算机都无法处理它! (任何从事遗传学工作的人的附加信息)本质上,插补似乎删除了 snp ID 的 rs 数字,我只剩
我有一个以前可能被问过的问题,但我很难找到正确的描述。我希望有人能帮助我。 在下面的代码中,我设置了varprice,我想添加javascript变量accu_id以通过rails在我的数据库中查找记
我有一个简单的 SVG 文件,在 Firefox 中可以正常查看 - 它的一些包装文本使用 foreignObject 包含一些 HTML - 文本包装在 div 中:
所以我正在为学校编写一个 Ruby 程序,如果某个值是 1 或 3,则将 bool 值更改为 true,如果是 0 或 2,则更改为 false。由于我有 Java 背景,所以我认为这段代码应该有效:
我做了什么: 我在这些账户之间创建了 VPC 对等连接 互联网网关也连接到每个 VPC 还配置了路由表(以允许来自双方的流量) 情况1: 当这两个 VPC 在同一个账户中时,我成功测试了从另一个 La
我有一个名为 contacts 的表: user_id contact_id 10294 10295 10294 10293 10293 10294 102
我正在使用 Magento 中的新模板。为避免重复代码,我想为每个产品预览使用相同的子模板。 特别是我做了这样一个展示: $products = Mage::getModel('catalog/pro
“for”是否总是检查协议(protocol)中定义的每个函数中第一个参数的类型? 编辑(改写): 当协议(protocol)方法只有一个参数时,根据该单个参数的类型(直接或任意)找到实现。当协议(p
我想从我的 PHP 代码中调用 JavaScript 函数。我通过使用以下方法实现了这一点: echo ' drawChart($id); '; 这工作正常,但我想从我的 PHP 代码中获取数据,我使
这个问题已经有答案了: Event binding on dynamically created elements? (23 个回答) 已关闭 5 年前。 我有一个动态表单,我想在其中附加一些其他 h
我正在尝试找到一种解决方案,以在 componentDidMount 中的映射项上使用 setState。 我正在使用 GraphQL连同 Gatsby返回许多 data 项目,但要求在特定的 pat
我在 ScrollView 中有一个 View 。只要用户按住该 View ,我想每 80 毫秒调用一次方法。这是我已经实现的: final Runnable vibrate = new Runnab
我用 jni 开发了一个 android 应用程序。我在 GetStringUTFChars 的 dvmDecodeIndirectRef 中得到了一个 dvmabort。我只中止了一次。 为什么会这
当我到达我的 Activity 时,我调用 FragmentPagerAdapter 来处理我的不同选项卡。在我的一个选项卡中,我想显示一个 RecyclerView,但他从未出现过,有了断点,我看到
当我按下 Activity 中的按钮时,会弹出一个 DialogFragment。在对话框 fragment 中,有一个看起来像普通 ListView 的 RecyclerView。 我想要的行为是当
我是一名优秀的程序员,十分优秀!