65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2017 年 1 月 4 日

CPOL

5分钟阅读

viewsIcon

12208

downloadIcon

898

WPF 中具有固定大小的单元格的数据网格,行数和列数动态更新。

Grid 12×10 with cell 25×25

引言

本文介绍了 WPF datagrid,其单元格具有定义的固定大小,但行数和列数会动态更新以填满所有可用空间。例如,此类网格可用于无限 2D 游戏区域或细胞自动机的实现。在上一篇博文中,我们讨论了具有动态定义行数和列数但所有单元格大小相同的 WPF 数据网格。

特点

该应用程序展示了以下功能:

  1. 所有单元格具有固定的宽度和高度
  2. 单元格大小可在运行时更改
  3. 行数和列数由用户控件大小定义
  4. 网格尽可能多地占用空间
  5. 单击切换单元格状态
  6. 异步添加/删除单元格方法
  7. 调整大小计时器,可防止单元格更新过于频繁
  8. 保留单元格状态
  9. 使用依赖项容器
  10. 日志记录

背景

该解决方案使用 C# 6、.NET 4.6.1、WPF(MVVM 模式)、NuGet 包 UnityIkc5.TypeLibrary

解决方案

WPF 应用程序

Grid 25×16 with cell 25×25 and values

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

计时器触发时,方法会检查 widthheight 是否都有效,并重新创建单元格集。然后执行 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();
}

更新单元格视图模型集合

Grid 22×13 with cell 25×25 until resizing

创建新的单元格集后,需要更新单元格视图模型集合。在上一篇博文中,每次都重新创建此集合,这导致应用程序挂起。此问题通过异步方法更新当前集合来解决。由于 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 方法针对从 0Math.Max(Cells.Count, GridHeight) 的每一行位置执行。可能出现以下情况:

  1. rowNumber >= GridHeight,这意味着 Cell 集合包含的行数多于网格的当前大小。这些行应被移除。
    Application.Current.Dispatcher.Invoke(
      () => Cells.RemoveAt(positionToProcess),
      DispatcherPriority.ApplicationIdle,
      cancellationToken);	
  2. 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);
        }
      }
    }	
  3. else”情况,即 rowNumber >= Cells.CountrowNumber < 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.csOnStartup 方法中完成。

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 日:初始博文
© . All rights reserved.