源代码
介绍 , 这篇文章专门介绍了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