具有动态行数和列数的网格,第2部分

源代码

介绍 , 这篇文章专门介绍了Wpf datagrid,其中的单元格已经定义了固定的大小,但是行数和列数是动态更新的,以填充所有可用空间。例如,这种网格可以用于无限2D场的游戏或实现元胞自动机。在前一篇文章中,Wpf数据网格被认为具有动态定义的行数和列数,但所有单元格具有相同的大小。 原文发表在博客上。 特性 应用程序演示了以下功能: 所有单元格具有固定的宽度和高度;单元格的大小可以在运行时改变;行数和列数由用户控件大小定义;网格占据尽可能多的空间;单击可切换单元格的状态;异步添加/删除单元格的方法;调整计时器,防止过于频繁的细胞更新;保护细胞状态;依赖容器的使用;日志记录。 背景 解决方案使用c# 6, . net 4.6.1, Wpf与MVVM模式,NuGet包Unity和Ikc5.TypeLibrary。 解决方案 Wpf应用程序 Wpf应用程序是在一个主窗口的MVVM模式下完成的。动态网格是作为用户控件实现的,它包含绑定到单元格视图模型的可观察集合的DataGrid控件。正如上面提到的,这篇文章的代码是基于前一篇文章的代码,所以这里我们关注的是新的或者修改过的代码。 动态数据网格的视图模型包括单元格、视图和网格大小、单元格集合的数据模型和单元格视图模型集合的集合。视图大小属性绑定到数据网格控件的实际大小。实际上,从MVVM模式的角度来看,这并不是一种明确的方法,视图模型应该对视图一无所知,但它是通过绑定和附加属性来精确实现的。网格大小,即行数和列数,是按视图大小除以单元格大小计算的。由于行数和列数都是整数,视图中单元格的实际大小不能等于单元格的宽度和高度。 更改控制大小并计算网格的行数和列数后,重新创建单元格集,但保留单元格的状态。然后用异步方法更新单元格视图模型的集合。方法分析必要的更改并删除或添加行,并将单元格视图模型删除或添加到行中。异步方法允许应用程序负责,并且使用取消令牌允许在控件大小再次更改时取消更新。 动态网格控件 动态网格视图模型实现了IDynamicGridViewModel界面,该界面具有大小属性、单元格集合数据模型、单元格视图模型集合的可观察集合,以及多个颜色属性: 隐藏,收缩,复制Code

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的问题): 隐藏,复制Code

attached:SizeObserver.Observe="True"
attached:SizeObserver.ObservedWidth="{Binding ViewWidth, Mode=OneWayToSource}"
attached:SizeObserver.ObservedHeight="{Binding ViewHeight, Mode=OneWayToSource}"

调整计时器 对于视图大小的绑定有一个问题——因为绑定是在单个线程中执行的,视图宽度和高度的新值会在不同的时刻出现。这意味着有必要等待下一个。此外,为了防止过于频繁的改变网格大小,如果用户是缓慢地调整窗口,定时器在应用程序中使用。计时器是在构造函数中创建的,每当一个视图高度或视图宽度发生变化时,就会启动或重新启动计时器。 隐藏,收缩,复制Code

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

当计时器计时时,方法检查宽度和高度是否有效并重新创建单元格。然后方法CreateOrUpdateCellViewModels更新单元格视图模型集合的可观察集合被执行: 隐藏,收缩,复制Code

/// <summary>
/// Method change data model and grid size due to change of view size.
/// </summary>
/// <paramname="sender"></param>
/// <paramname="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架构和动态网格用户控制项源绑定到单元格集合,该集合的所有更改都是通过Dispatcher完成的。在应用程序中的优先级DispatcherPriority。ApplicationIdle在所有数据绑定之后执行时使用,但是可以使用其他值。 起始点是方法CreateOrUpdateCellViewModels,该方法在第一次创建单元格集合,创建取消令牌,并为第一行启动异步循环方法CreateCellViewModelsAsync。 隐藏,收缩,复制Code

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(单元格)的每一行位置执行。数,GridHeight)。可能出现下列情况: rowNumber >= GridHeight,这意味着集合单元格包含的行比当前网格大小的行多。这些行应该是re移动: 隐藏,复制CodeApplication.Current.Dispatcher.Invoke ( () =比;Cells.RemoveAt (positionToProcess), DispatcherPriority.ApplicationIdle, cancellationToken); rowNumber & lt;细胞。计数,这意味着具有该索引的行存在于集合单元格中,且索引小于网格高度。在这种情况下,方法UpdateCellViewModelRow被调用: 隐藏,复制CodeApplication.Current.Dispatcher.Invoke ( () =比;UpdateCellViewModelRow (positionToProcess), DispatcherPriority.ApplicationIdle, cancellationToken); 我们注意,这一行是observablection<icellviewmodel> /icellviewmodel>。根据此集合的长度和网格宽度之间的关系,删除额外的单元格视图模型,使用动态网格数据模型中的新的ICell实例更新现有的单元格模型,并添加丢失的单元格视图模型: 隐藏,复制代码/ / / & lt; summary> ///在行中添加或删除单元格视图模型。 / / / & lt; / summary> /// <param name="rowNumber">数据模型的行数。</param> 私有void UpdateCellViewModelRow(int rowNumber) { var row =单元格[rowNumber]; //删除额外的单元格 而行。数比;GridWidth) row.RemoveAt (GridWidth); for (var pos = 0;pos & lt;GridWidth;pos + +) { //创建新的视图模型或更新现有的视图模型 var cell = CellSet。rowNumber GetCell (pos); 如果(pos & lt;row.Count) 行(pos)。细胞=细胞; 其他的 { var cellViewModel = new cellViewModel (cell); row.Add (cellViewModel); } } } “else”情况,即rowNumber >=单元格。Count和rowNumber <表示收集单元格不包含必要的行。这个行是通过方法CreateCellViewModelRow创建的: 隐藏,复制代码/ / / & lt; summary> ///添加新的对应于的单元格视图模型行 ///数据模型中的行数。 / / / & lt; / summary> /// <param name="rowNumber">数据模型的行数。</param> 私有void CreateCellViewModelRow(int rowNumber) { _logger。Log($"Create {rowNumber} row of cells"); var row = new observablection<ICellViewModel>(); for (var x = 0;x & lt;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(行); } 依赖容器 Unity被用作依赖容器。在这篇文章中,我们将EmptyLogger注册为logger,并为DynamicGridViewModel的实例创建singleton。在Wpf应用程序中DI容器的初始化是在App.xaml.cs的OnStartup方法中完成的: 隐藏,复制Code

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构造函数的参数是由容器解析的: 隐藏,复制Code

public MainWindow(IDynamicGridViewModel dynamicGridViewModel)
{
  InitializeComponent();
  DataContext = dynamicGridViewModel;
}

同理,DynamicGridViewModel构造函数的输入参数由容器来解析: 隐藏,复制Code

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.01.04最初的帖子。 本文转载于:http://www.diyabc.com/frontweb/news271.html

posted @ 2020-08-05 01:58  Dincat  阅读(292)  评论(0编辑  收藏  举报