在进行 WPF 开发时,最常见的错误之一是人们试图将他们的视图适合他们现有的数据模型,而实际上应该由中间层(即视图模型)在两者之间进行调解。 XAML 非常强大,可以做一些聪明的事情,但是如果你发现自己不得不在视图中做太多的工作,或者过度依赖转换器之类的东西来将你的数据转换成更有用的形式,那么这通常是一个很好的迹象,表明你的视图模型没有正常工作。
退后一步,从另一个方向看:您心中有一个视图,您需要什么类型的数据才能使该视图起作用?首先你要显示两件事:你的标题(在顶部和左边)和你的单元格,所以首先在你的视图模型中为它们创建类:
public class CustomGridHeader
{
public int Column { get; set; }
public int Row { get; set; }
public string Header { get; set; }
}
public class CustomGridCell
{
public int Column { get; set; }
public int Row { get; set; }
public object Content { get; set; }
}
现在,您的视图模型将需要一组要显示的内容(标题和单元格)以及一个引用字典中数据的函数。我将把翻译步骤留给你,但这里已经足够开始了:
private int _NumColumns;
public int NumColumns
{
get { return this._NumColumns; }
set { this._NumColumns = value; RaisePropertyChanged(() => this.NumColumns); }
}
private int _NumRows;
public int NumRows
{
get { return this._NumRows; }
set { this._NumRows = value; RaisePropertyChanged(() => this.NumRows); }
}
private object[] _GridData;
public object[] GridData
{
get { return this._GridData; }
set { this._GridData = value; RaisePropertyChanged(() => this.GridData); }
}
private string[] Types = new string[] { "Type1", "Type2", "Type3" };
private string[] Categories = new string[] { "Cat1", "Cat2", "Cat3" };
private void UpdateGridData()
{
this.NumColumns = this.Categories.Length + 1;
this.NumRows = this.Types.Length + 1;
this.GridData = new object[(this.Categories.Length + 1) * (this.Types.Length + 1)];
// set category and type headers
for (int i=0; i<this.Categories.Length; i++)
this.GridData[i + 1] = new CustomGridHeader { Column = i + 1, Row = 0, Header = this.Categories[i] };
for (int i=0; i<this.Types.Length; i++)
this.GridData[(i + 1) * this.NumColumns] = new CustomGridHeader { Column = 0, Row = i + 1, Header = this.Types[i] };
// fill the cells
for (int i = 0; i < this.Categories.Length; i++)
for (int j = 0; j < this.Types.Length; j++)
this.GridData[(i + 1) * this.NumColumns + j + 1] = new CustomGridCell {Column = i+1, Row = j+1, Content = String.Format("{0}/{1}", i, j)};
}
从代码方面来说,这就是您所需要的,您现在可以将您的数据以一种可以被您的视图使用的形式呈现出来。您想在网格中显示您的数据,但从功能上讲,您真正要做的是显示项目列表,因此您真正应该做的是使用 ItemsControl,但将 Grid 指定为面板模板。您还需要设置 ItemContainerStyle 以设置每个元素的网格列和行。最后,您需要两个数据模板:一个指定标题应如何显示,另一个指定单元格应如何显示:
<ItemsControl ItemsSource="{Binding GridData}" Margin="50">
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type local:CustomGridHeader}">
<TextBlock Text="{Binding Header}" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="10"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:CustomGridCell}">
<Border BorderBrush="Black" BorderThickness="1" Margin="0,0,-1,-1">
<TextBlock Text="{Binding Content}" Background="AliceBlue" HorizontalAlignment="Center" VerticalAlignment="Center" Width="50" Height="50"/>
</Border>
</DataTemplate>
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:cmd ="http://www.galasoft.ch/mvvmlight">
<i:Interaction.Behaviors>
<local:GridSizeBehavior Columns="4" Rows="4"/>
</i:Interaction.Behaviors>
</Grid>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Grid.Column" Value="{Binding Column}" />
<Setter Property="Grid.Row" Value="{Binding Row}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
唯一缺少的部分是 GridSizeBehavior。 Grid 控件没有“NumColumns”或“NumRows”字段,但您可以通过附加行为自己有效地添加它(出于这个和其他原因,我不相信 Grid 是完成此任务的最佳控件,但这就是你所要求的,这就是我提供的):
public class GridSizeBehavior : Behavior<Grid>
{
public int Columns
{
get { return (int)GetValue(ColumnsProperty); }
set { SetValue(ColumnsProperty, value); }
}
// Using a DependencyProperty as the backing store for Columns. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ColumnsProperty =
DependencyProperty.Register("Columns", typeof(int), typeof(GridSizeBehavior), new PropertyMetadata(0, OnColumnsChanged));
static void OnColumnsChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
var behaviour = depObj as GridSizeBehavior;
if (behaviour == null)
return;
behaviour.SetColumns();
}
private void SetColumns()
{
var grid = this.AssociatedObject;
if (grid == null)
return;
int columns = this.Columns;
grid.ColumnDefinitions.Clear();
for (int i = 0; i < columns; i++)
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
}
public int Rows
{
get { return (int)GetValue(RowsProperty); }
set { SetValue(RowsProperty, value); }
}
// Using a DependencyProperty as the backing store for Rows. This enables animation, styling, binding, etc...
public static readonly DependencyProperty RowsProperty =
DependencyProperty.Register("Rows", typeof(int), typeof(GridSizeBehavior), new PropertyMetadata(0, OnRowsChanged));
static void OnRowsChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
var behaviour = depObj as GridSizeBehavior;
if (behaviour == null)
return;
behaviour.SetRows();
}
private void SetRows()
{
var grid = this.AssociatedObject;
if (grid == null)
return;
int Rows = this.Rows;
grid.RowDefinitions.Clear();
for (int i = 0; i < Rows; i++)
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
}
protected override void OnAttached()
{
base.OnAttached();
SetColumns();
SetRows();
}
}
把所有这些放在一起,你就会得到你想要的结果:
此处显示的确切实现的主要缺点是视图不会自动更新以响应对原始字典的更改,但鉴于字典不支持无论如何都不会发生的集合更改通知。是的,您完全可以在视图中使用转换器来枚举列和行并直接绑定到您的字典数据,但最终您会发现您的代码与我的代码非常匹配无论如何,我们已经在视图模型中完成了,所以您不妨以易于调试和单元测试的形式在那里完成。