使用 ApexGrid 整理 XAML
对 Grid 控件的一个小巧而整洁的补充,可以整理 WPF、Silverlight 和 WP7 中的 XAML

引言
Grid
是任何 WPF、Silverlight 或 WP7 开发者工具箱中最关键的部分之一。然而,创建行和列定义的 XAML 有点过于冗长——尤其是当你到处使用 Grid 时,例如在 DataTemplates
、ControlTemplates
、List Items 等等中。
在本文中,我将向您展示如何扩展标准的 Grid
类,使其拥有两个新属性——Rows
和 Columns
,这将允许我们在行内定义行和列。
这与 Apex 有何关联
这是我 Apex 库中的众多控件之一。我将它们一个一个上传。但是,您不需要 Apex 库或任何其他文件来使用此类——您可以直接将其添加到您的项目中。
Apex 适用于 WPF、Silverlight 和 WP7。我将在本文中逐步向您展示如何使此类适用于每个平台。
问题
即使是相当简单的 Grid 定义也相当冗长
lt;!-- Too verbose! -->
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="66" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Grid content goes here. -->
</Grid>
我们可能只在实际的 Grid 中有几个控件,但仅用于定义行和列就有十几行。如果我们能这样做,那不是很棒吗?
<!-- Tidier and cleaner. -->
<Grid Rows="2*,Auto,*,66" Columns="2*,*,Auto">
<!-- Grid content goes here. -->
</Grid>
是的,我们可以——尽管最终结果将不是一个 Grid,而是从中定义的类。(您实际上可以通过使用附加属性来扩展现有的 Grid 来做到这一点。)
解决方案
向您的 WPF、Silverlight 或 WP7 项目添加一个新类。我们不需要使用用户控件模板,因为我们将从现有类派生。我们不需要使用自定义控件模板,因为我们不需要为此类定义任何 XAML。让我们开始吧——让类派生自 Grid
(我使用的是 Apex 项目中的命名空间和类名,如果您将其用作自己类的基准,则可以根据需要命名)
using System.Windows.Controls;
using System.Windows;
using System.ComponentModel;
using System.Collections.Generic;
using System;
namespace Apex.Controls
{
/// <summary>
/// The ApexGrid control is a Grid that supports easy definition of rows and columns.
/// </summary>
public class ApexGrid : Grid
{
}
我们将在 Grid 中添加两个新属性——Rows
和 Columns
。这些属性将是 string
类型,用于设置行和列的定义。我们不能将这些属性添加为标准属性,而是添加为依赖项属性,以便我们能够进行绑定等操作,就像 Grid 的其他属性一样。将这两个依赖项属性添加到类中,并使用 DependencyProperty.Register
函数将它们连接起来。
/// <summary>
/// The rows dependency property.
/// </summary>
private static readonly DependencyProperty rowsProperty =
DependencyProperty.Register("Rows", typeof(string), typeof(ApexGrid));
/// <summary>
/// The columns dependency property.
/// </summary>
private static readonly DependencyProperty columnsProperty =
DependencyProperty.Register("Columns", typeof(string), typeof(ApexGrid));
其中一件重要的事情是,我们也提供返回这些依赖项属性值的标准 CLR 属性。
/// <summary>
/// Gets or sets the rows.
/// </summary>
/// <value>The rows.</value>
public string Rows
{
get { return (string)GetValue(rowsProperty); }
set { SetValue(rowsProperty, value); }
}
/// <summary>
/// Gets or sets the columns.
/// </summary>
/// <value>The columns.</value>
public string Columns
{
get { return (string)GetValue(columnsProperty); }
set { SetValue(columnsProperty, value); }
}
现在我们有了属性——但它们什么也没做。我们需要的是在设置属性时创建适当的 Grid 或 Column 定义。这是我们必须小心的地方——看看下面的代码。
public string Columns
{
get { return (string)GetValue(columnsProperty); }
set
{
SetValue(columnsProperty, value);
BuildTheColumns();
}
}
这行不通——了解依赖项属性这一点非常重要。与标准属性不同,这些属性并不总是被使用。框架可以调用类中 static
的只读依赖项属性实例的 SetValue
——完全跳过属性访问器!总之,**永远**不要在依赖项属性访问器中执行除标准 GetValue
/SetValue
之外的任何操作——这只会导致麻烦。
那么我们如何知道属性何时发生更改?我们可以将一个 PropertyChangedCallback 委托传递给 DependencyProperty
的 Register
函数。这将允许我们指定一个在属性更改时被调用的函数。
将依赖项属性定义更改为如下所示(粗体显示)。
/// <summary>
/// The rows dependency property.
/// </summary>
private static readonly DependencyProperty rowsProperty =
DependencyProperty.Register("Rows", typeof(string), typeof(ApexGrid),
new PropertyMetadata(null, new PropertyChangedCallback(OnRowsChanged)));
/// <summary>
/// The columns dependency property.
/// </summary>
private static readonly DependencyProperty columnsProperty =
DependencyProperty.Register("Columns", typeof(string), typeof(ApexGrid),
new PropertyMetadata(null, new PropertyChangedCallback(OnColumnsChanged)));
并在下方添加“OnChanged
”函数。
/// <summary>
/// Called when the rows property is changed.
/// </summary>
/// <param name="dependencyObject">The dependency object.</param>
/// <param name="args">The <see cref="
System.Windows.DependencyPropertyChangedEventArgs"/>
instance containing the event data.</param>
private static void OnRowsChanged(DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs args)
{
}
/// <summary>
/// Called when the columns property is changed.
/// </summary>
/// <param name="dependencyObject">The dependency object.</param>
/// <param name="args">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/>
instance containing the event data.</param>
private static void OnColumnsChanged(DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs args)
{
}
现在我们有了一个实际提供此类功能的入口点。向“OnRowsChanged
”添加以下内容。
// Get the apex grid.
ApexGrid apexGrid = dependencyObject as ApexGrid;
// Clear any current rows definitions.
apexGrid.RowDefinitions.Clear();
// Add each row from the row lengths definition.
foreach (var rowLength in StringLengthsToGridLengths(apexGrid.Rows))
apexGrid.RowDefinitions.Add(new RowDefinition() { Height = rowLength });
这就是我们所需要的一切——非常简单。获取 Grid (作为函数的第一个参数传递)。然后清除所有行。然后调用我们假设的 StringLengthsToGridLengths
函数——该函数接收一个 string
并返回一个 GridLength
对象的枚举集合。然后,只需将具有指定高度的 RowDefinition
添加到行定义集合中即可。
通过添加下面的内容来完成 OnColumnsChanged
函数——然后我们将进入最后一部分,StringLengthsToGridLengths
。
// Get the apex grid.
ApexGrid apexGrid = dependencyObject as ApexGrid;
// Clear any current column definitions.
apexGrid.ColumnDefinitions.Clear();
// Add each column from the column lengths definition.
foreach (var columnLength in StringLengthsToGridLengths(apexGrid.Columns))
apexGrid.ColumnDefinitions.Add(new ColumnDefinition() { Width = columnLength });
只剩最后一件事要做——实际编写 StringLengthsToGridLengths
函数。我将逐一介绍。
/// <summary>
/// Turns a string of lengths, such as "3*,Auto,2000" into a set of gridlength.
/// </summary>
/// <param name="lengths">The string of lengths, separated by commas.</param>
/// <returns>A list of GridLengths.</returns>
private static List<GridLength> StringLengthsToGridLengths(string lengths)
{
// Create the list of GridLengths.
List<GridLength> gridLengths = new List<GridLength>();
// If the string is null or empty, this is all we can do.
if (string.IsNullOrEmpty(lengths))
return gridLengths;
// Split the string by comma.
string[] theLengths = lengths.Split(',');
我们创建将最终返回的 GridLength
s 列表。如果 string
是 null
或为空,则返回空列表。这会经常发生——想象一下您在 XAML 编辑器中将“3*”替换为“4*”——我们会删除每个字符然后重新输入——所以,在某个时候,一个空 string
将被传递给函数。调用 'Split
' 会将 string
分解为字符串数组,以逗号分隔。
// If we're NOT in silverlight, we have a gridlength converter
// we can use.
#if !SILVERLIGHT
// Create a grid length converter.
GridLengthConverter gridLengthConverter = new GridLengthConverter();
// Use the grid length converter to set each length.
foreach (var length in theLengths)
gridLengths.Add((GridLength)gridLengthConverter.ConvertFromString(length));
如果我们在 WPF 项目中,那真的很简单——GridLengthConverter
类将允许我们将每个 string
转换为 GridLength
。然而,此类也必须在 Silverlight 中工作——它没有 GridLengthConverter
(因此 WP7 也没有!)——所以我们必须稍微做些改变。
#else
// We are in silverlight and do not have a grid length converter.
// We can do the conversion by hand.
foreach(var length in theLengths)
{
// Auto is easy.
if(length == "Auto")
{
gridLengths.Add(new GridLength(1, GridUnitType.Auto));
}
如果字符串只是“Auto
”,那么我们就处理了上面相当微不足道的情况。
else if (length.Contains("*"))
{
// It's a starred value, remove the star and get the coefficient as a double.
double coefficient = 1;
string starVal = length.Replace("*", "");
// If there is a coefficient, try and convert it.
// If we fail, throw an exception.
if (starVal.Length > 0 && double.TryParse(starVal, out coefficient) == false)
throw new Exception("'" + length + "' is not a valid value.");
// We've handled the star value.
gridLengths.Add(new GridLength(coefficient, GridUnitType.Star));
}
如果 string
包含星号,我们可以假设它是一个带星号的值。我们尝试获取星号之前的数字(如果存在),然后将适当的 GridLength
添加到 gridLengths
列表中。
else
{
// It's not auto or star, so unless it's a plain old pixel
// value we must throw an exception.
double pixelVal = 0;
if(double.TryParse(length, out pixelVal) == false)
throw new Exception("'" + length + "' is not a valid value.");
// We've handled the star value.
gridLengths.Add(new GridLength(pixelVal, GridUnitType.Pixel));
}
}
#endif
// Return the grid lengths.
return gridLengths;
}
如果我们没有星号或 Auto,那么我们只有像素数。尝试转换并成功添加到列表中。
就是这样!整个东西现在都能正常工作了——这里有一个 Silverlight 的例子。
<Page x:Class="Apex.Page1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:a="clr-namespace:Apex.Controls"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
Title="Page1">
<!-- Tidier and cleaner. -->
<a:ApexGrid Rows="2*,Auto,*,66" Columns="2*,*,Auto">
<!-- Grid content goes here. -->
</a:ApexGrid>
</Page>
无论您使用的是 WPF、Silverlight 还是 WP7,ApexGrid
的工作方式完全相同。

尽情享用!
请稍后回来查看更新,并继续关注 Introducing Apex 文章——我将在接下来的几周内为 WPF、Silverlight 和 WP7 上传更多代码,并将维护 Introducing Apex 文章顶部的索引。