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

使用 ApexGrid 整理 XAML

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (30投票s)

2011年7月30日

CPOL

5分钟阅读

viewsIcon

61199

downloadIcon

1174

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

Splashscreen.png

引言

Grid 是任何 WPF、Silverlight 或 WP7 开发者工具箱中最关键的部分之一。然而,创建行和列定义的 XAML 有点过于冗长——尤其是当你到处使用 Grid 时,例如在 DataTemplatesControlTemplates、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(','); 

我们创建将最终返回的 GridLengths 列表。如果 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 的工作方式完全相同。

Samples.png

尽情享用!

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

© . All rights reserved.