具有动态行数和列数的 Grid - 第 2 部分





5.00/5 (4投票s)
WPF 中具有固定大小的单元格的数据网格,行数和列数动态更新。
引言
本文介绍了 WPF datagrid
,其单元格具有定义的固定大小,但行数和列数会动态更新以填满所有可用空间。例如,此类网格可用于无限 2D 游戏区域或细胞自动机的实现。在上一篇博文中,我们讨论了具有动态定义行数和列数但所有单元格大小相同的 WPF 数据网格。
特点
该应用程序展示了以下功能:
- 所有单元格具有固定的宽度和高度
- 单元格大小可在运行时更改
- 行数和列数由用户控件大小定义
- 网格尽可能多地占用空间
- 单击切换单元格状态
- 异步添加/删除单元格方法
- 调整大小计时器,可防止单元格更新过于频繁
- 保留单元格状态
- 使用依赖项容器
- 日志记录
背景
该解决方案使用 C# 6、.NET 4.6.1、WPF(MVVM 模式)、NuGet 包 Unity 和 Ikc5.TypeLibrary。
解决方案
WPF 应用程序
WPF 应用程序采用 MVVM 模式,只有一个主窗口。动态网格实现为用户控件,其中包含一个 DataGrid
控件,该控件绑定到单元格视图模型的集合的 ObservableCollection。如上所述,本文的代码基于上一篇博文中的代码,因此在这里,我们将重点介绍新的或已更改的代码。
动态数据网格的视图模型包含单元格、视图和网格大小、单元格集合的数据模型以及单元格视图模型的集合的集合。视图大小属性绑定到数据网格控件的实际大小。实际上,从 MVVM 模式的角度来看,这不是一个清晰的方法,因为视图模型不应该知道视图,但它通过绑定和附加属性以准确的方式实现。网格大小,即行数和列数,计算为视图大小除以单元格大小。由于行数和列数是整数,因此视图中单元格的实际大小可能不等于单元格宽度和高度的值。
更改控件大小并计算网格的行数和列数后,会重新创建单元格集合,但单元格状态会得以保留。然后,通过异步方法更新单元格视图模型集合。该方法分析必要的更改,并移除或添加行,以及从行中移除或添加单元格视图模型。异步方法可以保持应用程序的响应性,并且使用取消令牌可以在控件大小再次更改时取消更新。
动态网格控件
动态网格视图模型实现了 IDynamicGridViewModel
接口,该接口具有大小属性、单元格集的数据模型、单元格视图模型的集合的 ObservableCollection 以及多个颜色属性。
public interface IDynamicGridViewModel
{
/// <summary>
/// Width of current view - expected to be bound to view's actual
/// width in OneWay binding.
/// </summary>
int ViewWidth { get; set; }
/// <summary>
/// Height of current view - expected to be bound to view's actual
/// height in OneWay binding.
/// </summary>
int ViewHeight { get; set; }
/// <summary>
/// Width of the cell.
/// </summary>
int CellWidth { get; set; }
/// <summary>
/// Height of the cell.
/// </summary>
int CellHeight { get; set; }
/// <summary>
/// Count of grid columns.
/// </summary>
int GridWidth { get; }
/// <summary>
/// Count of grid rows.
/// </summary>
int GridHeight { get; }
/// <summary>
/// Data model.
/// </summary>
CellSet CellSet { get; }
/// <summary>
/// 2-dimensional collections for CellViewModels.
/// </summary>
ObservableCollection<ObservableCollection<ICellViewModel>>
Cells { get; }
/// <summary>
/// Start, the lightest, color of cells.
/// </summary>s
Color StartColor { get; set; }
/// <summary>
/// Finish, the darkest, color of cells.
/// </summary>
Color FinishColor { get; set; }
/// <summary>
/// Color of borders around cells.
/// </summary>
Color BorderColor { get; set; }
}
视图宽度和高度通过附加属性绑定到数据网格控件的实际大小(代码来自此 Stackoverflow 问题)。
attached:SizeObserver.Observe="True"
attached:SizeObserver.ObservedWidth="{Binding ViewWidth, Mode=OneWayToSource}"
attached:SizeObserver.ObservedHeight="{Binding ViewHeight, Mode=OneWayToSource}"
调整大小计时器
视图大小绑定存在一个问题——由于绑定是在单个线程中执行的,因此视图宽度和高度的新值在不同时刻到达。这意味着有必要等待另一个。此外,为了防止用户缓慢调整窗口大小时网格大小频繁更改,应用程序中使用了计时器。计时器在构造函数中创建,并在视图高度或视图宽度更改时启动或重启。
public DynamicGridViewModel(ILogger logger)
{
_resizeTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(100),
};
_resizeTimer.Tick += ResizeTimerTick;
// initialization
// ...
}
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (string.Equals(propertyName, nameof(ViewHeight),
StringComparison.InvariantCultureIgnoreCase) ||
string.Equals(propertyName, nameof(ViewWidth),
StringComparison.InvariantCultureIgnoreCase) ||
string.Equals(propertyName, nameof(CellHeight),
StringComparison.InvariantCultureIgnoreCase) ||
string.Equals(propertyName, nameof(CellWidth),
StringComparison.InvariantCultureIgnoreCase))
{
ImplementNewSize();
}
}
/// <summary>
/// Start timer when one of the view's dimensions is changed and wait for another.
/// </summary>
private void ImplementNewSize()
{
if (ViewHeight == 0 || ViewWidth == 0)
return;
if (_resizeTimer.IsEnabled)
_resizeTimer.Stop();
_resizeTimer.Start();
}
计时器触发时,方法会检查 width
和 height
是否都有效,并重新创建单元格集。然后执行 CreateOrUpdateCellViewModels
方法,该方法更新单元格视图模型集合的 ObservableCollection。
/// <summary>
/// Method change data model and grid size due to change of view size.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ResizeTimerTick(object sender, EventArgs e)
{
_resizeTimer.Stop();
if (ViewHeight == 0 || ViewWidth == 0)
return;
var newWidth = System.Math.Max(1,
(int)System.Math.Ceiling((double)ViewWidth / CellWidth));
var newHeight = System.Math.Max(1,
(int)System.Math.Ceiling((double)ViewHeight / CellHeight));
if (CellSet != null &&
GridWidth == newWidth &&
GridHeight == newHeight)
{
// the same size, nothing to do
return;
}
// preserve current points
var currentPoints = CellSet?.GetPoints().Where
(point => point.X < newWidth && point.Y < newHeight);
CellSet = new CellSet(newWidth, newHeight);
GridWidth = CellSet.Width;
GridHeight = CellSet.Height;
if (currentPoints != null)
CellSet.SetPoints(currentPoints);
CreateOrUpdateCellViewModels();
}
更新单元格视图模型集合
创建新的单元格集后,需要更新单元格视图模型集合。在上一篇博文中,每次都重新创建此集合,这导致应用程序挂起。此问题通过异步方法更新当前集合来解决。由于 WPF 架构,并且动态网格用户控件的项源绑定到 Cells
集合,因此对该集合的所有更改都通过 Dispatcher
完成。在应用程序中,使用优先级 DispatcherPriority.ApplicationIdle
,因为它在所有数据绑定之后执行,但也可以使用其他值。
起点是 CreateOrUpdateCellViewModels
方法,该方法首先创建 Cells
集合,创建取消令牌,然后为第一行启动异步递归方法 CreateCellViewModelsAsync
。
private async void CreateOrUpdateCellViewModels()
{
_logger.LogStart("Start");
// stop previous tasks that creates viewModels
if (_cancellationSource != null && _cancellationSource.Token.CanBeCanceled)
_cancellationSource.Cancel();
if (Cells == null)
Cells = new ObservableCollection<ObservableCollection<ICellViewModel>>();
try
{
_cancellationSource = new CancellationTokenSource();
await CreateCellViewModelsAsync(0, _cancellationSource.Token).ConfigureAwait(false);
}
catch (OperationCanceledException ex)
{
_logger.Exception(ex);
}
catch (AggregateException ex)
{
foreach (var innerException in ex.InnerExceptions)
{
_logger.Exception(innerException);
}
}
finally
{
_cancellationSource = null;
}
_logger.LogEnd("Completed - but add cells in asynchronous way");
}
由于单元格视图模型存储为集合的集合,每个内部集合对应于网格的一行。CreateCellViewModelsAsync
方法针对从 0
到 Math.Max(Cells.Count, GridHeight)
的每一行位置执行。可能出现以下情况:
rowNumber >= GridHeight
,这意味着Cell
集合包含的行数多于网格的当前大小。这些行应被移除。Application.Current.Dispatcher.Invoke( () => Cells.RemoveAt(positionToProcess), DispatcherPriority.ApplicationIdle, cancellationToken);
rowNumber < Cells.Count
,这意味着索引为该行的行存在于Cell
集合中,并且索引小于网格高度。在这种情况下,将调用UpdateCellViewModelRow
方法。Application.Current.Dispatcher.Invoke( () => UpdateCellViewModelRow(positionToProcess), DispatcherPriority.ApplicationIdle, cancellationToken);
请注意,行是ObservableCollection<icellviewmodel></icellviewmodel>
。根据此集合的长度与网格宽度之间的关系,将移除额外的单元格视图模型,将现有单元格模型更新为来自动态网格数据模型的新的ICell
实例,并添加缺失的单元格视图模型。/// <summary> /// Add or remove cell view models to the row. /// </summary> /// <param name="rowNumber">Number of row in data model.</param> private void UpdateCellViewModelRow(int rowNumber) { var row = Cells[rowNumber]; // delete extra cells while (row.Count > GridWidth) row.RemoveAt(GridWidth); for (var pos = 0; pos < GridWidth; pos++) { // create new ViewModel or update existent one var cell = CellSet.GetCell(pos, rowNumber); if (pos < row.Count) row[pos].Cell = cell; else { var cellViewModel = new CellViewModel(cell); row.Add(cellViewModel); } } }
- “
else
”情况,即rowNumber >= Cells.Count
且rowNumber < GridHeight
,这意味着Cell
集合不包含所需的行。此行由CreateCellViewModelRow
方法创建。/// <summary> /// Add new row of cell view models that corresponds to /// rowNumber row in data model. /// </summary> /// <param name="rowNumber">Number of row in data model.</param> private void CreateCellViewModelRow(int rowNumber) { _logger.Log($"Create {rowNumber} row of cells"); var row = new ObservableCollection<ICellViewModel>(); for (var x = 0; x < GridWidth; x++) { var cellViewModel = new CellViewModel(CellSet.GetCell(x, rowNumber)); row.Add(cellViewModel); } _logger.Log($"{rowNumber} row of cells is ready for rendering"); Cells.Add(row); }
依赖项容器
Unity 用作依赖项容器。在本文中,我们将 EmptyLogger
注册为记录器,并为 DynamicGridViewModel
实例创建单例。在 WPF 应用程序中,DI 容器的初始化在 App.xaml.cs 的 OnStartup
方法中完成。
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
IUnityContainer container = new UnityContainer();
container.RegisterType<ILogger, EmptyLogger>();
var dynamicGridViewModel = new DynamicGridViewModel(
container.Resolve<ILogger>())
{
// init properties
};
container.RegisterInstance(
typeof(IDynamicGridViewModel),
dynamicGridViewModel,
new ContainerControlledLifetimeManager());
var mainWindow = container.Resolve<MainWindow>();
Application.Current.MainWindow = mainWindow;
Application.Current.MainWindow.Show();
}
MainWindow
构造函数有一个由容器解析的参数。
public MainWindow(IDynamicGridViewModel dynamicGridViewModel)
{
InitializeComponent();
DataContext = dynamicGridViewModel;
}
同样,DynamicGridViewModel
构造函数的输入参数由容器解析。
public class DynamicGridViewModel : BaseNotifyPropertyChanged, IDynamicGridViewModel
{
private readonly ILogger _logger;
public DynamicGridViewModel(ILogger logger)
{
logger.ThrowIfNull(nameof(logger));
_logger = logger;
this.SetDefaultValues();
// initialization
// ...
_logger.Log("DynamicGridViewModel constructor is completed");
}
// other methods
// ...
}
历史
- 2017 年 1 月 4 日:初始博文