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

构建您自己的 Silverlight 数据网格:第二步

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.39/5 (9投票s)

2009年3月30日

CPOL

59分钟阅读

viewsIcon

47234

downloadIcon

869

实现编辑和验证功能。

1. 引言

教程概述

本文是讲解如何使用 Silverlight 和 GOA Toolkit 构建功能完备的数据网格教程的第二部分。

本教程的第一部分中,我们解释了如何创建一个只读网格的主体。在第二部分中,我们将专注于编辑和验证功能的实现。在第三部分中,我们将把注意力转向网格的标题部分。

在整篇文章中,我将假设您已经完成了教程的第一部分。如果还没有,我强烈建议您先完成。您可以在这里访问它:为 Silverlight 构建您自己的 DataGrid:第 1 步

入门

本教程是使用 GOA Toolkit 2009 Vol. 1 Build 251 的免费版编写的。如果您在本文发布前已经实现了教程的第一步,您可能需要升级。请检查您是否正在使用 GOA Toolkit 2009 Vol. 1 Build 251 或更高版本(而不是 build 212)。

请确保您的计算机上安装了此版本或更新的版本 (www.netikatech.com/downloads)。

实现编辑和验证过程的步骤并不难,但步骤繁多。如果您在阅读本文时感到迷茫,或者难以理解我们正在做的事情的目的,我们建议您参考本教程末尾的“单元格编辑过程”和“项验证过程”图片。这两张图片将为您提供编辑和验证过程的概览。在开始阅读本文之前打印这两张图片,并在执行教程的每一步时留意它们,这可能会是一个好主意。

第二篇文章紧接着第一篇结束的地方开始。在继续之前,您应该打开在教程第一步中创建的 GridBody 解决方案。

2. 基础单元格编辑

单元格状态

在教程的第一部分中,我们为单元格实现了两种可能的公共状态:StandardFocused

在单元格的代码中,我们使用私有字段 isFocused 来区分这两种状态。现在我们要实现编辑功能,我们必须为单元格添加一个新的状态:Edited 状态。

布尔字段 isFocused 不足以让我们追踪新的 Edited 状态。我们必须用更精细的东西来替换 isFocused 字段:CellState 枚举。

首先,让我们将这个枚举添加到我们的 GoaOpen\Extensions\Grid 文件夹中。

namespace Open.Windows.Controls
{
    public enum CellState
    {
        Standard = 0,
        Focused,
        Edited
    }
}

我们还需要将 Cell 类中的 isFocused 字段替换为 CellState 属性,并更新 OnGotFocusOnLostFocus 方法。

public CellState CellState
{
    get;
    private set;
}
protected override void OnGotFocus(RoutedEventArgs e)
{
    base.OnGotFocus(e);

    if (this.CellState != CellState.Focused) 
    {
        if (this.CellState != CellState.Edited)
        {
            VisualStateManager.GoToState(this, "Focused", true);
            this.CellState = CellState.Focused;
        }

        HandyContainer parentContainer = 
           HandyContainer.GetParentContainer(this);
        if (parentContainer != null)
        {
            parentContainer.CurrentCellName = this.Name;
            parentContainer.EnsureCellIsVisible(this);
        }
    }
}

protected override void OnLostFocus(RoutedEventArgs e)
{
    base.OnLostFocus(e);

    object currentFocusedElement = FocusManager.GetFocusedElement();
    if (!TreeHelper.IsChildOf(this, 
            currentFocusedElement as DependencyObject))
    {
        VisualStateManager.GoToState(this, "Standard", true);
        this.CellState = CellState.Standard;
    }
}

请注意,在 OnGotFocus 方法中,我们添加了一个新条件:this.CellState != CellState.Edited

这是因为当单元格被编辑时,我们不希望 OnGotFocus 方法将单元格的状态改回到 "Focused" 状态。

TextCell

点击时编辑

我们希望当用户点击当前单元格时,单元格状态变为“Edited”。对于 TextCell 来说,这意味着当用户点击单元格时,单元格中会显示一个 TextBox,以便用户可以编辑单元格的文本。

BeginEdit 方法

首先要做的是向 Cell 类添加一个 BeginEdit 方法。每当单元格状态必须变为 Edited 时,都会调用此方法。

internal bool BeginEdit()
{
    if (this.CellState == CellState.Edited)
        return true;

    if (this.IsTabStop)
    {
        VisualStateManager.GoToState(this, "Edited", true);
        bool isEditStarted = OnBeginEdit();
        if (isEditStarted)
            this.CellState = CellState.Edited;

        return isEditStarted;
    }

    return false;
}

protected abstract bool OnBeginEdit();

在这个方法中,我们首先检查单元格状态是否已经是“Edited”。如果单元格状态是“Edited”,那么我们什么也不用做,直接退出该方法。

然后,我们检查单元格是否可以获得焦点 (IsTabStop)。如果单元格不能获得焦点,那么它就不能被编辑。

接下来,我们通过调用 VisualStateManagerGoToState 方法来改变单元格的视觉状态。这将允许我们在单元格的模板(在 generic.xaml 文件末尾的单元格样式中定义)中执行一些操作。例如,对于 TextCell,我们会向单元格的模板添加一个 TextBox,并在单元格状态变为“Edited”时显示它。对于其他类型的单元格,我们可以执行其他操作,例如显示一个 ComboBox 或一个 DatePicker

之后,我们调用 OnBeginEdit 抽象方法。这个方法必须为每种类型的单元格重写。对于 TextCell,我们将使用这个方法来初始化在单元格被编辑时显示的 TextBox

TextCell 样式

我们现在必须更新 TextCell 样式,以考虑 Edited 状态。让我们转到 generic.xaml 文件的末尾并修改 TextCell 样式。在 TextCell 样式的模板中,找到 TextElement 并将其替换为以下元素:

<Grid>
    <TextBlock 
        x:Name="TextElement" 
        Text="{TemplateBinding Text}"
        Margin="{TemplateBinding Padding}"
        HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
        VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
    <TextBox 
        x:Name="TextBoxElement" 
        Visibility="Collapsed"
        Text="{TemplateBinding Text}"
        Margin="{TemplateBinding Padding}"
        HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
        VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
        Foreground="{TemplateBinding Foreground}"/>
</Grid>

我们现在有两个元素绑定到 TextCellText 属性:TextElement TextBlockTextBoxElement TextBox。默认情况下,TextBox 元素是折叠的(隐藏的)。我们必须将 "Edited" 视觉状态添加到模板中,并在该状态变为 "Active" 时使 TextBoxElement 可见。

<vsm:VisualState x:Name="Edited">
    <Storyboard>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="TextBoxElement" 
                                       Storyboard.TargetProperty="Visibility" 
                                       Duration="0">
            <DiscreteObjectKeyFrame KeyTime="0">
                <DiscreteObjectKeyFrame.Value>
                    <Visibility>Visible</Visibility>
                </DiscreteObjectKeyFrame.Value>
            </DiscreteObjectKeyFrame>
        </ObjectAnimationUsingKeyFrames>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="TextElement" 
                                       Storyboard.TargetProperty="Visibility" 
                                       Duration="0">
            <DiscreteObjectKeyFrame KeyTime="0">
                <DiscreteObjectKeyFrame.Value>
                    <Visibility>Collapsed</Visibility>
                </DiscreteObjectKeyFrame.Value>
            </DiscreteObjectKeyFrame>
        </ObjectAnimationUsingKeyFrames>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="FocusElement" 
                                       Storyboard.TargetProperty="Visibility" 
                                       Duration="0">
            <DiscreteObjectKeyFrame KeyTime="0">
                <DiscreteObjectKeyFrame.Value>
                    <Visibility>Visible</Visibility>
                </DiscreteObjectKeyFrame.Value>
            </DiscreteObjectKeyFrame>
        </ObjectAnimationUsingKeyFrames>
    </Storyboard>
</vsm:VisualState>

经过这些更改后,TextCell 样式将如下所示:

<Style TargetType="o:TextCell">
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="BorderBrush" Value="{StaticResource DefaultListControlStroke}"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="Foreground" Value="{StaticResource DefaultForeground}"/>
    <Setter Property="HorizontalContentAlignment" Value="Stretch" />
    <Setter Property="VerticalContentAlignment" Value="Stretch" />
    <Setter Property="Cursor" Value="Arrow" />
    <Setter Property="Padding" Value="2,2,1,1" />
    <Setter Property="Width" Value="100"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="o:TextCell">
                <Grid>
                    <vsm:VisualStateManager.VisualStateGroups>
                        <vsm:VisualStateGroup x:Name="CommonStates">
                            <vsm:VisualState x:Name="Standard"/>
                            <vsm:VisualState x:Name="Focused">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                            Storyboard.TargetName="FocusElement" 
                                            Storyboard.TargetProperty="Visibility" 
                                            Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                            <vsm:VisualState x:Name="Edited">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                                 Storyboard.TargetName="TextBoxElement" 
                                                 Storyboard.TargetProperty="Visibility" 
                                                 Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                                 Storyboard.TargetName="TextElement" 
                                                 Storyboard.TargetProperty="Visibility" 
                                                 Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Collapsed</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                            Storyboard.TargetName="FocusElement" 
                                            Storyboard.TargetProperty="Visibility" 
                                            Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                    </vsm:VisualStateManager.VisualStateGroups>
                    <Grid>
                        <TextBlock 
                            x:Name="TextElement" 
                            Text="{TemplateBinding Text}"
                            Margin="{TemplateBinding Padding}"
                            HorizontalAlignment=
                              "{TemplateBinding HorizontalContentAlignment}"
                            VerticalAlignment=
                              "{TemplateBinding VerticalContentAlignment}"/>
                        <TextBox 
                            x:Name="TextBoxElement" 
                            Visibility="Collapsed"
                            Text="{TemplateBinding Text}"
                            Margin="{TemplateBinding Padding}"
                            HorizontalAlignment=
                              "{TemplateBinding HorizontalContentAlignment}"
                            VerticalAlignment=
                              "{TemplateBinding VerticalContentAlignment}"
                            Foreground="{TemplateBinding Foreground}"/>
                    </Grid>
                    <Rectangle Name="FocusElement" 
                               Stroke="{StaticResource DefaultFocus}" 
                               StrokeThickness="1" 
                               IsHitTestVisible="false" 
                               StrokeDashCap="Round" 
                               Margin="0,1,1,0" 
                               StrokeDashArray=".2 2" 
                               Visibility="Collapsed" />
                    <Rectangle Name="CellRightBorder" 
                               Stroke="{TemplateBinding BorderBrush}" 
                               StrokeThickness="0.5" 
                               Width="1" 
                               HorizontalAlignment="Right"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

OnApplyTemplate 方法

我们必须能够从我们的代码中访问刚刚在 TextCell 模板中添加的 TextBoxElement

让我们向 TextCell 类添加一个 TextBoxElement 字段。我们将在模板应用到 TextCell 后立即获取对 TextBoxElement 的引用,即在 TextCellOnApplyTemplate 方法中。

private TextBox textBoxElement;
public override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    textBoxElement = GetTemplateChild("TextBoxElement") as TextBox;
}

OnBeginEdit 方法

TextCell 类中,我们还需要重写 Cell 类的 OnBeginEdit 方法,以便在单元格状态变为“Edited”时初始化 TextBoxElement

protected override bool OnBeginEdit()
{
    if (textBoxElement != null)
    {
        if (textBoxElement.Focus())
        {
            textBoxElement.SelectionStart = textBoxElement.Text.Length;
            return true;
        }
    }

    return false;
}

在这个方法中,我们将焦点放在 TextBoxElement 上,并将选择光标置于现有文本的末尾。这样,用户可以立即开始编辑文本。根据您的偏好,您可能希望当用户开始编辑单元格时,TextBoxElement 的全部文本都被选中。在这种情况下,您需要在上面的代码中调用 TextBoxElementSelectAll 方法。

我们还必须重写 CheckBoxCell 类的 OnBeginEdit 方法;否则,我们将无法构建我们的项目。由于我们目前不处理 CheckBoxCell,所以我们只实现一个最小化的 OnBeginEdit 方法。

protected override bool OnBeginEdit()
{
    return true;
}

点击时开始编辑

我们最初的要求是用户能够通过点击当前单元格来开始编辑。因此,我们必须重写 TextCellOnMouseLeftButtonDown 方法来实现这个功能。

(请注意:当前您应该打开了 CheckBoxCell 文件,但您必须在 TextCell 文件中重写 OnMouseLeftButtonDown 方法。在应用更改之前,不要忘记打开正确的文件。)

protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
    base.OnMouseLeftButtonDown(e);

    if (this.CellState == CellState.Focused)
        BeginEdit();               
    
    if (this.CellState == CellState.Edited)
        e.Handled = true;
        //we do not want that the ContainerItem 
        //OnMouseLeftButtonDown is called
}

请注意,在上面的方法中,我们在调用 BeginEdit 方法之前检查了单元格是否是当前单元格(this.CellState == CellState.Focused)。这是因为我们不希望用户一点击单元格就开始编辑。只有当用户点击当前单元格时,单元格才会进入编辑状态。

让我们构建我们的项目。

由于我们更改了一些类的代码,可能需要在相应文件的顶部添加一些 using 子句。如果您无法构建项目,并遇到类似“找不到类型或命名空间名称'ATypeName'(是否缺少 using 指令或程序集引用?)”的错误,这意味着相应文件中缺少一些 using 子句。

解决该错误最快的方法是:

  • 双击错误,以便打开相应的文件。
  • 在代码窗口中,右键单击导致错误的关键字(它将已经被高亮显示)。
  • 在出错关键字旁边打开的上下文菜单中,单击“解析”项。

让我们试试我们的改动。

如果我们启动程序并点击一个 TextCell,该单元格会成为当前单元格。如果我们再次点击该单元格,文本框就会显示出来,允许我们编辑其内容。

然而,我们面临几个问题:

  • 单元格中显示的 TextBox 样式很丑。它太高了,并且显示了不好看的边框和不想要的背景色。
  • 我们一离开单元格,在文本框中输入的字符就丢失了。

CellTextBoxStyle

当前在 TextCell 样式中使用的默认 TextBox 样式不适合单元格的外观。让我们为将在 TextCell 和任何其他需要显示 TextBox 的单元格内部使用的文本框创建一个新样式。

在 GoaOpen 项目的 generic.xaml 文件中,紧挨着 TextCell 样式之前,让我们添加这个新样式:

<Style x:Key="CellTextBoxStyle" TargetType="TextBox">
    <Setter Property="Background" Value="Transparent" />
    <Setter Property="Foreground" Value="{StaticResource DefaultForeground}"/>
    <Setter Property="BorderThickness" Value="0"/>
    <Setter Property="Padding" Value="0" />
    <Setter Property="BorderBrush" Value="{StaticResource DefaultListControlStroke}" />
    <Setter Property="SelectionBackground" Value="{StaticResource DefaultDownColor}" />
    <Setter Property="SelectionForeground" Value="{StaticResource DefaultForeground}" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="TextBox">
                <Grid x:Name="RootElement">
                    <vsm:VisualStateManager.VisualStateGroups>
                        <vsm:VisualStateGroup x:Name="CommonStates">
                            <vsm:VisualState x:Name="Normal" />
                            <vsm:VisualState x:Name="Disabled">
                                <Storyboard>
                                    <DoubleAnimation Duration="0" 
                                         Storyboard.TargetName="ContentElement" 
                                         Storyboard.TargetProperty="Opacity" 
                                         To="0.6"/>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                    </vsm:VisualStateManager.VisualStateGroups>
                    <Border x:Name="DisabledVisual" 
                            Background="{StaticResource DefaultDisabled}" 
                            BorderBrush="{TemplateBinding BorderBrush}" 
                            BorderThickness="1" CornerRadius="1" 
                            Visibility="Collapsed" 
                            IsHitTestVisible="False"/>
                    <ScrollViewer x:Name="ContentElement" 
                                  Style="{StaticResource ScrollViewerStyle}" 
                                  Padding="{TemplateBinding Padding}" 
                                  IsTabStop="False"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

这个样式是根据标准的 TextBox 样式创建的。我们移除了不必要的状态、边框和背景。

TextCell 样式的 Template 属性中,让我们将这个新样式应用到 TextBoxElement

<TextBox 
    x:Name="TextBoxElement"
    Style="{StaticResource CellTextBoxStyle}"
    Visibility="Collapsed"
    Text="{TemplateBinding Text}"
    Margin="{TemplateBinding Padding}"
    HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
    VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
    Foreground="{TemplateBinding Foreground}"/>

如果我们再次启动我们的应用程序并编辑一个 TextCell,我们可以看到编辑后单元格的外观比以前好多了。

应用编辑更改

现在我们有了一个可以编辑的单元格,我们希望实现以下功能:

  • 如果当前单元格处于编辑状态,并且用户导航到另一个单元格,用户对单元格文本所做的更改将应用到该单元格所绑定的属性上。例如,如果单元格绑定到 Person 类的 FirstName 属性,我们希望当用户导航到另一个单元格时,FirstName 属性值会更新。
  • 如果当前单元格处于编辑状态,并且用户按下“Esc”键,单元格将离开 Edited 状态;它会回到 Focus 状态,并且用户对单元格文本所做的所有更改都将被丢弃。

TwoWay 绑定

当我们在本教程的第一部分为我们的单元格应用绑定时,我们没有指定绑定的模式。

让我们以 Person 类的 FirstName 属性为例。此属性绑定到 FirstName TextCellText 属性。由于我们在网格主体的 ItemTemplate 中设置了绑定,任何对 PersonFirstName 属性值的更改也会应用到 FirstName TextCellText 上。因为我们希望用户能够编辑单元格,所以我们也希望任何对 TextCellText 属性的更改也能应用到 PersonFirstName 属性上。

为了实现这一点,我们必须告诉绑定这样做。这个更改很容易应用,因为绑定提供了一个“TwoWay”模式,其行为正是如此。让我们将此更改应用到除了 ChildrenCount 单元格之外的所有单元格。

打开 GridBody 项目的 Page.xaml 文件,并将 HandyContainerItemDataTemplate 替换为以下内容:

<g:ItemDataTemplate>
    <Grid>
        <o:HandyDataPresenter DataType="GridBody.Person">
            <g:GDockPanel>
                <g:GDockPanel.KeyNavigator>
                    <o:RowSpatialNavigator/>
                </g:GDockPanel.KeyNavigator>
                <g:GStackPanel Orientation="Horizontal" 
                               g:GDockPanel.Dock="Top">
                    <g:GStackPanel.KeyNavigator>
                        <o:RowSpatialNavigator/>
                    </g:GStackPanel.KeyNavigator>
                    <o:TextCell Text="{Binding FirstName, Mode=TwoWay}" 
                                x:Name="FirstName"/>
                    <o:TextCell Text="{Binding LastName, Mode=TwoWay}" 
                                x:Name="LastName"/>
                    <o:TextCell Text="{Binding Address, Mode=TwoWay}" 
                                x:Name="Address"/>
                    <o:TextCell Text="{Binding City, Mode=TwoWay}" 
                                x:Name="City"/>
                    <o:TextCell Text="{Binding ZipCode, Mode=TwoWay}" 
                                x:Name="ZipCode"/>
                    <o:CheckBoxCell IsChecked="{Binding IsCustomer, Mode=TwoWay}" 
                                    x:Name="IsCustomer"/>
                </g:GStackPanel>
                <Rectangle Height="1" 
                           Stroke="{StaticResource DefaultListControlStroke}" 
                           StrokeThickness="0.5" 
                           Margin="-1,0,0,-1" 
                           g:GDockPanel.Dock="Top"/>
                <o:TextCell Text="{Binding Comment}" 
                            g:GDockPanel.Dock="Fill" 
                            x:Name="Comment" 
                            Width="Auto"/>
            </g:GDockPanel>
        </o:HandyDataPresenter>
        <o:HandyDataPresenter DataType="GridBody.Country">
            <g:GStackPanel Orientation="Horizontal">
                <g:GStackPanel.KeyNavigator>
                    <o:RowSpatialNavigator/>
                </g:GStackPanel.KeyNavigator>
                <o:TextCell Text="{Binding Name, Mode=TwoWay}" 
                            x:Name="CountryName"/>
                <o:TextCell Text="{Binding Children.Count}" 
                            x:Name="ChildrenCount"/>
            </g:GStackPanel>
        </o:HandyDataPresenter>
    </Grid>
</g:ItemDataTemplate>

CommitEdit

就像我们创建了一个 BeginEdit 方法来“强制”单元格进入编辑状态一样,我们也将实现一个 CommitEdit 方法,它将强制单元格应用用户的更改并离开 Edited 状态。

让我们向 Cell 类添加 CommitEdit 方法:

internal bool CommitEdit(bool keepFocus)
{
    if (this.CellState != CellState.Edited)
        return true;

    OnCommitEdit();

    if (this.CellState == CellState.Edited)
    {
        if (keepFocus)
        {
            VisualStateManager.GoToState(this, "Focused", true);
            this.CellState = CellState.Focused;
            bool gotFocus = this.Focus();
            Debug.Assert(gotFocus);
        }
        else
        {
            VisualStateManager.GoToState(this, "Standard", true);
            this.CellState = CellState.Standard;
        }
    }

    return true;
}

protected abstract void OnCommitEdit();

此方法有一个 keepFocus 参数,用于确定在离开 Edited 状态后,单元格的状态是必须设置回 Focused 状态还是 Standard 状态。

就像我们创建 BeginEdit 方法时创建了 OnBeginEdit 方法一样,我们也创建了一个 OnCommitEdit 抽象方法。这个方法必须在继承的单元格中被重写,以应用用户所做的更改。对于 TextCell 来说,OnCommitEdit 的实现非常简短:

protected override void OnCommitEdit()
{
    if (textBoxElement != null)
        this.Text = textBoxElement.Text;
}

由于我们目前不处理 CheckBox 单元格,让我们在 CheckBoxCell 类中实现一个最小化的 OnCommitEdit

protected override void OnCommitEdit()
{
}

我们的要求是当用户导航到另一个单元格时应用他们所做的更改。这可以转化为以下要求:当当前单元格不再是当前单元格时,调用 CommitEdit 方法。它也可以转化为:在单元格的 OnLostFocus 方法中调用 CommitEdit 方法。

所以,让我们相应地修改 Cell 类的 OnLostFocus 方法:

protected override void OnLostFocus(RoutedEventArgs e)
{
    base.OnLostFocus(e);

    object currentFocusedElement = FocusManager.GetFocusedElement();
    if (!TreeHelper.IsChildOf(this, currentFocusedElement as DependencyObject))
    {
        if (CellState == CellState.Edited)
            CommitEdit(false);
        else
        {
            VisualStateManager.GoToState(this, "Standard", true);
            this.CellState = CellState.Standard;
        }
    }
}

CancelEdit

我们有一个 BeginEdit 方法来强制单元格进入 "Edited" 状态,还有一个 CommitEdit 方法来强制一个已编辑的单元格提交用户所做的更改并离开编辑状态。我们还需要一个方法来强制一个已编辑的单元格放弃更改并离开 Edited 状态。

我们将在 Cell 类上以与其他两个方法相同的方式实现这个方法,并且我们还将创建一个 OnCancelEdit 抽象方法,该方法必须在继承的单元格类中被重写。

internal virtual bool CancelEdit(bool keepFocus)
{
    if (this.CellState != CellState.Edited)
        return true;

    OnCancelEdit();

    if (this.CellState == CellState.Edited)
    {
        if (keepFocus)
        {
            VisualStateManager.GoToState(this, "Focused", true);
            this.CellState = CellState.Focused;
            bool gotFocus = this.Focus();
            Debug.Assert(gotFocus);

        }
        else
        {
            VisualStateManager.GoToState(this, "Standard", true);
            this.CellState = CellState.Standard;
        }

    }


    return true;
}

protected abstract void OnCancelEdit();

TextCell 类中,OnCancelEdit 方法的实现与 OnCommitEdit 的实现一样简短。

protected override void OnCancelEdit()
{
    if (textBoxElement != null)
        textBoxElement.Text = this.Text;
}

现在,我们将在 CheckBoxCell 类中实现一个空的 OnCancelEdit 方法。

protected override void OnCancelEdit()
{
}

我们的第二个要求是:“如果当前单元格正在被编辑,并且用户按下了‘Esc’键,那么单元格将离开 Edited 状态;它会回到 Focused 状态,并且用户对单元格文本所做的所有更改都将被丢弃。”

这可以通过重写 Cell 类的 OnKeyDown 方法并适当地调用 CancelEdit 方法来实现。

protected override void OnKeyDown(KeyEventArgs e)
{
    if (CellState == CellState.Edited)
    {
        switch (e.Key)
        {
            case Key.Escape:
                CancelEdit(true);
                e.Handled = true;
                break;
        }
    }

    base.OnKeyDown(e);
}

如果我们现在启动我们的应用程序,我们可以测试我们已经实现的新功能。

  • 双击 LastName2 单元格进行编辑(我们先点击一次使其成为当前单元格,然后再点击第二次进行编辑)。
  • 通过在键盘上输入内容来更改单元格的文本。
  • 使用键盘导航键或通过点击导航到另一个单元格。

这一次,我们所做的更改在我们离开单元格时被保留了下来。

让我们重复同样的过程,但这次使用 Esc 键。

  • 双击 LastName2 单元格进行编辑(我们先点击一次使其成为当前单元格,然后再点击第二次进行编辑)。
  • 通过在键盘上输入内容来更改单元格的文本。
  • 按下 Esc 键。

当按下 Esc 键时,更改被丢弃,单元格退出 Edited 状态。

无需点击即可编辑

我们不能强迫用户每次需要编辑单元格时都去点击它。如果当前单元格在用户用键盘输入内容后能自动切换到 Edited 状态,那会方便得多。

这可以通过在 TextCell 类中重写 OnKeyDown 方法来实现。

protected override void OnKeyDown(KeyEventArgs e)
{
    if (CellState != CellState.Edited)
    {
        switch (e.Key)
        {
            case Key.Left:
            case Key.Up:
            case Key.Down:
            case Key.Right:
            case Key.PageDown:
            case Key.PageUp:
            case Key.Home:
            case Key.End:
            case Key.Enter:
            case Key.Ctrl:
            case Key.Shift:
            case Key.Alt:
            case Key.Escape:
            case Key.Tab:
                break;

            default:
                if (BeginEdit() && (textBoxElement != null))
                    textBoxElement.Text = "";
                break;
        }
    }

    base.OnKeyDown(e);
}

当用户按下键时,会调用 BeginEdit 方法,强制单元格进入 Edited 状态。此外,textBoxElement 的文本被清除。这样,用户输入的文本将替换单元格的当前文本。

请注意,在 KeyDown 方法中,某些键(如导航键,左、上)不会被考虑,因为它们被用于其他进程。

我们可以通过启动应用程序,导航到任何一个 TextCell,然后输入一些文本来测试这个新功能。我们会注意到,该单元格会自动切换到 Edited 状态,并且输入的文本会替换掉单元格的文本。

CheckBoxCell

引言

现在是时候将我们的编辑功能也应用到 CheckBoxCell 类了。然而,我们对 CheckBoxCell 的要求与对 TextCell 的不完全相同。

当用户点击一个 TextCell 时,该单元格成为当前单元格。然后他/她可以直接通过键入内容或者再次点击该单元格来开始编辑。然而,对于 CheckBox 单元格来说,这种行为对用户来说可能看起来很奇怪。这意味着用户将必须先点击单元格使其成为当前单元格,然后再点击一次来改变它的值。这不是用户所期望的行为。当看到单元格中的复选框时,用户会期望,当他点击该单元格时,无论该单元格是否是当前单元格,它的值都会改变。在实现 CheckBoxCell 的编辑功能时,我们必须考虑到这一点。

CheckBoxCell 样式

这一次,我们不必在单元格的模板内添加编辑控件。我们将自己处理编辑过程。

因此,CheckBoxCell 样式的修改很短。我们只需要将 "Edited" VisualState 添加到 "CommonStates" VisualStateGroups 中。

<vsm:VisualStateManager.VisualStateGroups>
    <vsm:VisualStateGroup x:Name="CommonStates">
        <vsm:VisualState x:Name="Standard"/>
        <vsm:VisualState x:Name="Focused">
            <Storyboard>
                <ObjectAnimationUsingKeyFrames 
                    Storyboard.TargetName="focusElement" 
                    Storyboard.TargetProperty="Visibility" 
                    Duration="0">
                    <DiscreteObjectKeyFrame KeyTime="0">
                        <DiscreteObjectKeyFrame.Value>
                            <Visibility>Visible</Visibility>
                        </DiscreteObjectKeyFrame.Value>
                    </DiscreteObjectKeyFrame>
                </ObjectAnimationUsingKeyFrames>
            </Storyboard>
        </vsm:VisualState>
        <vsm:VisualState x:Name="Edited">
            <Storyboard>
                <ObjectAnimationUsingKeyFrames 
                    Storyboard.TargetName="focusElement" 
                    Storyboard.TargetProperty="Visibility" 
                    Duration="0">
                    <DiscreteObjectKeyFrame KeyTime="0">
                        <DiscreteObjectKeyFrame.Value>
                            <Visibility>Visible</Visibility>
                        </DiscreteObjectKeyFrame.Value>
                    </DiscreteObjectKeyFrame>
                </ObjectAnimationUsingKeyFrames>
            </Storyboard>
        </vsm:VisualState>
    </vsm:VisualStateGroup>
</vsm:VisualStateManager.VisualStateGroups>

EditedValue

IsChecked 属性是 CheckBoxCell 的值。由于我们必须能够取消用户在编辑单元格时所做的更改,我们不能将他所做的更改直接存储在 IsChecked 属性中。我们将改用一个 EditedValue 属性。

private bool editedValue;
private bool EditedValue
{
    get { return editedValue; }
    set
    {
        if (editedValue != value)
        {
            editedValue = value;
            isOnReadOnlyChange = true;
            if (editedValue)
                CheckMarkVisibility = Visibility.Visible;
            else
                CheckMarkVisibility = Visibility.Collapsed;

            isOnReadOnlyChange = false;
        }
    }
}

该属性的设置器(setter)实现方式是,当编辑的值被修改时,CheckMarkVisibility 属性会更新,以便用户可以看到更改。

OnBeginEdit、OnCancelEdit 和 OnCommitEdit 方法

OnBeginEditOnCancelEditOnCommitEdit 方法很容易实现。

protected override bool OnBeginEdit()
{
    if (this.CellState != CellState.Edited)
        editedValue = this.IsChecked;

    return true;
}

protected override void OnCancelEdit()
{
    //Reset the EditedValue to be sure that 
    //the CheckMarkVisibility value returns to the old value
    EditedValue = this.IsChecked;
}

protected override void OnCommitEdit()
{
    this.IsChecked = EditedValue;
}

OnMouseLeftButtonDown 方法

如前所述,当用户点击单元格时,即使它不是当前单元格,也必须强制该单元格切换到 Edited 状态。然而,这并不像看起来那么容易实现。

一个单元格只有在它是当前单元格并且当前单元格拥有焦点时才能被编辑。这意味着如果用户点击的 CheckBoxCell 不是当前单元格,我们必须“将”焦点放在那个单元格上,然后紧接着编辑该单元格。然而,将焦点放在一个单元格上并不是一个完全同步的过程。我们不能在单元格上调用 Focus 方法,然后就假设该单元格已经获得了焦点并且所有相关的事件(LostFocusGotFocus...)都已经被调用。这样做是行不通的。特别是,LostFocusGotFocus 事件不是同步调用的,因此,我们不能在调用 Focus 方法后立即编辑单元格,因为它还不是当前单元格。

因此,当用户点击单元格时,我们会将焦点设置到该单元格上,然后,在开始编辑单元格之前,我们会让 Silverlight 完成焦点处理过程。我们将通过使用 Dispatcher 的 BeginInvoke 方法异步调用 BeginEdit 方法来实现这一点。

protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
    base.OnMouseLeftButtonDown(e);
    if ((this.CellState == CellState.Focused) || (this.CellState == CellState.Edited))
    {
        BeginEdit();
        if (this.CellState == CellState.Edited)
            EditedValue = !EditedValue;
    }
    else
        Dispatcher.BeginInvoke(new Action(MouseDownBeginEdit));
    
}

private void MouseDownBeginEdit()
{
    if ((this.CellState == CellState.Focused) || (this.CellState == CellState.Edited))
    {
        BeginEdit();
        if (this.CellState == CellState.Edited)
            EditedValue = !EditedValue;
    }
}

OnKeyDown 方法

OnKeyDown 方法的实现方式与 TextCell 相同。

当用户按下空格键时,CheckBoxCell 的值会改变。

protected override void OnKeyDown(KeyEventArgs e)
{
    switch (e.Key)
    {
        case Key.Space:
            if (BeginEdit())
            {
                EditedValue = !EditedValue;
                e.Handled = true;
            }
            break;
    }

    base.OnKeyDown(e);
}

我们可以通过启动我们的应用程序并点击一个 CheckBoxCell 来测试我们的更改。或者,我们可以使用键盘导航键导航到一个 CheckBoxCell,然后通过按空格键来改变它的值。

3. 高级编辑功能

IsDirty

引言

在程序流程中,了解当前单元格是否被编辑以及其值是否已更改通常很重要。

例如,当应用程序关闭时,这个值很有用。通过知道当前单元格正在被编辑并且其值已更改,我们可以采取适当的措施,例如警告用户应用更改。

如果一个单元格被编辑过且其值已改变,我们称之为“脏”单元格。

IsCurrentCellDirty 属性

顾名思义,IsCurrentCellDirty 属性将用于了解 HandyContainer 的当前单元格是否为脏数据。

让我们在我们的 HandyContainer 分部类(位于 GoaOpen\Extensions\Grid 文件夹中的那个)中实现这个属性。

public bool IsCurrentCellDirty
{
    get { return CurrentDirtyCell != null; }

}

private Cell currentDirtyCell;
internal Cell CurrentDirtyCell
{
    get { return currentDirtyCell; }
    set
    {
        if (currentDirtyCell != value)
        {
            currentDirtyCell = value;
            OnIsCurrentCellDirtyChanged(EventArgs.Empty);
        }

    }
}

public event EventHandler IsCurrentCellDirtyChanged;
protected virtual void OnIsCurrentCellDirtyChanged(EventArgs e)
{
    if (IsCurrentCellDirtyChanged != null)
        IsCurrentCellDirtyChanged(this, e);
}

由于在本教程的后续步骤中拥有对当前脏单元格的引用将非常重要,我们实现了一个内部的 CurrentDirtyCell 属性。如果当前单元格是脏的,这个属性将被填充。如果没有当前单元格或者它不是脏的,该属性的值将为 null

IsCurrentCellDirty 的值是根据 CurrentDirtyCell 的值计算得出的。我们还添加了一个 IsCurrentCellDirtyChanged 事件。

IsDirty 单元格属性

Cell 类的 IsDirty 属性用于了解该单元格是否为脏数据。该值与包含该 CellHandyContainerCurrentDirtyCell 值同步。

private bool isDirty;
public bool IsDirty
{
    get { return isDirty; }
    internal set
    {
        if (isDirty != value)
        {
            isDirty = value;
            HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
            if (parentContainer != null)
            {
                if (isDirty)
                    parentContainer.CurrentDirtyCell = this;
                else
                    parentContainer.CurrentDirtyCell = null;
            }
        }
    }
}

CellCancelEditCommitEdit 方法被调用时,IsDirty 属性值被设置回 false

internal bool CommitEdit(bool keepFocus)
{
    if (this.CellState != CellState.Edited)
        return true;

    if (IsDirty)
    {
        OnCommitEdit();
        IsDirty = false;
    }

    if (this.CellState == CellState.Edited)
    {
        if (keepFocus)
        {
            VisualStateManager.GoToState(this, "Focused", true);
            this.CellState = CellState.Focused;
            bool gotFocus = this.Focus();
            Debug.Assert(gotFocus);
        }
        else
        {
            VisualStateManager.GoToState(this, "Standard", true);
            this.CellState = CellState.Standard;
        }
    }

    return true;
}

internal virtual bool CancelEdit(bool keepFocus)
{
    if (this.CellState != CellState.Edited)
        return true;

    if (IsDirty)
    {
        OnCancelEdit();
        IsDirty = false;
    }

    if (this.CellState == CellState.Edited)
    {
        if (keepFocus)
        {
            VisualStateManager.GoToState(this, "Focused", true);
            this.CellState = CellState.Focused;
            bool gotFocus = this.Focus();
            Debug.Assert(gotFocus);

        }
        else
        {
            VisualStateManager.GoToState(this, "Standard", true);
            this.CellState = CellState.Standard;
        }
    }

    return true;
}

为了知道单元格是否为脏数据,TextCell 必须监视 TextBoxElementTextChanged 事件。如果单元格处于 Edited 状态并且 TextBoxElement 的文本被更改,那么就意味着该单元格是脏数据。

public override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    textBoxElement = GetTemplateChild("TextBoxElement") as TextBox;
    if (textBoxElement != null)
        textBoxElement.TextChanged += 
          new TextChangedEventHandler(textBoxElement_TextChanged);
}

private void textBoxElement_TextChanged(object sender, TextChangedEventArgs e)
{
    if (this.CellState == CellState.Edited)
        this.IsDirty = true;
}

CheckBoxCellEditedValue 改变时,它就变“脏”了。

private bool EditedValue
{
    get { return editedValue; }
    set
    {
        if (editedValue != value)
        {
            editedValue = value;
            isOnReadOnlyChange = true;
            if (editedValue)
                CheckMarkVisibility = Visibility.Visible;
            else
                CheckMarkVisibility = Visibility.Collapsed;

            IsDirty = true;
            isOnReadOnlyChange = false;
        }
    }
}

以编程方式编辑单元格

到目前为止,单元格只有在用户与它们交互时才能切换到或退出 Edited 状态。重要的是我们能够从程序的代码中做到这一点。因此,我们将向 HandyContainer 添加一个 BeginEditCommitEditCancelEdit 方法。通过调用 BeginEdit 方法,我们将强制当前单元格切换到 Edited 状态。通过调用 CommitEditCancelEdit 方法,我们将强制当前单元格提交或取消用户所做的更改并离开 Edited 状态。

让我们将这三个方法添加到我们的 HandyContainer 分部类中:

public bool BeginEdit()
{
    Cell currentCell = GetCurrentCell();
    if (currentCell != null)
        return currentCell.BeginEdit();

    return false;
}

public bool CommitEdit()
{
    return CommitEdit(true);
}

public bool CommitEdit(bool keepFocus)
{
    Cell currentCell = GetCurrentCell();
    if (currentCell != null)
        return CurrentDirtyCell.CommitEdit(keepFocus);

    return true;
}

public bool CancelEdit()
{
    return CancelEdit(true);
}

public bool CancelEdit(bool keepFocus)
{
    Cell currentCell = GetCurrentCell();
    if (currentCell != null)
        return CurrentDirtyCell.CancelEdit(keepFocus);

    return true;
}

private Cell GetCurrentCell()
{
    ContainerItem currentItem = this.CurrentItem;
    if ((currentItem != null) && !String.IsNullOrEmpty(this.CurrentCellName))
        return currentItem.FindCell(this.CurrentCellName);

    return null;
}

CanEdit

在某些情况下,能够阻止用户编辑单元格至关重要。例如,在我们的示例中,我们希望用户无法编辑 ChildrenCount 单元格,因为 Country 类的 Children.Count 属性是只读的。

让我们向我们的 Cell 类添加一个 CanEdit 依赖属性。我们正在实现一个依赖属性(而不是一个“标准”属性),因为我们希望能够在 XAML 中设置它的值。

public static readonly DependencyProperty CanEditProperty;

static Cell()
{
    CanEditProperty = DependencyProperty.Register("CanEdit", 
                      typeof(bool), typeof(Cell), new PropertyMetadata(true));
}

public bool CanEdit
{
    get { return (bool)GetValue(CanEditProperty); }
    set { SetValue(CanEditProperty, value); }
}

然后,让我们更新 Cell 类的 BeginEdit 方法。

internal bool BeginEdit()
{
    if (this.CellState == CellState.Edited)
        return true;

    if (this.IsTabStop && CanEdit)
    {
        ...

现在,让我们为 ChildrenCount TextCellCanEdit 属性添加一个 False 值。

  • 打开我们的 GridBody 项目的 Page.xaml 文件。
  • HandyContainerItemTemplate 中找到 ChildrenCount TextCell
  • 应用更改
<o:TextCell Text="{Binding Children.Count}" 
            x:Name="ChildrenCount" 
            CanEdit="False"/>

我们可以测试我们的更改。

  • 启动应用程序。
  • 尝试编辑其中一个 ChildrenCount 单元格。

正如预期的那样,无法编辑该单元格。

事件

引言

我们仍然需要实现事件,以便在当前单元格被编辑时得到通知。此外,即使我们实现了 CanEdit 属性,在某些情况下它可能功能不足。例如,应该可以仅在特定条件下中止编辑过程。

因此,我们将添加以下事件:

  • CurrentCellBeginEditing:这个事件将在编辑过程的最开始发生。与这个事件关联的 EventArg 将公开一个 Cancel 属性,允许从代码中取消编辑过程。
  • CurrentCellBeginEdited:这个事件将在开始编辑过程结束时,即单元格进入 Edited 状态后发生。
  • CurrentCellEndEdit:此事件将在编辑过程结束时,即单元格离开 Edited 状态后发生。

此外,我们还将实现 IsCurrentCellInEditMode 属性。该属性将用于了解当前单元格是否正在被编辑。不要混淆 IsCurrentCellDirty 属性(当前单元格正在被编辑并且其值已更改)和 IsCurrentCellInEditMode 属性(当前单元格正在被编辑,但我们不关心其值是否已更改)。

在 HandyContainer 中实现事件

internal void _OnCurrentCellBeginEditing(GCancelEventArgs e)
{
    OnCurrentCellBeginEditing(e);
}

public event EventHandler<GCancelEventArgs> CurrentCellBeginEditing;
protected virtual void OnCurrentCellBeginEditing(GCancelEventArgs e)
{
    if (CurrentCellBeginEditing != null)
        CurrentCellBeginEditing(this, e);
}

internal void _OnCurrentCellBeginEdited(EventArgs e)
{
    CurrentEditedCell = this.GetCurrentCell();
    OnCurrentCellBeginEdited(e);
}

public event EventHandler CurrentCellBeginEdited;
protected virtual void OnCurrentCellBeginEdited(EventArgs e)
{
    if (CurrentCellBeginEdited != null)
        CurrentCellBeginEdited(this, e);
}

public event EventHandler CurrentCellEndEdit;
internal void _OnCurrentCellEndEdit(EventArgs e)
{
    CurrentEditedCell = null;
    OnCurrentCellEndEdit(e);
}

internal void OnCurrentCellEndEdit(EventArgs e)
{
    if (CurrentCellEndEdit != null)
        CurrentCellEndEdit(this, e);
}

public bool IsCurrentCellInEditMode
{
    get { return CurrentEditedCell != null; }
}

internal Cell CurrentEditedCell
{
    get;
    set;
}

_OnCurrentCellBeginEditing_OnCurrentCellBeginEdited_OnCurrentCellEndEdit 内部方法将从 Cell 类中调用。CurrentEditedCell 的值在 _OnCurrentCellBeginEdited_OnCurrentCellEndEdit 方法中更新。

从 Cell 类调用 _OnCurrentCellBeginEditing, _OnCurrentCellBeginEdited 和 _OnCurrentCellEndEdit

_OnCurrentCellBeginEditing_OnCurrentCellBeginEdited 方法都在 Cell 类的 BeginEdit 方法中被调用。

internal bool BeginEdit()
{
    if (this.CellState == CellState.Edited)
        return true;

    if (this.IsTabStop && this.CanEdit)
    {
        HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
        GCancelEventArgs cancelEventArgs = new GCancelEventArgs(false);
        parentContainer._OnCurrentCellBeginEditing(cancelEventArgs);
        if (cancelEventArgs.Cancel)
            return false;

        VisualStateManager.GoToState(this, "Edited", true);
        bool isEditStarted = OnBeginEdit();
        if (isEditStarted)
        {
            this.CellState = CellState.Edited;
            parentContainer._OnCurrentCellBeginEdited(EventArgs.Empty);
        }

        return isEditStarted;
    }

    return false;
}

_OnCurrentCellEndEdit 方法在 Cell 类的 CancelEditCommitEdit 方法中都会被调用。

internal bool CommitEdit(bool keepFocus)
{
    if (this.CellState != CellState.Edited)
        return true;

    if (IsDirty)
    {
        OnCommitEdit();
        IsDirty = false;
    }

    if (this.CellState == CellState.Edited)
    {
        if (keepFocus)
        {
            VisualStateManager.GoToState(this, "Focused", true);
            this.CellState = CellState.Focused;
            bool gotFocus = this.Focus();
            Debug.Assert(gotFocus);
        }
        else
        {
            VisualStateManager.GoToState(this, "Standard", true);
            this.CellState = CellState.Standard;
        }

        HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
        parentContainer._OnCurrentCellEndEdit(EventArgs.Empty);

    }

    return true;
}

internal virtual bool CancelEdit(bool keepFocus)
{
    if (this.CellState != CellState.Edited)
        return true;

    if (IsDirty)
    {
        OnCancelEdit();
        IsDirty = false;
    }

    if (this.CellState == CellState.Edited)
    {
        if (keepFocus)
        {
            VisualStateManager.GoToState(this, "Focused", true);
            this.CellState = CellState.Focused;
            bool gotFocus = this.Focus();
            Debug.Assert(gotFocus);

        }
        else
        {
            VisualStateManager.GoToState(this, "Standard", true);
            this.CellState = CellState.Standard;
        }

        HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
        parentContainer._OnCurrentCellEndEdit(EventArgs.Empty);

    }

    return true;
}

更新 CommitEdit 和 CancelEdit 方法

现在我们已经实现了一个 CurrentEditedCell 属性,让我们使 HandyContainerCommitEditCancelEdit 方法更高效。

public bool CommitEdit(bool keepFocus)
{
    if (CurrentEditedCell != null)
        return CurrentEditedCell.CommitEdit(keepFocus);

    return true;
}
public bool CancelEdit(bool keepFocus)
{
    if (CurrentEditedCell != null)
        return CurrentEditedCell.CancelEdit(keepFocus);

    return true;
}

4. 单元格验证

引言

我们必须提供一种方法来验证用户在单元格中输入的数据。我们还应该能够通知用户他/她输入的数据是错误的,并且我们还应该能够强制用户在执行其他操作之前输入有效的数据。

Silverlight 已经提供了一种在绑定过程中调用的验证机制。我们将看到如何在我们的网格中使用它。然而,这种机制对我们的用途来说功能太弱了。我们还将实现我们自己的验证方法和事件来补充现有的验证机制。

CurrentCellValidating 事件

引言

此事件将在用户输入的数据被“发送”到单元格绑定的属性之前发生。与此事件关联的 EventArgs 将有一个 Cancel 属性,允许我们取消验证过程并强制单元格保持编辑状态。

CellValidatingEventArgs

让我们向 GoaOpen\Extensions\Grid 文件夹添加一个新的 CellValidatingEventArgs 类。这个专门的 EventArgs 将与 CurrentCellValidating 事件一起使用。

using Netika.Windows.Controls;

namespace Open.Windows.Controls
{
    public class CellValidatingEventArgs : GCancelEventArgs
    {
        public CellValidatingEventArgs(object oldValue, object newValue, bool cancel)
            : base(cancel)
        {
            OldValue = oldValue;
            NewValue = newValue;
        }

        public object OldValue
        {
            get;
            private set;
        }

        public object NewValue
        {
            get;
            private set;
        }
    }
}

CellValidatingEventArgs 继承自 GCancelEventArgs,因为 GCancelEventArgs 已经实现了一个 Cancel 属性。

我们还实现了 OldValueNewValue 属性。这些属性将让我们知道单元格的当前值(用户输入的值)和单元格之前的值(用户在单元格中输入内容之前的值)。

CurrentCellValidating 事件

让我们在我们的 HandyContainer 分部类中实现 CurrentCellValidating 事件。

internal void _OnCurrentCellValidating(CellValidatingEventArgs e)
{
    OnCurrentCellValidating(e);
}

public event EventHandler<CellValidatingEventArgs> CurrentCellValidating;
protected virtual void OnCurrentCellValidating(CellValidatingEventArgs e)
{
    if (CurrentCellValidating != null)
        CurrentCellValidating(this, e);
}

_OnCurrentCellValidating 方法将从 Cell 类中调用。

调用 _OnCurrentCellValidating 方法

我们需要从 Cell 类的 CommitEdit 方法中调用 _OnCurrentCellValidating 方法。在此调用之后,如果 EventArgsCancel 值被设置为 true,则意味着必须取消 CommitEdit 过程。

internal bool CommitEdit(bool keepFocus)
{
    if (this.CellState != CellState.Edited)
        return true;

    if (IsDirty)
    {
        HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
        if (parentContainer != null)
        {
            CellValidatingEventArgs cancelEventArgs = 
              new CellValidatingEventArgs(this.CurrentValue, this.DirtyValue, false);
            parentContainer._OnCurrentCellValidating(cancelEventArgs);
            if (cancelEventArgs.Cancel)
                return false;

            OnCommitEdit();

            IsDirty = false;
        }
    }

    if (this.CellState == CellState.Edited)
    . . .

CellValidatingEventArgsnewValueoldValue 参数由单元格的 CurrentValueDirtyValue 填充。

这些属性尚未实现。CurrentValue 属性必须填充单元格的值,而不考虑用户通过编辑单元格所做的任何更改。DirtyValue 属性必须填充单元格的值,同时考虑用户通过编辑单元格所做的更改。

实现 CurrentValue 和 DirtyValue

CurrentValueDirtyValue 属性必须在 TextCellCheckBoxCell 中实现。

首先,让我们在 Cell 类中添加抽象属性。

protected abstract object CurrentValue
{
    get;
}

protected abstract object DirtyValue
{
    get;
}

然后让我们在 TextCell 类中实现这些属性。

protected override object CurrentValue
{
    get { return this.Text; }
}

protected override object DirtyValue
{
    get
    {
        if (textBoxElement != null)
            return textBoxElement.Text;

        return this.Text;
    }
}

我们也在 CheckBoxCell 类中实现这些属性。

protected override object CurrentValue
{
    get { return this.IsChecked; }
}

protected override object DirtyValue
{
    get { return EditedValue; }
}

测试 CurrentCellValidating 事件

让我们通过在我们的 GridBody 项目中使用 CurrentCellValidating 事件来测试我们的更改。

首先,让我们修改 GridBody 项目的 Page.xaml 来处理这个事件。

<o:HandyContainer
    x:Name="MyGridBody"
    VirtualMode="On"
    AlternateType="Items"
    HandyDefaultItemStyle="Node"
    HandyStyle="GridBodyStyle"
    CurrentCellValidating="MyGridBody_CurrentCellValidating">

然后,让我们向 Page.xaml.cs 文件添加一些代码,以验证我们名字单元格的值。

private void MyGridBody_CurrentCellValidating(object sender, 
             Open.Windows.Controls.CellValidatingEventArgs e)
{
    if (MyGridBody.CurrentCellName == "FirstName")
    {
        string newValue = e.NewValue as string;
        if (string.IsNullOrEmpty(newValue))
            e.Cancel = true;
    }
}

通过我们实现 CurrentCellValidating 事件的方式,用户将无法用空的名字填充单元格。

让我们试试我们的改动。

  • 启动应用程序
  • 编辑 FirstName3 单元格。
  • 删除单元格内容。
  • 导航到 LastName3 单元格以提交所做的更改。

我们看到网格工作得不是很好。FirstName3 单元格的提交过程被取消了,单元格仍然处于编辑状态,但没有任何东西阻止我们导航到 LastName3 单元格。最终,我们有 FirstName3 单元格仍然在编辑,但当前单元格是 LastName3 单元格。

因此,在继续之前,我们必须在网格主体内部实现一种新的行为:如果当前单元格尚未验证(即提交),我们不能让用户导航到另一个单元格。

取消导航

引言

我们已经实现了一种在编辑过程中验证网格单元格的方法,并且我们还实现了一种在单元格值不正确时取消单元格提交过程的方法。然而,仅仅取消提交过程是不够的。我们还需要能够取消导航过程。

幸运的是,我们的代码管理着单元格之间的导航,因此我们能够在必要时添加条件来阻止导航。

当用户点击单元格时取消

为了在用户点击单元格时取消导航,我们需要修改 Cell 类的 OnMouseLeftButtonDown 方法。

protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
    base.OnMouseLeftButtonDown(e);
    object currentFocusedElement = FocusManager.GetFocusedElement();
    if (!TreeHelper.IsChildOf(this, currentFocusedElement as DependencyObject))
    {
        HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
        if (parentContainer != null)
        {
            if (CanNavigateOnMouseDown(e.OriginalSource as DependencyObject))
                this.Focus();
            else
                e.Handled = true;
                //in that case we do not want that the event goes further
        }
    }
}

protected bool CanNavigateOnMouseDown(DependencyObject clickSource)
{
    HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
    if (parentContainer != null)
    {
        bool canNavigate = true;
        if (parentContainer.IsCurrentCellInEditMode)
        {
            if (!TreeHelper.IsChildOf(parentContainer.CurrentEditedCell, clickSource))
                canNavigate = parentContainer.CommitEdit(false);
        }

        return canNavigate;
    }

    return false;
}

我们添加了一个 CanNavigateOnMouseDown 方法。如果当前单元格正在被编辑,而用户点击了另一个单元格,我们会强制当前单元格提交。只有当提交过程运行到最后(返回 true)时,我们才允许导航。

让我们试试我们的改动。

  • 启动应用程序
  • 编辑 FirstName3 单元格。
  • 删除单元格内容。
  • 通过点击导航到 LastName3 单元格。

这次导航被取消了,FirstName3 单元格仍然是当前单元格。

当用户点击 CheckBoxCell 时取消

让我们测试一下当我们点击一个 CheckBoxCell 时的导航取消功能。

  • CheckBoxCell 类的 MouseDownBeginEdit 方法的开头添加一个断点。
  • 启动应用程序
  • 编辑 FirstName3 单元格。
  • 删除单元格内容。
  • 通过点击导航到一个 CheckBoxCell 单元格。

在这种情况下,网格工作不正常:导航被取消了,但是 MouseDownBeginEdit 方法被调用了(我们可以通过我们添加的断点看到这一点)。

这是因为我们处理 CheckBoxCellOnMouseLeftButtonDown 方法的方式是,一旦用户点击单元格,编辑过程就开始了。

让我们改进我们的 CheckBoxCell 类的 OnMouseLeftDown 方法。

protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
    base.OnMouseLeftButtonDown(e);

    if ((this.CellState == CellState.Focused) || (this.CellState == CellState.Edited))
    {
        BeginEdit();
        if (this.CellState == CellState.Edited)
            EditedValue = !EditedValue;
    }
    else if (CanNavigateOnMouseDown(e.OriginalSource as DependencyObject) && 
             IsTabStop && CanEdit)
        Dispatcher.BeginInvoke(new Action(MouseDownBeginEdit));
}

当 FocusCell 方法被调用时取消

在本教程的第 1 步中,我们通过添加一个 FocusCell 方法来增强了 ContainerItem 类。这个方法在内部使用,并且可以从外部用于以编程方式导航到一个单元格。

我们需要增强这个方法,以便在当前单元格无法提交时“它不起作用”。

public bool FocusCell(string cellName)
{
    HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
    //The following condition is not perfectly correct. It will be enhanced later on
    if ((parentContainer != null) && 
        parentContainer.IsCurrentCellInEditMode && 
        (parentContainer.CurrentEditedCell.Name != cellName))
    {
        bool canNavigate = parentContainer.CommitEdit(false);

        if (!canNavigate)
            return false;
    }

    object focusedElement = FocusManager.GetFocusedElement();
    FrameworkElement firstChild = GetFirstTreeChild() as FrameworkElement;
    if (firstChild != null)
    {
        Cell cell = firstChild.FindName(cellName) as Cell;
        if (cell != null)
        {
            if (cell.Focus())
            {
                if (!TreeHelper.IsChildOf(this, focusedElement as DependencyObject))
                {
                    if (parentContainer != null)
                        parentContainer._OnNavigatorSetKeyboardFocus(this);
                }
                return true;
            }
        }
    }

    return false;
}

点击项时取消

在第1步中,我们允许一个项在被点击时获得焦点。在我们的示例中,这可能发生,例如,如果用户恰好点击了两个项之间的线上(即两行之间)。

然而,如果当前单元格无法提交,我们不能让一个项目获得焦点。

我们必须在我们的 ContainerItem 部分类中重写 OnMouseLeftButtonDown 方法来避免这种情况。

protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
    bool canNavigate = true;
    HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
    if (parentContainer != null)
    {
        if (!TreeHelper.IsChildOf(parentContainer.CurrentDirtyCell, 
                                  e.OriginalSource as DependencyObject))
            canNavigate = parentContainer.CommitEdit(false);
    }

    if (canNavigate)
        base.OnMouseLeftButtonDown(e);
    else
        e.Handled = true;
}

取消 GridSpatialNavigator 的导航

首先,让我们向我们的 GridSpatialNavigator 添加一个 ValidateCell 方法。

protected static bool ValidateCell(IKeyNavigatorContainer container)
{
    HandyContainer parentContainer = 
      HandyContainer.GetParentContainer((FrameworkElement)container);
    return parentContainer.CommitEdit(true);
}

如果当前单元格无法提交,此方法将返回 false

现在让我们修改 KeyDownActiveKeyDown 方法。如果用户按下了导航键(Tab, Up, Down),我们将首先调用 ValidateCell 方法来检查当前单元格是否可以提交。只有当 ValidateCell 方法返回 true 时,才会调用祖先 SpatialNavigatorKeyDownActiveKeyDown 方法。

public override void ActiveKeyDown(IKeyNavigatorContainer container, GKeyEventArgs e)
{
    LastKeyProcessed = e.Key;
    LastModifier = Keyboard.Modifiers;
    if ((((e.Key != Key.Home) && (e.Key != Key.End)) ||
        ((Keyboard.Modifiers & ModifierKeys.Control) != ModifierKeys.Control)) &&
        (e.Key != Key.Tab))
    {
        if (e.Key == Key.Enter)
            e.Key = Key.Down;

        LastKeyProcessed = e.Key;
        switch (e.Key)
        {
            case Key.Down:
            case Key.Up:
            case Key.PageDown:
            case Key.PageUp:
            case Key.Enter:
            case Key.Home:
            case Key.End:
            case Key.Right:
            case Key.Left:
            case Key.Tab:
                if (ValidateCell(container))
                    base.ActiveKeyDown(container, e);
                else
                    e.Handled = true;
                break;

            default:
                base.ActiveKeyDown(container, e);
                break;
        }

    }
    else
        ProcessKey(container, e);
}

public override void KeyDown(IKeyNavigatorContainer container, GKeyEventArgs e)
{
    LastKeyProcessed = e.Key;
    LastModifier = Keyboard.Modifiers;
    if ((((e.Key != Key.Home) && (e.Key != Key.End)) ||
        ((Keyboard.Modifiers & ModifierKeys.Control) != ModifierKeys.Control)) &&
        (e.Key != Key.Tab))
    {
        if (e.Key == Key.Enter)
            e.Key = Key.Down;

        LastKeyProcessed = e.Key;

        switch (e.Key)
        {
            case Key.Down:
            case Key.Up:
            case Key.PageDown:
            case Key.PageUp:
            case Key.Enter:
            case Key.Home:
            case Key.End:
            case Key.Right:
            case Key.Left:
            case Key.Tab:
                if (ValidateCell(container))
                    base.KeyDown(container, e);
                else
                    e.Handled = true;
                break;

            default:
                base.KeyDown(container, e);
                break;
        }
    }
    else
        ProcessKey(container, e);
}

我们还必须以同样的方式修改 ProcessKey 方法。

private void ProcessKey(IKeyNavigatorContainer container, GKeyEventArgs e)
{
    if (!ValidateCell(container))
    {
        e.Handled = true;
        return;
    }

    GStackPanel gStackPanel = (GStackPanel)container;
    HandyContainer parentContainer = HandyContainer.GetParentContainer(gStackPanel);

    if (gStackPanel.Children.Count > 0)
    {
        ...

取消 RowSpatialNavigator 的导航

让我们向我们的 RowSpatialNavigator 添加与我们向 GridSpatialNavigator 添加的相同的 ValidateCell 方法。(如果您认为两次添加相同的方法很奇怪,那么您想对了。GridSpatialNavigatorValidateCell 方法稍后将被修改。)

protected static bool ValidateCell(IKeyNavigatorContainer container)
{
    HandyContainer parentContainer = 
       HandyContainer.GetParentContainer((FrameworkElement)container);
    return parentContainer.CommitEdit(true);
}

让我们也以与在我们的 GridSpatialNavigator 中修改的方式相同地修改 RowSpatialNavigatorKeyDownActiveKeyDown 方法。

public override void ActiveKeyDown(IKeyNavigatorContainer container, GKeyEventArgs e)
{
    if (((e.Key != Key.Home) && (e.Key != Key.End)) ||
        ((Keyboard.Modifiers & ModifierKeys.Control) != ModifierKeys.Control))
    {
        switch (e.Key)
        {
            case Key.Down:
            case Key.Up:
            case Key.PageDown:
            case Key.PageUp:
            case Key.Enter:
            case Key.Home:
            case Key.End:
            case Key.Right:
            case Key.Left:
            case Key.Tab:
                if (ValidateCell(container))
                    base.ActiveKeyDown(container, e);
                else
                    e.Handled = true;
                break;

            default:
                base.ActiveKeyDown(container, e);
                break;
        }
    }
}

public override void KeyDown(IKeyNavigatorContainer container, GKeyEventArgs e)
{
    if (((e.Key != Key.Home) && (e.Key != Key.End)) ||
        ((Keyboard.Modifiers & ModifierKeys.Control) != ModifierKeys.Control))
    {
        switch (e.Key)
        {
            case Key.Down:
            case Key.Up:
            case Key.PageDown:
            case Key.PageUp:
            case Key.Enter:
            case Key.Home:
            case Key.End:
            case Key.Right:
            case Key.Left:
            case Key.Tab:
                if (ValidateCell(container))
                    base.KeyDown(container, e);
                else
                    e.Handled = true;
                break;

            default:
                base.KeyDown(container, e);
                break;
        }
    }
}

测试导航取消

让我们再试一次,看看我们的更改是否有效。

  • 启动应用程序
  • 编辑 FirstName3 单元格。
  • 删除单元格内容。
  • 尝试通过以下方式导航到另一个单元格:
    • 点击它
    • 按上、下、左、右、Home 或 End 键
    • 按 Tab 或 Shift-Tab 键。
    • 按 Ctrl-Home 或 Ctrl-End 键。

在所有这些情况下,FirstName3 单元格仍然是当前单元格。

取消网格滚动

目前,当一个单元格是“脏”的时候,用户可以使用垂直滚动条来滚动网格。这种行为可能会产生不希望的后果。例如,用户可以用一个错误的值编辑一个单元格,然后将网格滚动到一个被编辑的单元格不可见的位置,再点击另一个单元格。在这种情况下,验证过程将会在一个不再可见的单元格上启动。

与其实现冗长而困难的算法来处理像前面描述的那样的情况,例如,将网格滚动回被编辑单元格可见的位置,我们将用简单的方式来处理它:如果一个单元格不能被提交,用户就不能滚动网格。

这可以通过重写我们的 HandyContainer 部分类的 OnVerticalOffsetChanging 方法非常容易地实现。

protected override void OnVerticalOffsetChanging(CancelOffsetEventArgs e)
{
    if (!CommitEdit())
        e.Cancel = true;

    base.OnVerticalOffsetChanging(e);
}

取消节点展开和折叠

当一个单元格被编辑且无法提交时,我们取消了网格的滚动,以避免被编辑的单元格被移动到未显示区域。

当展开一个显示在被编辑单元格上方的节点时,我们会让该节点的子项出现在该节点和被编辑单元格之间。在这种情况下,被编辑单元格的位置可能会被移动到网格的未显示区域。

当折叠一个包含被编辑单元格的项的父节点(如果有的话)时,我们会让该项以及因此被编辑的单元格消失。

由于这些原因,如果当前单元格正在被编辑并且无法提交,我们将不允许折叠或展开节点。

让我们重写 ContainerItem 类的 OnIsExpandedChanging 方法。

protected override void OnIsExpandedChanging(
          Netika.Windows.Controls.IsExpandedChangingEventArgs e)
{
    HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
    if ((parentContainer != null) && (!parentContainer.CommitEdit()))
        e.Cancel = true;
    else
        base.OnIsExpandedChanging(e);            
}

提醒用户

引言

当一个单元格无法提交时,它会保持编辑状态。然而,用户可能会对这种行为感到困惑,并想知道为什么他不能导航到另一个单元格。为了让用户明白他输入的数值是错误的,让我们在单元格无法提交时将其边框变为红色。

ValidStates

为了能够对无效的单元格应用视觉外观,我们将在单元格的模板中添加一个“ValidStatesVisualStateGroup。该组将有两个可能的状态:“Valid”和“NotValid”。如果单元格处于 NotValid 状态,将显示一个警告用户的视觉元素。

让我们在 TextCell 样式中实现这一更改。“ValidStatesVisualStateGroup 被添加到 VisualStateManager.VisualStateGroups 的末尾。警告用户的视觉元素是一个名为 ValidElement 的红色矩形。

<Style TargetType="o:TextCell">
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="BorderBrush" Value="{StaticResource DefaultListControlStroke}"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="Foreground" Value="{StaticResource DefaultForeground}"/>
    <Setter Property="HorizontalContentAlignment" Value="Stretch" />
    <Setter Property="VerticalContentAlignment" Value="Stretch" />
    <Setter Property="Cursor" Value="Arrow" />
    <Setter Property="Padding" Value="2,2,1,1" />
    <Setter Property="Width" Value="100"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="o:TextCell">
                <Grid>
                    <vsm:VisualStateManager.VisualStateGroups>
                        <vsm:VisualStateGroup x:Name="CommonStates">
                            <vsm:VisualState x:Name="Standard"/>
                            <vsm:VisualState x:Name="Focused">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                            Storyboard.TargetName="FocusElement" 
                                            Storyboard.TargetProperty="Visibility" 
                                            Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                            <vsm:VisualState x:Name="Edited">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                             Storyboard.TargetName="TextBoxElement" 
                                             Storyboard.TargetProperty="Visibility" 
                                             Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                             Storyboard.TargetName="TextElement" 
                                             Storyboard.TargetProperty="Visibility" 
                                             Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Collapsed</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                            Storyboard.TargetName="FocusElement" 
                                            Storyboard.TargetProperty="Visibility" 
                                            Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="ValidStates">
                            <vsm:VisualState x:Name="Valid"/>
                            <vsm:VisualState x:Name="NotValid">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                            Storyboard.TargetName="ValidElement" 
                                            Storyboard.TargetProperty="Visibility" 
                                            Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                    </vsm:VisualStateManager.VisualStateGroups>
                    <Rectangle 
                        Name="ValidElement" 
                        Stroke="Red" 
                        StrokeThickness="2"
                        IsHitTestVisible="false"
                        Margin="0,1,1,0"
                        Visibility="Collapsed"/>
                    <Grid>
                        <TextBlock 
                            x:Name="TextElement" 
                            Text="{TemplateBinding Text}"
                            Margin="{TemplateBinding Padding}"
                            HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                            VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                        <TextBox 
                            x:Name="TextBoxElement"
                            Style="{StaticResource CellTextBoxStyle}"
                            Visibility="Collapsed"
                            Text="{TemplateBinding Text}"
                            Margin="{TemplateBinding Padding}"
                            HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                            VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                            Foreground="{TemplateBinding Foreground}"/>
                    </Grid>
                    <Rectangle Name="FocusElement" 
                               Stroke="{StaticResource DefaultFocus}" 
                               StrokeThickness="1" 
                               IsHitTestVisible="false" 
                               StrokeDashCap="Round" 
                               Margin="0,1,1,0" 
                               StrokeDashArray=".2 2" 
                               Visibility="Collapsed" />
                    <Rectangle Name="CellRightBorder" 
                               Stroke="{TemplateBinding BorderBrush}" 
                               StrokeThickness="0.5" 
                               Width="1" 
                               HorizontalAlignment="Right"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

让我们在 CheckBoxCell 样式中实现同样的更改。

<Style TargetType="o:CheckBoxCell">
    <Setter Property="Background" Value="Transparent" />
    <Setter Property="BorderBrush" 
               Value="{StaticResource DefaultListControlStroke}"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="Foreground" 
               Value="{StaticResource DefaultForeground}"/>
    <Setter Property="Cursor" Value="Arrow" />
    <Setter Property="Width" Value="20"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="o:CheckBoxCell">
                <Grid Background="Transparent">
                    <vsm:VisualStateManager.VisualStateGroups>
                        <vsm:VisualStateGroup x:Name="CommonStates">
                            <vsm:VisualState x:Name="Standard"/>
                            <vsm:VisualState x:Name="Focused">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="focusElement" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                            <vsm:VisualState x:Name="Edited">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="focusElement" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="ValidStates">
                            <vsm:VisualState x:Name="Valid"/>
                            <vsm:VisualState x:Name="NotValid">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="ValidElement" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                    </vsm:VisualStateManager.VisualStateGroups>
                    <Rectangle 
                        x:Name="ShadowVisual" 
                        Fill="{StaticResource DefaultShadow}" 
                        Height="12" 
                        Width="12" 
                        RadiusX="2" 
                        RadiusY="2" 
                        Margin="1,1,-1,-1"/>
                    <Rectangle Name="ValidElement" 
                               Stroke="Red" 
                               StrokeThickness="2"
                               Margin="0,1,1,0" 
                               IsHitTestVisible="false"
                               Visibility="Collapsed"/>
                    <Border 
                        x:Name="BackgroundVisual" 
                        Background="{TemplateBinding Background}" 
                        Height="12" 
                        Width="12" 
                        BorderBrush="{TemplateBinding BorderBrush}" 
                        CornerRadius="2" 
                        BorderThickness="{TemplateBinding BorderThickness}"/>
                    <Grid 
                        x:Name="CheckMark" 
                        Width="8" 
                        Height="8" 
                        Visibility="{TemplateBinding CheckMarkVisibility}" >
                        <Path 
                            Stretch="Fill" 
                            Stroke="{TemplateBinding Foreground}" 
                            StrokeThickness="2" 
                            Data="M129.13295,140.87834 L132.875,145 L139.0639,137" />
                    </Grid>
                    <Rectangle 
                        x:Name="ReflectVisual" 
                        Fill="{StaticResource DefaultReflectVertical}" 
                        Height="5" 
                        Width="10" 
                        Margin="1,1,1,6" 
                        RadiusX="2" 
                        RadiusY="2"/>
                    <Rectangle 
                        Name="focusElement" 
                        Stroke="{StaticResource DefaultFocus}" 
                        StrokeThickness="1" 
                        Fill="{TemplateBinding Background}" 
                        IsHitTestVisible="false" 
                        StrokeDashCap="Round" 
                        Margin="0,1,1,0" 
                        StrokeDashArray=".2 2" 
                        Visibility="Collapsed" />
                    <Rectangle 
                        Name="CellRightBorder" 
                        Stroke="{TemplateBinding BorderBrush}" 
                        StrokeThickness="0.5" 
                        Width="1" 
                        HorizontalAlignment="Right"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

IsValid 单元格属性

我们仍然需要适当地从 Valid 状态切换到 NotValid 状态。让我们向 Cell 类添加一个 IsValid 属性。

private bool isValid = true;
public bool IsValid
{
    get { return isValid; }
    internal set
    {
        if (isValid != value)
        {
            isValid = value;
            if (isValid)
                VisualStateManager.GoToState(this, "Valid", true);
            else
                VisualStateManager.GoToState(this, "NotValid", true);
        }
    }
}

IsValid 方法必须在 CancelEditCommitEdit 方法中设置。

internal virtual bool CancelEdit(bool keepFocus)
{
    if (this.CellState != CellState.Edited)
        return true;

    if (IsDirty)
    {
        OnCancelEdit();

        IsValid = true;  
        IsDirty = false;
    }            
    . . .
internal bool CommitEdit(bool keepFocus)
{
    if (this.CellState != CellState.Edited)
        return true;

    if (IsDirty)
    {
        HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
        if (parentContainer != null)
        {
            CellValidatingEventArgs cancelEventArgs = 
              new CellValidatingEventArgs(this.CurrentValue, this.DirtyValue, false);
            parentContainer._OnCurrentCellValidating(cancelEventArgs);
            if (cancelEventArgs.Cancel)
            {
                IsValid = false;
                return false;
            }

            OnCommitEdit();

            IsValid = true; 
            IsDirty = false;                                       
        }
    }

    ...

让我们试试我们的改动。

  • 启动应用程序
  • 编辑 FirstName3 单元格。
  • 删除单元格内容。
  • 尝试导航到另一个单元格。
  • 保持应用程序运行。

这一次,FirstName3 的边框显示为红色。

  • 在 FirstName3 单元格中输入一些内容。
  • 导航到另一个单元格。

单元格的背景显示正常。

使用绑定验证

引言

Silverlight 已经提供了一种在绑定过程中调用的验证机制。

为了让它工作,让我们修改 LastName 单元格上的绑定(在 GridBody 项目的 Page.xaml 文件中进行修改)。

<o:TextCell 
    Text="{Binding LastName, Mode=TwoWay, 
          NotifyOnValidationError=true, ValidatesOnExceptions=true}" 
    x:Name="LastName"/>

NotifyOnValidationError 属性将告诉绑定在发生错误时调用单元格上的 BindingValidationError 事件(详见下文以了解更多信息)(这比这稍微复杂一些,因为该事件是一个路由事件;如果您想了解更多关于绑定验证过程的信息,请阅读 Silverlight 文档)。ValidateOnExceptions 属性将告诉绑定,如果在绑定过程中抛出异常,它必须将此异常作为验证错误处理。

让我们修改我们 Person 类的 LastName 属性,以便如果我们尝试用 null 或空字符串填充它时,它会抛出异常。

private string lastName;
public string LastName
{
    get { return lastName; }
    set
    {
        if (lastName != value)
        {
            if (string.IsNullOrEmpty(value))
                throw new Exception("Last name cannot be empty");

            lastName = value;
            OnPropertyChanged("LastName");
        }
    }
}

最后,让我们在我们的单元格类中处理 BindingValidationError 事件。

public Cell()
{
    this.BindingValidationError += 
      new EventHandler<ValidationErrorEventArgs>(Cell_BindingValidationError);
}

private void Cell_BindingValidationError(object sender, ValidationErrorEventArgs e)
{
    if (e.Action == ValidationErrorEventAction.Added)
        MessageBox.Show("Invalid Data");
    else if (e.Action == ValidationErrorEventAction.Removed)
        MessageBox.Show("Valid Data");
}

让我们试试我们的改动。

  • 启动应用程序
  • 编辑 LastName3 单元格
  • 删除单元格内容。
  • 导航到另一个单元格。

显示一个“无效数据”消息框。

现在我们已经体验了 Silverlight 内置的绑定验证过程,我们需要正确处理 BindingValidationError,使其适应我们的验证流程。

正确处理 BindingValidationError 事件

首先,让我们更改我们的 Cell_BindingValidationError 方法,以便它设置 isBindingValid 标志。

private bool isBindingValid = true;
private void Cell_BindingValidationError(object sender, 
                         ValidationErrorEventArgs e)
{
    if (e.Action == ValidationErrorEventAction.Added)
        isBindingValid = false;
    else if (e.Action == ValidationErrorEventAction.Removed)
        isBindingValid = true;
}

接下来,让我们修改我们的 CommitEdit 方法,以便考虑 isBindingValid 标志值。

internal bool CommitEdit(bool keepFocus)
{
    if (this.CellState != CellState.Edited)
        return true;

    if (IsDirty)
    {
        HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
        if (parentContainer != null)
        {
            CellValidatingEventArgs cancelEventArgs = 
              new CellValidatingEventArgs(this.CurrentValue, this.DirtyValue, false);
            parentContainer._OnCurrentCellValidating(cancelEventArgs);
            if (cancelEventArgs.Cancel)
            {
                IsValid = false;
                return false;
            }

            OnCommitEdit();

            if (!isBindingValid)
            {
                IsValid = false;
                return false;
            }

            IsValid = true; 
            IsDirty = false;                                       
        }
    }
    . . .

请注意,我们在调用 OnCommitEdit 方法之后处理了 isBindingValid 的值。实际上,BindingValidationError 事件将在我们尝试将用户输入的值应用到单元格绑定的属性之后被调用。因此,在检查 isBindingValid 标志之前,必须先调用 OnCommitEdit 方法。

如果我们现在尝试这个应用程序,LastName 单元格的验证过程会正确执行。

  • 启动应用程序
  • 编辑 LastName3 单元格
  • 删除单元格内容。
  • 导航到另一个单元格。

LastName3 单元格保持编辑状态,并且它仍然是当前单元格。单元格的边框显示为红色。

CurrentCellValidated 事件

引言

当一个单元格被提交时,至少会启动两个验证操作:OnCurrentCellValidating 事件和绑定验证。

我们需要一个事件来知道验证操作何时完成,以便能够开始发布验证操作。这就是 CurrentCellValidated 事件的目的。

实现 CurrentCellValidated 事件

currentCellValidated 事件将在我们的 HandyContainer 部分类中以与其他事件相同的方式实现。

internal void _OnCurrentCellValidated(EventArgs e)
{
    OnCurrentCellValidated(e);
}

public event EventHandler CurrentCellValidated;
protected virtual void OnCurrentCellValidated(EventArgs e)
{
    if (CurrentCellValidated != null)
        CurrentCellValidated(this, e);
}

调用 _OnCurrentCellValidated 方法

此方法必须在验证处理后,在 Cell 类的 CommitEdit 方法中调用。

internal bool CommitEdit(bool keepFocus)
{
    if (this.CellState != CellState.Edited)
        return true;

    if (IsDirty)
    {
        HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
        if (parentContainer != null)
        {
            CellValidatingEventArgs cancelEventArgs = 
              new CellValidatingEventArgs(this.CurrentValue, this.DirtyValue, false);
            parentContainer._OnCurrentCellValidating(cancelEventArgs);
            if (cancelEventArgs.Cancel)
            {
                IsValid = false;
                return false;
            }

            OnCommitEdit();

            if (!isBindingValid)
            {
                IsValid = false;
                return false;
            }

            IsValid = true; 
            IsDirty = false;

            parentContainer._OnCurrentCellValidated(EventArgs.Empty);
        }
    }
    . . .

5. 项的验证

引言

了解单元格的状态以及它们何时改变是不够的。每个单元格都是一个整体的一部分:即项(也就是一行)。一个单元格的修改可能会对整个项产生影响。因此,了解一个项何时被修改并能够对该项采取适当的行动是很重要的。

IsDirty 项

引言

如果一个项中至少有一个单元格的值被改变了,我们称之为“脏”项。一旦该项被验证(见下文),该项就不再是脏的了。

HandyContainer IsCurrentItemDirty 属性

我们将以实现 IsCurrentCellDirty 属性相同的方式实现 IsCurrentItemDirty 属性。

public bool IsCurrentItemDirty
{
    get { return CurrentDirtyItem != null; }

}

private ContainerItem currentDirtyItem;
internal ContainerItem CurrentDirtyItem
{
    get { return currentDirtyItem; }
    set
    {
        if (currentDirtyItem != value)
        {
            currentDirtyItem = value;
            OnIsCurrentItemDirtyChanged(EventArgs.Empty);
        }

    }
}

public event EventHandler IsCurrentItemDirtyChanged;
protected virtual void OnIsCurrentItemDirtyChanged(EventArgs e)
{
    if (IsCurrentItemDirtyChanged != null)
        IsCurrentItemDirtyChanged(this, e);
}

我们实现了一个内部的 CurrentDirtyItem 属性。如果当前项是“脏”的,这个属性将被填充。如果没有当前项或者它不是“脏”的,该属性的值将为 null

IsCurrentItemDirty 的值是根据 CurrentDirtyItem 的值计算得出的。

我们还添加了一个 IsCurrentItemDirtyChanged 事件。

IsDirty 项的属性

ContainerItemIsDirty 属性不仅仅是一个标志。

我们必须考虑以下规则:

  • 如果一个单元格变脏,持有该单元格的项(即行)也变脏。
  • 如果一个单元格被提交,它就不再是脏的了,但是持有该单元格的项会一直保持脏的状态,直到它被验证。
  • 如果一个单元格被取消,它就不再是脏的。如果持有该单元格的项在单元格被编辑之前已经是脏的,那么该项仍然是脏的。如果持有该单元格的项在单元格被编辑之前不是脏的,那么该项就不再是脏的了。

为了能够实现这些规则,我们将向 ContainerItem 类添加一个 IsDirtyCount 属性。一旦该项的某个单元格变脏,ContainerItemIsDirtyCount 属性就会加一。如果某个单元格的编辑被取消,IsDirtyCount 属性就会减一。如果 IsDirtyCount 属性值大于0,则 ContainerItem 为脏。

ContainerItem 被验证时(见下文),IsDirtyCount 属性被重置为 0。

private int isDirtyCount;
internal int IsDirtyCount
{
    get { return isDirtyCount; }
    set
    {
        if (isDirtyCount != value)
        {
            isDirtyCount = value;

            HandyContainer parentContainer = 
                 HandyContainer.GetParentContainer(this);
            if (parentContainer != null)
            {
                if (isDirtyCount > 0)
                    parentContainer.CurrentDirtyItem = this;
                else
                    parentContainer.CurrentDirtyItem = null;
            }
        }
    }
}

public bool IsDirty
{
    get { return IsDirtyCount > 0; }
}

从 Cell 类更新 IsDirtyCount 属性

由于我们需要从一个单元格访问父 ContainerItem,让我们在 ContainerItem 类中创建一个静态的 GetParentContainerItem 方法,就像我们在 HandyContainer 类中创建了一个静态的 GetParentContainer 方法一样。

public static ContainerItem GetParentContainerItem(FrameworkElement element)
{
    DependencyObject parentElement = element;
    while (parentElement != null)
    {
        ContainerItem parentContainerItem = parentElement as ContainerItem;
        if (parentContainerItem != null)
            return parentContainerItem;

        parentElement = VisualTreeHelper.GetParent(parentElement);
    }

    return null;
}

接下来要做的是在单元格变脏时,增加其父 ContainerItemIsDirtyCount。让我们修改 Cell 类的 IsDirty 属性的实现。

public bool IsDirty
{
    get { return isDirty; }
    internal set
    {
        if (isDirty != value)
        {
            isDirty = value;
            HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
            if (parentContainer != null)
            {
                if (isDirty)
                    parentContainer.CurrentDirtyCell = this;
                else
                    parentContainer.CurrentDirtyCell = null;
            }

            if (isDirty)
            {
                ContainerItem parentRow = ContainerItem.GetParentContainerItem(this);
                if (parentRow != null)
                    parentRow.IsDirtyCount++;
            }
        }
    }
}

接下来,当单元格的 CancelEdit 方法被调用时,我们必须减少父 ContainerItemIsDirtyCount 值。

internal virtual bool CancelEdit(bool keepFocus)
{
    if (this.CellState != CellState.Edited)
        return true;

    if (IsDirty)
    {
        OnCancelEdit();

        IsValid = true;  
        IsDirty = false;
        ContainerItem parentRow = ContainerItem.GetParentContainerItem(this);
        if (parentRow != null)
            parentRow.IsDirtyCount--;
    }            

    if (this.CellState == CellState.Edited)
    {
        ...

已编辑的项

引言

如果一个项的其中一个单元格正在被编辑,我们称之为“已编辑的项”。

HandyContainer IsCurrentItemEdited 属性

首先,让我们向我们的 HandyContainer 类添加一个 IsCurrentItemEdited 属性。像往常一样,我们还将实现一个 CurrentEditedItem 属性和一个 IsCurrentItemEditedChanged 事件。

public bool IsCurrentItemEdited
{
    get { return CurrentEditedItem != null; }

}

private ContainerItem currentEditedItem;
internal ContainerItem CurrentEditedItem
{
    get { return currentEditedItem; }
    set
    {
        if (currentEditedItem != value)
        {
            currentEditedItem = value;
            OnIsCurrentItemEditedChanged(EventArgs.Empty);
        }

    }
}

public event EventHandler IsCurrentItemEditedChanged;
protected virtual void OnIsCurrentItemEditedChanged(EventArgs e)
{
    if (IsCurrentItemEditedChanged != null)
        IsCurrentItemEditedChanged(this, e);
}

更新 CurrentEditedItem 属性

让我们修改 HandyContainer_OnCurrentCellBeginEdited_OnCurrentCellEndEdit 方法,以保持 CurrentEditedItem 属性值的最新状态。

internal void _OnCurrentCellBeginEdited(EventArgs e)
{
    CurrentEditedCell = this.GetCurrentCell();
    CurrentEditedItem = ContainerItem.GetParentContainerItem(CurrentEditedCell);
    OnCurrentCellBeginEdited(e);
}
internal void _OnCurrentCellEndEdit(EventArgs e)
{
    CurrentEditedCell = null;
    CurrentEditedItem = null;
    OnCurrentCellEndEdit(e);
}

验证 ContainerItem

引言

当一个项是“脏”的时候,我们必须能够验证它。项的验证可以自动发生。例如,当用户点击另一个项时,在另一个项成为当前项之前,当前项会被验证。当前项的验证过程也可以通过在 HandyContainer 上调用一个 Validate 方法来强制执行。当然,验证过程的目的是能够采取行动并能够在此过程中取消导航。因此,我们还必须添加验证事件。

CurrentItemValidating 和 CurrentItemValidated 事件

就像我们向 HandyContainer 类添加了 CurrentCellValidatingCurrentCellValidated 事件一样,我们也需要向该类添加 CurrentItemValidatingCurrentItemValidated 事件。CurrentItemValidating 事件可以被取消。在这种情况下,验证过程会停止,并且导航过程(如果有的话)也会被取消。

HandyContainer 中实现这些事件遵循与其他事件实现相同的模式。

首先,让我们向我们的 GoaOpen\Extensions\Grid 文件夹添加一个 ItemValidatingEventArgs 类。

using Netika.Windows.Controls;

namespace Open.Windows.Controls
{
    public class ItemValidatingEventArgs : GCancelEventArgs
    {
        public ItemValidatingEventArgs(ContainerItem item, bool cancel)
            : base(cancel)
        {
            Item = item;
        }

        public ContainerItem Item
        {
            get;
            private set;
        }
    }
}

然后,让我们修改我们的 HandyContainer 部分类,并添加 CurrentItemValidatingCurrentItemValidated 事件。

internal void _OnCurrentItemValidating(ItemValidatingEventArgs e)
{
    OnCurrentItemValidating(e);
}

public event EventHandler<ItemValidatingEventArgs> CurrentItemValidating;
protected virtual void OnCurrentItemValidating(ItemValidatingEventArgs e)
{
    if (CurrentItemValidating != null)
        CurrentItemValidating(this, e);
}
internal void _OnCurrentItemValidated(EventArgs e)
{
    OnCurrentItemValidated(e);
}

public event EventHandler CurrentItemValidated;
protected virtual void OnCurrentItemValidated(EventArgs e)
{
    if (CurrentItemValidated != null)
        CurrentItemValidated(this, e);
}

_OnCurrentItemValidating_OnCurrentItemValidated 方法将从 ContainerItem 类中调用。

ContainerItem Validate 方法

ContainerItemValidate 方法是每次需要验证一个项时都会被调用的方法。

ContainerItemValidate 方法必须扩展 CellCommitEdit 方法的行为。如果 ContainerItem 的一个单元格被编辑,调用 ContainerItemValidate 方法必须对该单元格产生与直接调用该单元格的 CommitEdit 方法相同的结果。因此,即使该项不是脏的,如果该项持有一个被编辑的单元格,也必须调用该单元格的 CommitEdit 方法。

internal bool Validate(bool keepFocus)
{
    HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
    if (parentContainer != null)
    {
        if (parentContainer.IsCurrentCellInEditMode && 
              !parentContainer.CommitEdit(keepFocus))
            return false;

        if (!IsDirty)
            return true;

        ItemValidatingEventArgs eventArgs = new ItemValidatingEventArgs(this, false);
        parentContainer._OnCurrentItemValidating(eventArgs);
        if (eventArgs.Cancel)
            return false;

        IsDirtyCount = 0;

        parentContainer._OnCurrentItemValidated(EventArgs.Empty);
    }

    return true;
}

HandyContainer Validate 方法

就像我们有一个 CommitEdit 方法来“验证”HandyContainer 的当前单元格一样,我们也必须有一个 Validate 方法来验证当前项。

请注意,只有在当前项被编辑或是脏数据时,才应调用 Validate 方法。因此,让我们首先向 HandyContainer 添加 IsCurrentItemEditedOrDirtyCurrentEditedOrDirtyItem 属性。

internal bool IsCurrentItemEditedOrDirty
{
    get { return IsCurrentItemEdited || IsCurrentItemDirty; }
}

internal ContainerItem CurrentEditedOrDirtyItem
{
    get { return CurrentDirtyItem ?? CurrentEditedItem; }
}

然后,让我们实现 Validate 方法。

public bool Validate()
{
    return Validate(true);
}

public bool Validate(bool keepFocus)
{
    if (CurrentEditedOrDirtyItem != null)
        return CurrentEditedOrDirtyItem.Validate(keepFocus);

    return true;
}

取消导航过程

引言

在本教程的前面部分,我们在几个地方修改了一些方法,以确保在允许用户导航到网格的另一部分之前,当前项可以被提交。

我们现在必须做同样的修改,以考虑在允许导航之前当前项是否可以被验证。

当用户点击单元格时取消

我们必须修改 Cell 类的 OnMouseLeftButtonDown 方法,以便如果当前项被编辑或者它是脏数据:

  • 如果用户点击了由另一个项目持有的单元格,我们在允许导航处理之前验证当前项目。
  • 如果用户点击了由当前项持有的单元格,我们在允许导航处理之前提交当前单元格。
  • 如果用户点击了当前单元格,我们允许导航处理。

如果当前项未被编辑或不是脏数据,我们允许导航过程,无需任何验证。

protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
    base.OnMouseLeftButtonDown(e);

    object currentFocusedElement = FocusManager.GetFocusedElement();
    if (!TreeHelper.IsChildOf(this, currentFocusedElement as DependencyObject))
    {
        HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
        if (parentContainer != null)
        {
            if (CanNavigateOnMouseDown(e.OriginalSource as DependencyObject))
                this.Focus();
            else
                e.Handled = true;
                //in that case we do not want that the event goes further
        }
    }
}

protected bool CanNavigateOnMouseDown(DependencyObject clickSource)
{
    HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
    if (parentContainer != null)
    {
        bool canNavigate = true;
        if (parentContainer.IsCurrentItemEditedOrDirty)
        {
            bool isEditedSameItem = parentContainer.CurrentEditedOrDirtyItem == 
                                      ContainerItem.GetParentContainerItem(this);
            if (isEditedSameItem)
            {
                if (!TreeHelper.IsChildOf(parentContainer.CurrentEditedCell, clickSource))
                    canNavigate = parentContainer.CommitEdit(false);
            }
            else
                canNavigate = parentContainer.Validate(false);
        }

        return canNavigate;
    }

    return false;
}
当调用 FocusCell 方法时取消导航

ContainerItemFocusCell 方法在内部使用,并且可以从外部用于以编程方式导航到一个单元格。我们需要增强这个方法,以便如果待聚焦的单元格位于与当前项不同的项中,并且当前项无法验证,则该方法“不起作用”。

让我们修改 ContainerItem 类的 FocusCell 方法。

public bool FocusCell(string cellName)
{
    HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
    if ((parentContainer != null) && parentContainer.IsCurrentItemEditedOrDirty)
    {
        bool canNavigate = true;
        bool isEditedSameItem = parentContainer.CurrentEditedOrDirtyItem == this;
        if (isEditedSameItem)
        {
            if (parentContainer.IsCurrentCellInEditMode && 
                   (parentContainer.CurrentEditedCell.Name != cellName))
                canNavigate = parentContainer.CommitEdit(false);
        }
        else
            canNavigate = parentContainer.Validate(false);

        if (!canNavigate)
            return false;
    }

    object focusedElement = FocusManager.GetFocusedElement();
    FrameworkElement firstChild = GetFirstTreeChild() as FrameworkElement;
    if (firstChild != null)
    {
     . . .
当一个项被点击时取消导航

在本教程的前面,我们在我们的 ContainerItem 部分类中重写了 OnMouseLeftButtonDown 方法,以便在当前单元格无法提交时,一个项不能获得焦点。

我们现在必须增强这个方法,以便如果当前项被编辑或为脏数据:

  • 如果用户点击了除当前项之外的另一个项,我们在允许导航处理之前验证当前项。
  • 如果用户点击了当前项中的某个地方,但没有点击当前单元格,我们在允许导航处理之前提交当前单元格。
  • 如果用户点击了当前单元格,我们允许导航处理。

如果当前项不是脏数据或未被编辑,我们允许导航过程,无需任何验证。

让我们修改 ContainerItem 类的 OnMouseLeftButtonDown 方法。

protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
    bool canNavigate = true;
    HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
    if ((parentContainer != null) && (parentContainer.IsCurrentItemEditedOrDirty))
    {
        bool isEditedSameItem = parentContainer.CurrentEditedOrDirtyItem == this;
        if (isEditedSameItem)
        {
            if (!TreeHelper.IsChildOf(parentContainer.CurrentEditedCell, 
                              e.OriginalSource as DependencyObject))
                canNavigate = parentContainer.CommitEdit(false);
        }
        else
            canNavigate = parentContainer.Validate(false);
    }

    if (canNavigate)
        base.OnMouseLeftButtonDown(e);
    else
        e.Handled = true;
}
在 GridSpatialNavigator 中取消导航

到目前为止,在允许 GridSpatialNavigator 处理导航之前,我们通过调用 GridSpatialNavigatorValidateCell 方法来检查当前单元格是否可以提交。

然而,GridSpatialNavigator 处理 HandyContainer 项之间的导航,因此,在允许导航处理之前,我们必须验证当前项(而不仅仅是当前单元格)。

首先,让我们用一个 ValidateItem 方法替换 GridSpatialNavigatorValidateCell 方法。

protected static bool ValidateItem(IKeyNavigatorContainer container)
{
    HandyContainer parentContainer = 
       HandyContainer.GetParentContainer((FrameworkElement)container);

    if (parentContainer.IsCurrentItemEditedOrDirty)
        return parentContainer.Validate(true);

    return true;
}

然后,在 ActiveKeyDownKeyDown 方法中,我们必须调用 ValidateItem 方法以确保当前项在调用祖先方法之前通过验证。

public override void ActiveKeyDown(IKeyNavigatorContainer container, GKeyEventArgs e)
{
    LastKeyProcessed = e.Key;
    LastModifier = Keyboard.Modifiers;
    if ((((e.Key != Key.Home) && (e.Key != Key.End)) || 
        ((Keyboard.Modifiers & ModifierKeys.Control) != ModifierKeys.Control)) &&
        (e.Key != Key.Tab))
    {
        if (e.Key == Key.Enter)
            e.Key = Key.Down;

        LastKeyProcessed = e.Key;
        switch (e.Key)
        {
            case Key.Down:
            case Key.Up:
            case Key.PageDown:
            case Key.PageUp:
            case Key.Enter:
            case Key.Home:
            case Key.End:
            case Key.Right:
            case Key.Left:
            case Key.Tab:
                if (ValidateItem(container))
                    base.ActiveKeyDown(container, e);
                else
                    e.Handled = true;
                break;

            default:
                base.ActiveKeyDown(container, e);
                break;
        }

    }
    else
        ProcessKey(container, e);
}

public override void KeyDown(IKeyNavigatorContainer container, GKeyEventArgs e)
{
    LastKeyProcessed = e.Key;
    LastModifier = Keyboard.Modifiers;
    if ((((e.Key != Key.Home) && (e.Key != Key.End)) || 
        ((Keyboard.Modifiers & ModifierKeys.Control) != ModifierKeys.Control)) &&
        (e.Key != Key.Tab))
    {
        if (e.Key == Key.Enter)
            e.Key = Key.Down;

        LastKeyProcessed = e.Key;

        switch (e.Key)
        {
            case Key.Down:
            case Key.Up:
            case Key.PageDown:
            case Key.PageUp:
            case Key.Enter:
            case Key.Home:
            case Key.End:
            case Key.Right:
            case Key.Left:
            case Key.Tab:
                if (ValidateItem(container))
                    base.KeyDown(container, e);
                else
                    e.Handled = true;
                break;

            default:
                base.KeyDown(container, e);
                break;
        }
    }
    else
        ProcessKey(container, e);
}

最后,我们还需要修改 ProcessKey 方法。

我们不能仅仅将 ProcessKey 方法开头的对 ValidateCell 方法的调用替换为对 ValidateItem 方法的调用。实际上,Tab 键导航是由 ProcessKey 方法处理的,而按下 Tab 键并不总是意味着我们将导航到另一个项。因此,在 Tab 键的情况下,我们将不得不调用单元格的 ValidateItem 方法或 CommitEdit 方法。

private void ProcessKey(IKeyNavigatorContainer container, GKeyEventArgs e)
{
    GStackPanel gStackPanel = (GStackPanel)container;
    HandyContainer parentContainer = 
       HandyContainer.GetParentContainer(gStackPanel);

    if (gStackPanel.Children.Count > 0)
    {
        if ((e.Key == Key.Home) && 
            ((Keyboard.Modifiers & ModifierKeys.Control) == 
                                       ModifierKeys.Control))
        {
            if (!ValidateItem(container))
            {
                e.Handled = true;
                return;
            }

            gStackPanel.MoveToFirstIndex();

            ContainerItem firstItem = (ContainerItem)gStackPanel.Children[0];

            parentContainer.CurrentCellName = firstItem.GetFirstCellName();
            if (firstItem.FocusCell(parentContainer.CurrentCellName))
                e.Handled = true;

        }
        else if ((e.Key == Key.End) && 
                ((Keyboard.Modifiers & ModifierKeys.Control) == 
                                           ModifierKeys.Control))
        {
            if (!ValidateItem(container))
            {
                e.Handled = true;
                return;
            }

            gStackPanel.MoveToLastIndex();

            ContainerItem lastContainerItem = 
              (ContainerItem)gStackPanel.Children[gStackPanel.Children.Count - 1];

            parentContainer.CurrentCellName = lastContainerItem.GetLastCellName();
            if (lastContainerItem.FocusCell(parentContainer.CurrentCellName))
                e.Handled = true;
        }
        else if (e.Key == Key.Tab)
        {
            ContainerItem currentItem = 
              parentContainer.GetElement(parentContainer.HoldFocusItem) 
              as ContainerItem;
            if (currentItem != null)
            {
                if ((Keyboard.Modifiers & ModifierKeys.Shift) == 
                                              ModifierKeys.Shift)
                {
                    if (String.IsNullOrEmpty(parentContainer.CurrentCellName) ||
                        (parentContainer.CurrentCellName == 
                                    currentItem.GetFirstCellName()))
                    {
                        if (!ValidateItem(container))
                        {
                            e.Handled = true;
                            return;
                        }

                        ContainerItem prevItem = currentItem.PrevNode as ContainerItem;
                        if (prevItem != null)
                        {
                            parentContainer.CurrentCellName = prevItem.GetLastCellName();
                            if (prevItem.FocusCell(parentContainer.CurrentCellName))
                            {
                                gStackPanel.EnsureVisible(
                                        gStackPanel.Children.IndexOf(prevItem));
                                e.Handled = true;
                            }
                        }
                    }
                    else
                    {
                        if (!parentContainer.CommitEdit(true))
                            e.Handled = true;
                    }
                }
                else
                {
                    if (String.IsNullOrEmpty(parentContainer.CurrentCellName) ||
                        (parentContainer.CurrentCellName == 
                                    currentItem.GetLastCellName()))
                    {
                        if (!ValidateItem(container))
                        {
                            e.Handled = true;
                            return;
                        }

                        ContainerItem nextItem = currentItem.NextNode as ContainerItem;
                        if (nextItem != null)
                        {
                            parentContainer.CurrentCellName = nextItem.GetFirstCellName();
                            if (nextItem.FocusCell(parentContainer.CurrentCellName))
                            {
                                gStackPanel.EnsureVisible(
                                           gStackPanel.Children.IndexOf(nextItem));
                                e.Handled = true;
                            }
                        }
                    }
                    else
                    {
                        if (!parentContainer.CommitEdit(true))
                            e.Handled = true;
                    }
                }
            }
        }
    }
}

取消网格的滚动

为了避免诸如单元格在不可见时被提交等令人困惑的情况,我们实现了一旦用户尝试垂直滚动网格就提交当前单元格的功能。出于同样的原因,如果当前单元格无法提交,我们不允许网格滚动。

我们必须改进这种行为,以便考虑到项的验证。一旦用户垂直滚动网格,当前项就必须被验证,并且如果当前项无法验证,则不允许网格滚动。

让我们修改我们的 HandyContainer 部分类的 OnVerticalOffsetChanging 方法以实现这些新规则。

protected override void OnVerticalOffsetChanging(CancelOffsetEventArgs e)
{
    if (this.IsCurrentItemEditedOrDirty && !this.Validate())
        e.Cancel = true;

    base.OnVerticalOffsetChanging(e);
}

取消节点展开和折叠

如果当前单元格无法提交,则不允许展开或折叠节点。我们必须扩展此行为,以便在当前项无法验证时,不能展开或折叠节点。

让我们修改我们的 ContainerItem 部分类的 OnIsExpandedChanging 方法。

protected override void OnIsExpandedChanging(
          Netika.Windows.Controls.IsExpandedChangingEventArgs e)
{
    HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
    if ((parentContainer != null) && (!parentContainer.Validate()))
        e.Cancel = true;
    else
        base.OnIsExpandedChanging(e);            
}

测试项验证

让我们通过在我们的 GridBody 项目中使用 CurrentItemValidating 事件来测试我们的更改。

首先,让我们向我们的 Person 类添加一个 Validate 方法。

public bool Validate()
{
    int zipCodeValue = 0;
    int.TryParse(zipCode, out zipCodeValue);
    if ((city.ToUpper() == "NEW YORK") && 
             ((zipCodeValue < 10001) || (zipCodeValue > 10292)))
        return false;

    return true;
}

在这个方法中,我们检查如果用户输入的城市是“纽约”,那么邮政编码是否在预期范围内(10001到10292之间)。在实际应用中,我们当然会检查每个城市的邮政编码。

让我们修改 GridBody 项目的 Page.xaml 来处理 CurrentItemValidating 事件。

<handycontainer x:name="MyGridBody" virtualmode="On" alternatetype="Items" 
    handydefaultitemstyle="Node" handystyle="GridBodyStyle" 
    currentcellvalidating="MyGridBody_CurrentCellValidating" 
    currentitemvalidating="MyGridBody_CurrentItemValidating" />
<handycontainer x:name="MyGridBody" virtualmode="On" alternatetype="Items" 
    handydefaultitemstyle="Node" handystyle="GridBodyStyle" 
    currentcellvalidating="MyGridBody_CurrentCellValidating" 
    currentitemvalidating="MyGridBody_CurrentItemValidating" />

然后,让我们向 Page.xaml.cs 文件添加一些代码,以验证我们项的值。

private void MyGridBody_CurrentItemValidating(object sender, ItemValidatingEventArgs e)
{
    Person currentPerson = HandyContainer.GetItemSource(e.Item) as Person;
    if ((currentPerson != null) && (!currentPerson.Validate()))
        e.Cancel = true;
}

现在让我们来试试项目验证过程。

  • 启动应用程序
  • 编辑 City4 单元格并输入 New York
  • 编辑 ZipCode4 单元格并输入 0
  • 尝试导航到另一个单元格。

我们能够在当前项内部的单元格之间导航,但由于该项无效,我们无法导航到另一个项。我们无法垂直滚动网格。

  • 在 ZipCode4 单元格中输入一个有效值,例如 10001。

现在,我们能够导航到另一个项,并且网格可以垂直滚动。

提醒用户

引言

当一个项无法验证时,用户可能会因为无法导航或滚动而感到困惑。为了让用户明白当前项有问题,让我们在它无法验证时将项的边框显示为红色。

IsValid 属性

让我们向我们的 ContainerItem 分部类添加一个 IsValid 属性。这个方法将适当地将 ContainerItemValid 状态切换到 NotValid 状态。

private bool isValid = true;
public bool IsValid
{
    get { return isValid; }
    internal set
    {
        if (isValid != value)
        {
            isValid = value;
            if (isValid)
                VisualStateManager.GoToState(this, "Valid", true);
            else
                VisualStateManager.GoToState(this, "NotValid", true);
        }
    }
}

实现 Valid 和 NotValid 状态

Container_RowItemStyleContainer_RowNodeStyle 两种样式中,我们都需要实现 ValidNotValid 状态。这意味着要添加一个新的 ValidStates VisualStateGroup

<vsm:VisualStateGroup x:Name="ValidStates">
    <vsm:VisualState x:Name="Valid"/>
    <vsm:VisualState x:Name="NotValid">
        <Storyboard>
            <ObjectAnimationUsingKeyFrames 
                Storyboard.TargetName="ValidElement" 
                Storyboard.TargetProperty="Visibility" 
                Duration="0">
                <DiscreteObjectKeyFrame KeyTime="0">
                    <DiscreteObjectKeyFrame.Value>
                        <Visibility>Visible</Visibility>
                    </DiscreteObjectKeyFrame.Value>
                </DiscreteObjectKeyFrame>
            </ObjectAnimationUsingKeyFrames>
        </Storyboard>
    </vsm:VisualState>
</vsm:VisualStateGroup>

以及一个 ValidElement Rectangle

<Rectangle Name="ValidElement" 
   Stroke="Red" 
   StrokeThickness="2"
   IsHitTestVisible="false" 
   Visibility="Collapsed"
   Margin="0,1,1,0"/>

最终,我们的 Container_RowItemStyleContainer_RowNodeStyle 样式将看起来像这样:

<Style x:Key="Container_RowItemStyle" TargetType="o:HandyListItem">
    <Setter Property="HorizontalAlignment" Value="Left" />
    <Setter Property="HorizontalContentAlignment" Value="Stretch" />
    <Setter Property="VerticalContentAlignment" Value="Center" />
    <Setter Property="Cursor" Value="Arrow" />
    <Setter Property="Padding" Value="0" />
    <Setter Property="Margin" Value="0"/>
    <Setter Property="Background" 
           Value="{StaticResource DefaultControlBackground}" />
    <Setter Property="Foreground" 
           Value="{StaticResource DefaultForeground}"/>
    <Setter Property="FontSize" Value="11" />
    <Setter Property="Indentation" Value="10" />
    <Setter Property="IsTabStop" Value="True" />
    <Setter Property="IsKeyActivable" Value="True"/>
    <Setter Property="ItemUnpressDropDownBehavior" Value="CloseAll" />
    <Setter Property="BorderBrush" 
          Value="{StaticResource DefaultListControlStroke}"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="o:HandyListItem">
                <Grid Background="Transparent" x:Name="LayoutRoot">
                    <vsm:VisualStateManager.VisualStateGroups>
                        <vsm:VisualStateGroup x:Name="CommonStates">
                            <vsm:VisualState x:Name="Normal"/>
                            <vsm:VisualState x:Name="Disabled">
                                <Storyboard>
                                    <DoubleAnimation 
                                        Duration="0" 
                                        Storyboard.TargetName="ELEMENT_ContentPresenter" 
                                        Storyboard.TargetProperty="Opacity" 
                                        To="0.6"/>
                                    <DoubleAnimation 
                                        Duration="0" 
                                        Storyboard.TargetName="SelectedVisual" 
                                        Storyboard.TargetProperty="Opacity" 
                                        To="0.6"/>
                                    <DoubleAnimation 
                                        Duration="0" 
                                        Storyboard.TargetName="ReflectVisual" 
                                        Storyboard.TargetProperty="Opacity" 
                                        To="0"/>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="FocusStates">
                            <vsm:VisualState x:Name="NotFocused"/>
                            <vsm:VisualState x:Name="Focused">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="FocusVisual" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="MouseOverStates">
                            <vsm:VisualState x:Name="NotMouseOver"/>
                            <vsm:VisualState x:Name="MouseOver">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="MouseOverVisual" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="PressedStates">
                            <vsm:VisualState x:Name="NotPressed"/>
                            <vsm:VisualState x:Name="Pressed">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="PressedVisual" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="SelectedStates">
                            <vsm:VisualState x:Name="NotSelected"/>
                            <vsm:VisualState x:Name="Selected">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="SelectedVisual" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="ReflectVisual" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="AlternateStates">
                            <vsm:VisualState x:Name="NotIsAlternate"/>
                            <vsm:VisualState x:Name="IsAlternate">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="AlternateBackgroundVisual" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="BackgroundVisual" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Collapsed</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="OrientationStates">
                            <vsm:VisualState x:Name="Horizontal"/>
                            <vsm:VisualState x:Name="Vertical"/>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="ValidStates">
                            <vsm:VisualState x:Name="Valid"/>
                            <vsm:VisualState x:Name="NotValid">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="ValidElement" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                    </vsm:VisualStateManager.VisualStateGroups>
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="1*"/>
                            <RowDefinition Height="1*"/>
                        </Grid.RowDefinitions>
                        <Border x:Name="BackgroundVisual" 
                            Background="{TemplateBinding Background}" 
                            Grid.RowSpan="2" />
                        <Border x:Name="AlternateBackgroundVisual" 
                            Background="{StaticResource DefaultAlternativeBackground}" 
                            Grid.RowSpan="2" 
                            Visibility="Collapsed"/>
                        <Rectangle x:Name="SelectedVisual" 
                               Fill="{StaticResource DefaultDownColor}" 
                               Grid.RowSpan="2" 
                               Visibility="Collapsed"/>
                        <Rectangle x:Name="MouseOverVisual" 
                               Fill="{StaticResource DefaultDarkGradientBottomVertical}" 
                               Grid.RowSpan="2" 
                               Margin="0,0,1,0"
                               Visibility="Collapsed"/>
                        <Grid x:Name="PressedVisual" 
                          Visibility="Collapsed" 
                          Grid.RowSpan="2" >
                            <Grid.RowDefinitions>
                                <RowDefinition Height="1*"/>
                                <RowDefinition Height="1*"/>
                            </Grid.RowDefinitions>
                            <Rectangle 
                                Fill="{StaticResource DefaultDarkGradientBottomVertical}" 
                                Grid.Row="1" 
                                Margin="0,0,1,0" />
                        </Grid>
                        <Rectangle 
                            x:Name="ReflectVisual" 
                            Fill="{StaticResource DefaultReflectVertical}" 
                            Margin="1,1,1,0" 
                            Visibility="Collapsed"/>                            
                        <Rectangle Name="ValidElement" 
                               Stroke="Red" 
                               StrokeThickness="2"
                               IsHitTestVisible="false" 
                               Visibility="Collapsed"
                               Margin="0,1,1,0"
                               Grid.RowSpan="2"/>
                        <Rectangle 
                            x:Name="FocusVisual" 
                            Grid.RowSpan="2" 
                            Stroke="{StaticResource DefaultFocus}" 
                            StrokeDashCap="Round" 
                            Margin="0,1,1,0" 
                            StrokeDashArray=".2 2" 
                            Visibility="Collapsed"/>
                        <!-- Item content -->
                        <g:GContentPresenter
                          Grid.RowSpan="2" 
                          x:Name="ELEMENT_ContentPresenter"
                          Content="{TemplateBinding Content}"
                          ContentTemplate="{TemplateBinding ContentTemplate}"
                          OrientatedHorizontalAlignment=
                                "{TemplateBinding HorizontalContentAlignment}"
                          OrientatedMargin="{TemplateBinding Padding}"
                          OrientatedVerticalAlignment=
                                "{TemplateBinding VerticalContentAlignment}"  
                          PresenterOrientation=
                                "{TemplateBinding PresenterOrientation}"/>
                        <Rectangle x:Name="BorderElement" 
                           Grid.RowSpan="2" 
                           Stroke="{TemplateBinding BorderBrush}" 
                           StrokeThickness="{TemplateBinding BorderThickness}" 
                           Margin="-1,0,0,-1"/>
                    </Grid>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<Style x:Key="Container_RowNodeStyle" TargetType="o:HandyListItem">
    <Setter Property="HorizontalAlignment" Value="Left" />
    <Setter Property="HorizontalContentAlignment" Value="Stretch" />
    <Setter Property="VerticalContentAlignment" Value="Center" />
    <Setter Property="Cursor" Value="Arrow" />
    <Setter Property="Padding" Value="0" />
    <Setter Property="Margin" Value="0"/>
    <Setter Property="Foreground" 
          Value="{StaticResource DefaultForeground}"/>
    <Setter Property="Background" Value="White" />
    <Setter Property="FontSize" Value="11" />
    <Setter Property="Indentation" Value="10" />
    <Setter Property="IsTabStop" Value="True" />
    <Setter Property="IsKeyActivable" Value="True"/>
    <Setter Property="ItemUnpressDropDownBehavior" 
         Value="CloseAll" />
    <Setter Property="BorderBrush" 
         Value="{StaticResource DefaultListControlStroke}"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="o:HandyListItem">
                <Grid x:Name="LayoutRoot" 
                           Background="Transparent">
                    <vsm:VisualStateManager.VisualStateGroups>
                        <vsm:VisualStateGroup x:Name="CommonStates">
                            <vsm:VisualState x:Name="Normal"/>
                            <vsm:VisualState x:Name="Disabled">
                                <Storyboard>
                                    <DoubleAnimation 
                                        Duration="0" 
                                        Storyboard.TargetName="ELEMENT_ContentPresenter" 
                                        Storyboard.TargetProperty="Opacity" 
                                        To="0.6"/>
                                    <DoubleAnimation 
                                        Duration="0" 
                                        Storyboard.TargetName="ExpandedVisual" 
                                        Storyboard.TargetProperty="Opacity" 
                                        To="0.6"/>
                                    <DoubleAnimation 
                                        Duration="0" 
                                        Storyboard.TargetName="SelectedVisual" 
                                        Storyboard.TargetProperty="Opacity" 
                                        To="0.6"/>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="ExpandedReflectVisual" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="SelectedReflectVisual" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <DoubleAnimation 
                                        Duration="0" 
                                        Storyboard.TargetName="HasItem" 
                                        Storyboard.TargetProperty="Opacity" 
                                        To="0.6"/>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="FocusStates">
                            <vsm:VisualState x:Name="NotFocused"/>
                            <vsm:VisualState x:Name="Focused">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="FocusVisual" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="MouseOverStates">
                            <vsm:VisualState x:Name="NotMouseOver"/>
                            <vsm:VisualState x:Name="MouseOver">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="MouseOverVisual" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="ExpandedOverVisual" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="PressedStates">
                            <vsm:VisualState x:Name="NotPressed"/>
                            <vsm:VisualState x:Name="Pressed">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="PressedVisual" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="SelectedStates">
                            <vsm:VisualState x:Name="NotSelected"/>
                            <vsm:VisualState x:Name="Selected">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="SelectedVisual" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="HasItemsStates">
                            <vsm:VisualState x:Name="NotHasItems">
                                <Storyboard>
                                    <DoubleAnimation 
                                        Duration="0" 
                                        Storyboard.TargetName="ExpandedVisual" 
                                        Storyboard.TargetProperty="Opacity" 
                                        To="0"/>
                                </Storyboard>
                            </vsm:VisualState>
                            <vsm:VisualState x:Name="HasItems">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="HasItem" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>

                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="IsExpandedStates">
                            <vsm:VisualState x:Name="NotIsExpanded"/>
                            <vsm:VisualState x:Name="IsExpanded">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="CheckedArrow" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="ArrowUnchecked" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Collapsed</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="ExpandedVisual" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="AlternateStates">
                            <vsm:VisualState x:Name="NotIsAlternate"/>
                            <vsm:VisualState x:Name="IsAlternate">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="AlternateBackgroundVisual" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="BackgroundVisual" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Collapsed</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="InvertedStates">
                            <vsm:VisualState x:Name="InvertedItemsFlowDirection">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="ArrowCheckedToTop" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="ArrowCheckedToBottom" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Collapsed</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                            <vsm:VisualState x:Name="NormalItemsFlowDirection"/>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="ValidStates">
                            <vsm:VisualState x:Name="Valid"/>
                            <vsm:VisualState x:Name="NotValid">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="ValidElement" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                    </vsm:VisualStateManager.VisualStateGroups>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="*"/>
                        <RowDefinition Height="Auto"/>
                    </Grid.RowDefinitions>
                    <StackPanel Orientation="Horizontal">
                        <Rectangle Width="{TemplateBinding FullIndentation}" />
                        <Grid MinWidth="16" Margin="0,0,1,0">
                            <Grid x:Name="HasItem" 
                              Visibility="Collapsed" 
                              Height="16" Width="16" 
                              Margin="0,0,0,0">
                                <Path x:Name="ArrowUnchecked" 
                                  HorizontalAlignment="Right" 
                                  Height="8" Width="8" 
                                  Fill="{StaticResource DefaultForeground}" 
                                  Stretch="Fill" 
                                  Data="M 4 0 L 8 4 L 4 8 Z" />
                                <Grid x:Name="CheckedArrow" 
                                            Visibility="Collapsed">
                                    <Path x:Name="ArrowCheckedToTop" 
                                      HorizontalAlignment="Right" 
                                      Height="8" Width="8" 
                                      Fill="{StaticResource DefaultForeground}" 
                                      Stretch="Fill" 
                                      Data="M 8 4 L 0 4 L 4 0 z" 
                                      Visibility="Collapsed"/>
                                    <Path x:Name="ArrowCheckedToBottom" 
                                      HorizontalAlignment="Right" 
                                      Height="8" Width="8" 
                                      Fill="{StaticResource DefaultForeground}" 
                                      Stretch="Fill" 
                                      Data="M 0 4 L 8 4 L 4 8 Z" />
                                </Grid>
                                <ToggleButton 
                                  x:Name="ELEMENT_ExpandButton" 
                                  Height="16" Width="16"  
                                  Style="{StaticResource EmptyToggleButtonStyle}" 
                                  IsChecked="{TemplateBinding IsExpanded}" 
                                  IsThreeState="False" IsTabStop="False"/>
                            </Grid>
                        </Grid>
                        <Grid>
                            <Border x:Name="BackgroundVisual" 
                                Background="{TemplateBinding Background}" />
                            <Rectangle 
                               Fill="{StaticResource DefaultAlternativeBackground}" 
                               x:Name="AlternateBackgroundVisual" 
                               Visibility="Collapsed"/>
                            <Grid x:Name="ExpandedVisual" 
                                       Visibility="Collapsed">
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="1*"/>
                                    <RowDefinition Height="1*"/>
                                </Grid.RowDefinitions>
                                <Rectangle Fill="{StaticResource DefaultBackground}" 
                                       Grid.RowSpan="2"/>
                                <Rectangle 
                                    x:Name="ExpandedOverVisual" 
                                    Fill="{StaticResource 
                                    DefaultDarkGradientBottomVertical}" 
                                    Grid.RowSpan="2" Visibility="Collapsed" 
                                    Margin="0,0,0,1"/>
                                <Rectangle 
                                    x:Name="ExpandedReflectVisual" 
                                    Fill="{StaticResource DefaultReflectVertical}" 
                                    Margin="0,1,0,0"/>
                            </Grid>
                            <Grid x:Name="SelectedVisual" 
                              Visibility="Collapsed" >
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="1*"/>
                                    <RowDefinition Height="1*"/>
                                </Grid.RowDefinitions>
                                <Rectangle Fill="{StaticResource DefaultDownColor}" 
                                       Grid.RowSpan="2"/>
                                <Rectangle 
                                   x:Name="SelectedReflectVisual" 
                                   Fill="{StaticResource DefaultReflectVertical}" 
                                   Margin="0,1,1,0" 
                                   RadiusX="1" RadiusY="1"/>
                            </Grid>
                            <Rectangle 
                               x:Name="MouseOverVisual" 
                               Fill="{StaticResource DefaultDarkGradientBottomVertical}" 
                               Visibility="Collapsed" Margin="0,0,1,0"/>
                            <Grid x:Name="PressedVisual" 
                                           Visibility="Collapsed">
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="1*"/>
                                    <RowDefinition Height="1*"/>
                                </Grid.RowDefinitions>
                                <Rectangle 
                                    Fill="{StaticResource DefaultDownColor}" 
                                    Grid.RowSpan="2"/>
                                <Rectangle 
                                    Fill="{StaticResource DefaultDarkGradientBottomVertical}" 
                                    Grid.Row="1" Margin="0,0,1,0"/>
                                <Rectangle 
                                    Fill="{StaticResource DefaultReflectVertical}" 
                                    Margin="0,1,1,0" 
                                    RadiusX="1" RadiusY="1"/>
                            </Grid>
                            <Rectangle 
                               HorizontalAlignment="Stretch" 
                               VerticalAlignment="Top" 
                               Stroke="{TemplateBinding BorderBrush}" 
                               StrokeThickness="0.5" 
                               Height="1"/>                                
                            <Rectangle 
                               Name="ValidElement" 
                               Stroke="Red" 
                               StrokeThickness="2"
                               IsHitTestVisible="false" 
                               Visibility="Collapsed"
                               Margin="0,1,1,0"/>
                            <Rectangle 
                               x:Name="FocusVisual" 
                               Stroke="{StaticResource DefaultFocus}" 
                               StrokeDashCap="Round" Margin="0,1,1,0" 
                               StrokeDashArray=".2 2" 
                               Visibility="Collapsed"/>
                            <g:GContentPresenter
                              x:Name="ELEMENT_ContentPresenter"
                              Content="{TemplateBinding Content}"
                              ContentTemplate="{TemplateBinding ContentTemplate}"
                              Cursor="{TemplateBinding Cursor}"
                              OrientatedHorizontalAlignment=
                                "{TemplateBinding HorizontalContentAlignment}"
                              OrientatedMargin="{TemplateBinding Padding}"
                              OrientatedVerticalAlignment=
                                "{TemplateBinding VerticalContentAlignment}" 
                              PresenterOrientation=
                                "{TemplateBinding PresenterOrientation}"/>
                            <Rectangle 
                               x:Name="BorderElement" 
                               Stroke="{TemplateBinding BorderBrush}" 
                               StrokeThickness="{TemplateBinding BorderThickness}" 
                               Margin="-1,0,0,-1"/>
                        </Grid>
                    </StackPanel>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

更新 IsValid 值

让我们更新 ContainerItem 类的 Validate 方法,以便适当地更新 IsValid 属性值。

internal bool Validate(bool keepFocus)
{
    HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
    if (parentContainer != null)
    {
        if (parentContainer.IsCurrentCellInEditMode && 
                    !parentContainer.CommitEdit(keepFocus))
            return false;

        if (!IsDirty)
            return true;

        ItemValidatingEventArgs eventArgs = new ItemValidatingEventArgs(this, false);
        parentContainer._OnCurrentItemValidating(eventArgs);
        if (eventArgs.Cancel)
        {
            IsValid = false;
            return false;
        }

        IsValid = true;

        IsDirtyCount = 0;

        parentContainer._OnCurrentItemValidated(EventArgs.Empty);
    }

    return true;
}

测试我们的更改

让我们测试一下我们的更改,看看右边框是否能正确显示。

  • 启动应用程序
  • 编辑 City4 单元格并输入 New York
  • 编辑 ZipCode4 单元格并输入 0
  • 尝试导航到另一个项目。

当前项周围显示一个红色边框。

  • 在 ZipCode4 单元格中输入一个有效值,例如 10001。
  • 导航到另一个项目

红色边框不再显示。

7. 完善编辑过程

聚焦外部控件

引言

在编辑过程中,还有一种情况我们没有考虑到:当另一个控件获得焦点时会发生什么?让我们通过在我们的网格主体旁边添加一个按钮(或任何可以获得焦点的控件)来说明这种情况。

<UserControl x:Class="GridBody.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:g="clr-namespace:Netika.Windows.Controls;assembly=GoaEssentials"
    xmlns:o="clr-namespace:Open.Windows.Controls;assembly=GoaOpen">
    <Grid x:Name="LayoutRoot" Background="White">
        <g:GDockPanel>
            <Button Content="Focus Button" 
                 g:GDockPanel.Dock="Top" 
                 Margin="5" Width="200"/>
            <o:HandyContainer
                x:Name="MyGridBody"
                VirtualMode="On"
                AlternateType="Items"
                HandyDefaultItemStyle="Node"
                HandyStyle="GridBodyStyle"
                CurrentCellValidating="MyGridBody_CurrentCellValidating"
                CurrentItemValidating="MyGridBody_CurrentItemValidating"
                g:GDockPanel.Dock="Fill">
                . . .
            </o:HandyContainer>
        </g:GDockPanel>
    </Grid>
</UserControl>

让我们看看当一个项正在编辑时,我们点击按钮会发生什么。

  • 启动应用程序
  • 编辑 Address3 单元格并输入一些内容。
  • 点击页面顶部的按钮。

单元格被提交,按钮获得焦点。这是预期的行为。现在让我们看看当一个单元格正在被编辑且该单元格无法提交时,我们点击按钮会发生什么。

  • 启动应用程序
  • 编辑 FirstName3 单元格。
  • 删除单元格内容。
  • 点击页面顶部的按钮。

按钮获得了焦点,并且由于单元格无法提交,FirstName3 单元格周围显示了红色边框。现在让我们点击网格中的某个地方(但不是 FirstName3 单元格)。什么也没发生。

这既是预期的行为,也是不希望的行为。作为程序员,我们明白发生了什么——因为单元格没有被提交,另一个单元格就不能获得焦点,点击网格没有任何效果。因此,这是预期的行为。然而,作为用户,我们希望一旦我们点击网格的任何地方,FirstName3 单元格就能重新获得焦点。所以,这也是不希望的行为。

让我们试着按 Tab 键。这一次,焦点被“移动”到了网格,但位置不对。网格顶部的项或者持有被编辑单元格的项获得了焦点(根据你在编辑单元格之前做的操作,另一个项可能会获得焦点)。我们本期望 FirstName3 单元格会获得焦点。

最后,让我们看看当一个项目无法被验证时会发生什么

  • 启动应用程序
  • 编辑 City 3 单元格
  • 键入 New York
  • 点击页面顶部的按钮。

按钮获得了焦点,但项目未被验证。没有任何东西警告用户他输入的值是不正确的。如果我们点击网格,那么该项目会根据点击网格的位置被验证或不被验证。作为用户,我们期望当按钮被按下时(即网格失去焦点时),网格能够被验证。

在失去焦点时进行验证

首先,让我们在 HandyContainer 分部类中重写 OnGotFocusOnLostFocus 方法,以便在网格失去焦点时对其进行验证。

private bool hasFocus = false;
protected override void OnGotFocus(RoutedEventArgs e)
{
    base.OnGotFocus(e);
    hasFocus = true;

}

protected override void OnLostFocus(RoutedEventArgs e)
{
    base.OnLostFocus(e);

    object currentFocusedElement = FocusManager.GetFocusedElement();
    if (!TreeHelper.IsChildOf(this, currentFocusedElement as DependencyObject))
    {
        hasFocus = false;
        Validate(false);
    }
}

让我们试试我们的改动。

  • 启动应用程序
  • 编辑 City 3 单元格
  • 键入 New York
  • 点击页面顶部的按钮。

这一次,当我们点击按钮时,网格被验证,并且在无效项目周围显示了一个红色边框。

ResumeEdit 方法

在上述情况中,我们希望一旦我们点击网格的任何地方,或者一旦网格重新获得焦点,编辑过程就能恢复。让我们向 Cell 类添加一个 ResumeEdit 方法。这个方法将在需要时被调用以重新开始编辑单元格。

internal bool ResumeEdit()
{
    return OnBeginEdit();
}

我们再向 HandyContainer 添加一个 ResumeEdit 方法。该方法将检查网格是否包含脏单元格,如果有,它将恢复对该单元格的编辑。如果没有脏单元格,它将检查网格是否包含脏项目,如果有,它将把焦点设置回该项目。

internal bool ResumeEdit()
{
    if (this.CurrentDirtyCell != null)
        return CurrentDirtyCell.ResumeEdit();
    else if (this.CurrentDirtyItem != null)
    {
        FrameworkElement focusElement = 
             FocusManager.GetFocusedElement() as FrameworkElement;
        if (ContainerItem.GetParentContainerItem(focusElement) != this.CurrentDirtyItem)
            return this.CurrentDirtyItem.Focus();
    }
    return false;
}

调用 ResumeEdit 方法

我们现在需要在以下情况调用 ResumeEdit 方法:

  • 网格或其子元素之一获得焦点时
  • 网格或其子元素之一被点击时

然而,如果网格已经持有焦点(即网格或其子元素之一已经获得焦点),则不得调用 ResumeEdit 方法。

HandyContainer 的 OnMouseLeftButtonDown 方法

让我们重写 HandyContainerOnMouseLeftButtonDown 方法来调用 ResumeEdit 方法。

protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
    base.OnMouseLeftButtonDown(e);

    if (!hasFocus)
        ResumeEdit();
}
HandyContainer 的 OnGotFocus 方法

我们还需要修改 OnGotFocus 方法,以便在网格获得焦点时调用 ResumeEdit 方法。

private bool hasFocus = false;
protected override void OnGotFocus(RoutedEventArgs e)
{
    base.OnGotFocus(e);
    if (!hasFocus)
    {
        hasFocus = true;
        ResumeEdit();
    }
}
ContainerItem 的 OnMouseLeftButtonDown 方法

我们也需要更改 ContainerItemOnMouseLeftButtonDown 方法。

protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
    bool canNavigate = true;
    HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
    if ((parentContainer != null) && 
        (parentContainer.IsCurrentItemEditedOrDirty))
    {
        bool isEditedSameItem = 
             parentContainer.CurrentEditedOrDirtyItem == this;
        if (isEditedSameItem)
        {
            if (!TreeHelper.IsChildOf(parentContainer.CurrentEditedCell, 
                             e.OriginalSource as DependencyObject))
                canNavigate = parentContainer.CommitEdit(false);
        }
        else
            canNavigate = parentContainer.Validate(false);
    }

    if (canNavigate)
        base.OnMouseLeftButtonDown(e);
    else
    {
        e.Handled = true;
        FrameworkElement focusElement = 
              FocusManager.GetFocusedElement() as FrameworkElement;
        if (!TreeHelper.IsChildOf(parentContainer, focusElement))
            parentContainer.ResumeEdit();
    }

}
Cell 的 OnMouseLeftButtonDown 方法

最后,我们必须更改 Cell 类的 OnMouseLeftButtonDown 方法。

protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
    base.OnMouseLeftButtonDown(e);

    object currentFocusedElement = FocusManager.GetFocusedElement();
    if (!TreeHelper.IsChildOf(this, currentFocusedElement as DependencyObject))
    {
        HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
        if (parentContainer != null)
        {
            if (CanNavigateOnMouseDown(e.OriginalSource as DependencyObject))
                this.Focus();
            else
            {
                e.Handled = true; //in that case we do not want that the event goes further
                FrameworkElement focusElement = 
                   FocusManager.GetFocusedElement() as FrameworkElement;
                if (!TreeHelper.IsChildOf(parentContainer, focusElement))
                    parentContainer.ResumeEdit();
            }
            
        }
    }
}

测试我们的更改

让我们通过重做本章开始时所做的测试来检验我们的更改。让我们看看,当一个单元格正在被编辑且无法提交时,如果我们点击按钮会发生什么。

  • 启动应用程序
  • 编辑 FirstName3 单元格。
  • 删除单元格内容。
  • 点击页面顶部的按钮。

按钮获得焦点,并且由于单元格无法提交,FirstName3 单元格周围会显示一个红色边框。现在让我们点击网格中的某个位置(但不是 FirstName3 单元格)。这一次,FirstName3 单元格被聚焦并再次进入编辑状态。这是我们想要的行为。让我们尝试按 Tab 键。再次点击按钮然后按 Tab 键。FirstName3 单元格被聚焦并再次进入编辑状态。这也是我们想要的行为。最后,让我们看看当一个项目无法被验证时会发生什么。

  • 在 FirstName3 单元格中输入一些内容,以便它可以被提交。
  • 编辑 City 3 单元格
  • 键入 New York
  • 点击页面顶部的按钮。

项目被验证。如果我们点击网格或者按 Tab 键,无效的项目会获得焦点。

加强导航过程

引言

为了让我们的网格在单元格编辑时能按我们想要的方式工作,我们不得不在代码的多个地方进行修改,以便在一个单元格无法提交或一个项目无法验证时取消导航。这显然是我们网格代码中最薄弱的一点。

  • 如果我们需要改变导航的行为,我们将不得不关注代码中可能取消导航的各个地方。
  • 没有什么能阻止程序员以错误的方式使用网格,例如,直接在单元格上设置焦点(使用单元格的 Focus 方法),而不使用容器项目的 FocusCell 方法。在这种情况下,导航检查(当前单元格提交和项目验证)将被绕过。

让我们在代码中添加一些“栅栏”,以防止对网格的错误使用。

禁止聚焦处于编辑模式的单元格

我们必须注意的第一件事是,如果另一个单元格正在被编辑(且尚未提交),绝不允许某个单元格成为当前单元格。让我们修改 Cell 类的 OnGotFocus 方法。

protected override void OnGotFocus(RoutedEventArgs e)
{
    base.OnGotFocus(e);

    HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
    if ((parentContainer != null) && 
        (parentContainer.CurrentEditedCell != null) && 
        (parentContainer.CurrentEditedCell != this))
        throw new InvalidOperationException(
                 "Inavalid navigation operation");

    if ((this.CellState != CellState.Focused) && 
        (this.CellState != CellState.Edited))
    {
        VisualStateManager.GoToState(this, "Focused", true);
        this.CellState = CellState.Focused;

        if (parentContainer != null)
        {
            parentContainer.CurrentCellName = this.Name;
            parentContainer.EnsureCellIsVisible(this);
        }
    }
}

禁止聚焦项目

如果当前项目是脏的(并且尚未验证),我们也绝不允许另一个项目成为当前项目。让我们重写 ContainerItem 分部类的 OnGotFocus 方法来避免这种情况。

protected override void OnGotFocus(RoutedEventArgs e)
{
    HandyContainer parentContainer = HandyContainer.GetParentContainer(this);
    if (parentContainer != null)
    {
        if ((parentContainer.CurrentDirtyItem != null) && 
                 (parentContainer.CurrentDirtyItem != this))
            throw new InvalidOperationException(
                      "Invalid navigation operation");
    }

    base.OnGotFocus(e);
}

防止聚焦项目或单元格

在以上两个步骤中,我们通过在不允许的情况下抛出异常来阻止单元格和项目获得焦点。这是我们工作的“抑制”部分。现在让我们实现“预防”部分。当一个项目的单元格进入编辑状态时,我们将通过将其 IsTabStop 属性设置为 false 来防止其他单元格获得焦点;当一个项目变为脏的或被编辑时,我们也将通过将其 IsTabStop 属性设置为 false 来防止其他项目获得焦点。让我们向 Cell 类添加一个 FocusProtect() 和一个 FocusUnProtect() 方法。

private bool isFocusProtected;
private bool isTabStop;
internal void FocusProtect()
{
    if (isFocusProtected)
        return;

    isTabStop = this.IsTabStop;
    this.IsTabStop = false;
    isFocusProtected = true;
}

internal void FocusUnProtect()
{
    if (!isFocusProtected)
        return;

    IsTabStop = isTabStop;
    isFocusProtected = false;
}

当我们希望防止一个单元格获得焦点时,将调用 FocusProtect 方法;当该单元格可以再次获得焦点时,将调用 FocusUnProtect 方法。我们再向 ContainerItem 类添加一个 FocusProtect 和一个 FocusUnProtect 方法。

private bool isFocusProtected;
private bool isTabStop;
internal void FocusProtect(Cell skipedCell)
{
    if (isFocusProtected)
        return;

    isTabStop = this.IsTabStop;
    this.IsTabStop = false;
    List<Cell> cells = this.GetCells();
    foreach (Cell cell in cells)
        if (cell != skipedCell)
            cell.FocusProtect();

    isFocusProtected = true;
}

internal void FocusUnProtect(Cell skipedCell)
{
    if (!isFocusProtected)
        return;

    IsTabStop = isTabStop;
    List<Cell> cells = this.GetCells();
    foreach (Cell cell in cells)
        if (cell != skipedCell)
            cell.FocusUnProtect();

    isFocusProtected = false;
}

让我们向 HandyContainer 类添加一个 ItemsFocusProtect 和一个 ItemsFocusUnProtected 方法。ItemsFocusProtect 方法将“保护”除脏的或被编辑的项目之外的所有项目。被编辑或脏的项目将单独处理。

private void ItemsFocusProtect()
{
    Panel itemsHost = this.ItemsHost;
    UIElementCollection itemsHostChildren = itemsHost.Children;
    foreach (ContainerItem item in itemsHostChildren)
        if ((item != CurrentDirtyItem) && (item != CurrentEditedItem))
            item.FocusProtect(null);
}

private void ItemsFocusUnProtect()
{
    Panel itemsHost = this.ItemsHost;
    UIElementCollection itemsHostChildren = itemsHost.Children;
    foreach (ContainerItem item in itemsHostChildren)
        if ((item != CurrentDirtyItem) && (item != CurrentEditedItem))
            item.FocusUnProtect(null);
}

现在让我们在需要时调用这些方法。当一个单元格被编辑时,我们必须防止其他单元格获得焦点,并且必须防止项目获得焦点。让我们修改 HandyContainer_OnCurrentCellBeginEdited 方法。

internal void _OnCurrentCellBeginEdited(EventArgs e)
{
    CurrentEditedCell = this.GetCurrentCell();
    CurrentEditedItem = ContainerItem.GetParentContainerItem(CurrentEditedCell);
    CurrentEditedItem.FocusProtect(CurrentEditedCell);
    if (CurrentDirtyItem == null)
        ItemsFocusProtect();
    OnCurrentCellBeginEdited(e);

}

注意,只有当当前项目不为脏时,才会调用 ItemsFocusProtect 方法。确实,如果项目已经是脏的,这意味着 ItemsFocusProtect 方法之前已经被调用过了。当单元格不再被编辑时,我们可以移除对其他单元格的“焦点保护”。只有当当前项目不为脏时,项目才会被取消保护。让我们修改 HandyContainer_OnCurrentCellEndEdit 方法。

internal void _OnCurrentCellEndEdit(EventArgs e)
{
    CurrentEditedItem.FocusUnProtect(CurrentEditedCell);
    if (currentDirtyItem == null)
        ItemsFocusUnProtect();
    CurrentEditedCell = null;
    CurrentEditedItem = null;
    OnCurrentCellEndEdit(e);
}

最终,当一个项目不再是脏的时,我们可以从其他项目中移除“焦点保护”。让我们修改 HandyContainerCurrentDirtyItem 方法。

internal ContainerItem CurrentDirtyItem
{
    get { return currentDirtyItem; }
    set
    {
        if (currentDirtyItem != value)
        {
            if (value == null)
                ItemsFocusUnProtect();

            currentDirtyItem = value;                    
            OnIsCurrentItemDirtyChanged(EventArgs.Empty);
        }

    }
}

我们添加的栅栏并不完美。如果你希望在单元格被编辑时能够更改单元格或项目的 IsTabStop 属性,你将需要完善我们添加的方法。然而,道路已经指明。让我们试试我们的改动。

  • 启动应用程序
  • 编辑 FirstName3 单元格。
  • 按 Tab 键

LastName3 单元格没有获得焦点,而是焦点被发送到了应用程序“外部”。其他导航键工作正常。让我们看看当我们按 Tab 键时会发生什么。

  • RowSpatialNavigator 类的 KeyDown 方法的开头添加一个断点。
  • 编辑 FirstName3 单元格。
  • 按 Tab 键

断点从未被命中。当 FirstName3 单元格被编辑时,所有其他单元格和项目的 IsTabStop 属性被设置为 false。当我们按 Tab 键时,由于没有控件可以获得焦点,Silverlight 立即将焦点移动到下一个可以获得焦点的“东西”上:浏览器。

如果我们在应用程序中网格“之后”添加一个新按钮,问题就消失了。在这种情况下,当我们编辑 LastName3 单元格时,有一个可以获得焦点的控件(新按钮),焦点处理过程会如期进行。

为了解决这个问题,我们将在网格中添加一个假的控件。当除了被编辑的单元格之外没有其他单元格或项目可以获得焦点时,我们将使这个控件出现。这样,当按 Tab 键时,总会有一个控件可以获得焦点。这个技巧将使我们能够解决我们的问题。

首先让我们创建我们的控件。在 GoaOpen\Extensions\Grid 文件夹中,添加一个 GridLastFocusControl 类。

using System;
using System.Windows;
using System.Windows.Controls;

namespace Open.Windows.Controls
{
    public class GridLastFocusControl: Control
    {
        public GridLastFocusControl()
        {
            this.DefaultStyleKey = typeof(GridLastFocusControl);
        }

        protected override void OnGotFocus(RoutedEventArgs e)
        {
            throw new InvalidOperationException(
                       "GridLastFocusControl cannot get focus");
        }
    }
}

在我们的 generic.xaml 文件中,为我们的 GridLastFocusControl 添加一个空样式。

<Style TargetType="o:GridLastFocusControl">
</Style>

generic.xaml 文件中,我们还要修改 GridBodyStyle,以便将一个 GridLastFocusControl 添加到 HandyContainer 的控件模板中。

<Style x:Key="GridBodyStyle" TargetType="o:HandyContainer">
    ...
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="o:HandyContainer">
                <Border 
                    Background="{TemplateBinding Background}"
                    BorderBrush="{TemplateBinding BorderBrush}" 
                    BorderThickness="{TemplateBinding BorderThickness}" 
                    Padding="{TemplateBinding Padding}">
                    <Grid x:Name="ELEMENT_Root">
                        <g:Scroller 
                            x:Name="ElementScroller"
                            Style="{TemplateBinding ScrollerStyle}" 
                            Background="Transparent"
                            BorderThickness="0"
                            Margin="{TemplateBinding Padding}">
                            <g:GItemsPresenter
                                x:Name="ELEMENT_ItemsPresenter"
                                Opacity="{TemplateBinding Opacity}"
                                Cursor="{TemplateBinding Cursor}"
                                HorizontalAlignment = 
                                  "{TemplateBinding HorizontalContentAlignment}"
                                VerticalAlignment = 
                                  "{TemplateBinding VerticalContentAlignment}"/>
                        </g:Scroller>
                        <o:GridLastFocusControl Width="0" Height="0" 
                          Visibility="Collapsed" 
                          x:Name="LastFocusControl"/>
                    </Grid>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

让我们重写 HandyContainer 分部类的 OnApplyTemplate 方法,以便获得对 LastFocusControl 的引用。

Control lastFocusControl;
public override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    lastFocusControl = this.GetTemplateChild("LastFocusControl") as Control;
}

最后,让我们修改 HandyContainer 类的 ItemsFocusProtectItemsFocusUnProtect 方法,以便在需要时将 lastFocusControlVisibility 属性值在 VisibleCollapse 之间切换。

private void ItemsFocusProtect()
{
    Panel itemsHost = this.ItemsHost;
    UIElementCollection itemsHostChildren = itemsHost.Children;
    foreach (ContainerItem item in itemsHostChildren)
        if ((item != CurrentDirtyItem) && (item != CurrentEditedItem))
            item.FocusProtect(null);

    if (lastFocusControl != null)
        lastFocusControl.Visibility = Visibility.Visible;            
}

private void ItemsFocusUnProtect()
{
    Panel itemsHost = this.ItemsHost;
    UIElementCollection itemsHostChildren = itemsHost.Children;
    foreach (ContainerItem item in itemsHostChildren)
        if ((item != CurrentDirtyItem) && (item != CurrentEditedItem))
            item.FocusUnProtect(null);

    if (lastFocusControl != null)
        lastFocusControl.Visibility = Visibility.Collapsed;            
}

让我们试试我们的改动。

  • 启动应用程序
  • 编辑 FirstName3 单元格。
  • 按 Tab 键

这一次,LastName3 单元格获得了焦点。

创建一个 CanNavigate 方法

另一种改进代码的方法是创建一个“通用”的 CanNavigate 方法,并用对该方法的调用替换我们在不同地方进行的单独检查。

提醒一下,我们在以下地方检查是否需要取消导航:

  • 当用户点击一个单元格时
  • 当用户点击一个 CheckBoxCell
  • 当一个项目被点击时
  • GridSpatialNavigator
  • RowSpatialNavigator
  • FocusCell 方法中

我们不会在本教程中实现这个方法。这对读者来说是一个很好的练习。如果你需要深入改变网格处理导航的方式,我们建议你先自己实现一个通用的 CanNavigate 方法。如果你能完成这个任务,那将意味着你已经有足够的知识来改变导航的工作方式。

8. 加速网格

引言

到目前为止,我们对网格进行了修改,但没有考虑它们可能对网格性能产生的负面影响。这并非没有后果。事实上,我们的网格几乎比第一步结束时慢了两倍!它的启动更慢,滚动也更慢。性能下降主要来自一个瓶颈:在 TextCellControlTemplate 中使用了 TextBox。按照我们实现 TextCellControlTemplate 的方式,网格内显示的每个 TextCell 都会创建一个 TextBox。这意味着,如果网格显示例如30行(即项目),每行包含6个 TextCell,那么网格将需要一次性创建和管理180个文本框!事实上,它需要管理的文本框甚至更多,因为网格维护着一些行的缓存。

从控件模板中移除 TextBox

TextCellControlTemplate 中添加一个 TextBox 是实现 TextCellEdited 状态的一种简单快捷的方法。然而,并非必须这样做。让我们从 ControlTemplate 中移除 TextBox。我们将在需要时从 TextCell 代码中动态添加它。我们还需要移除使 TextBox 在“EditedVisualState 中可见的 StoryBoard

<Style TargetType="o:TextCell">
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="BorderBrush" 
      Value="{StaticResource DefaultListControlStroke}"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="Foreground" 
      Value="{StaticResource DefaultForeground}"/>
    <Setter Property="HorizontalContentAlignment" 
      Value="Stretch" />
    <Setter Property="VerticalContentAlignment" 
      Value="Stretch" />
    <Setter Property="Cursor" Value="Arrow" />
    <Setter Property="Padding" Value="2,2,1,1" />
    <Setter Property="Width" Value="100"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="o:TextCell">
                <Grid>
                    <vsm:VisualStateManager.VisualStateGroups>
                        <vsm:VisualStateGroup x:Name="CommonStates">
                            <vsm:VisualState x:Name="Standard"/>
                            <vsm:VisualState x:Name="Focused">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="FocusElement" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                            <vsm:VisualState x:Name="Edited">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                           Storyboard.TargetName="TextElement" 
                                           Storyboard.TargetProperty="Visibility" 
                                           Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Collapsed</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="FocusElement" 
                                        Storyboard.TargetProperty="Visibility" 
                                        Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                        <vsm:VisualStateGroup x:Name="ValidStates">
                            <vsm:VisualState x:Name="Valid"/>
                            <vsm:VisualState x:Name="NotValid">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                            Storyboard.TargetName="ValidElement" 
                                            Storyboard.TargetProperty="Visibility" 
                                            Duration="0">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </vsm:VisualState>
                        </vsm:VisualStateGroup>
                    </vsm:VisualStateManager.VisualStateGroups>
                    <Rectangle 
                        Name="ValidElement" 
                        Stroke="Red" 
                        StrokeThickness="2"
                        IsHitTestVisible="false"
                        Margin="0,1,1,0"
                        Visibility="Collapsed"/>
                    <Grid x:Name="TextContainerElement">
                        <TextBlock 
                            x:Name="TextElement" 
                            Text="{TemplateBinding Text}"
                            Margin="{TemplateBinding Padding}"
                            HorizontalAlignment=
                              "{TemplateBinding HorizontalContentAlignment}"
                            VerticalAlignment=
                              "{TemplateBinding VerticalContentAlignment}"/>
                    </Grid>
                    <Rectangle Name="FocusElement" 
                               Stroke="{StaticResource DefaultFocus}" 
                               StrokeThickness="1" 
                               IsHitTestVisible="false" 
                               StrokeDashCap="Round" 
                               Margin="0,1,1,0" 
                               StrokeDashArray=".2 2" 
                               Visibility="Collapsed" />
                    <Rectangle Name="CellRightBorder" 
                               Stroke="{TemplateBinding BorderBrush}" 
                               StrokeThickness="0.5" 
                               Width="1" 
                               HorizontalAlignment="Right"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

请注意,我们给包含 TextElementGrid 命名了。这是因为我们将需要对这个网格的引用,以便能够动态地在其中添加 TextBox

从代码创建 TextBox

让我们修改单元格的代码,以便在单元格被编辑时动态创建 TextBox

OnApplyTemplate 方法

我们首先需要修改 OnApplyTemplate 方法,以移除对不再存在的 TextBoxElement 的引用。我们还必须添加一个对新的 TextContainerElement 的新引用。

private Panel textContainerElement;
public override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    textContainerElement = GetTemplateChild("TextContainerElement") as Panel;
}

OnBeginEdit 方法

OnBeginEdit 方法被调用时,我们必须动态地创建 TextBox 并将其插入到 TextContainerElement 中。

private TextBox textBoxElement;
protected override bool OnBeginEdit()
{
    if (textContainerElement != null)
    {
        if (textBoxElement == null)
        {
            textBoxElement = new TextBox();
            textBoxElement.Style = 
              ResourceHelper.FindResource("CellTextBoxStyle") as Style;
            textBoxElement.Text = this.Text;
            textBoxElement.Margin = this.Padding;
            textBoxElement.HorizontalAlignment = this.HorizontalContentAlignment;
            textBoxElement.VerticalAlignment = this.VerticalContentAlignment;
            textBoxElement.Foreground = this.Foreground;
            textBoxElement.TextChanged += 
              new TextChangedEventHandler(textBoxElement_TextChanged);

            textContainerElement.Children.Add(textBoxElement);
        }
                        
        if (textBoxElement.Focus())
        {
            textBoxElement.SelectionStart = textBoxElement.Text.Length;
            return true;
        }
    }

    return false;
}

在上面的代码中,一旦我们创建了 TextBox 元素,我们就会初始化它的一些属性。然后,我们将 TextBox 添加到 textContainerElement 面板的子集合中。

另外请注意使用 ResourceHelper.FindResource 方法来查找要应用于 TextBoxStyle 属性的 CellTextBoxStyle 样式。

OnCommitEdit 和 OnCancelEdit 方法

修改 OnCommitEditOnCancelEdit 方法以在编辑过程完成后移除 TextBox 可能会很诱人。然而,当 OnCommitEdit 被调用时,编辑过程只是部分完成。如果你看一下 Cell 类的 CommitEdit 方法,你会看到在调用 OnCommitEdit 方法之后会检查 isBindingValid 标志(绑定验证过程的结果)。在代码的这一点上,提交过程仍然可以被取消。此外,OnCommitEdit 方法和 OnCancelEdit 方法只有在单元格是脏的情况下才会被调用。

AfterEdit 方法

让我们在 Cell 类中添加一个 AfterEdit 虚方法。

protected virtual void AfterEdit()
{
}

让我们在 CommitEdit 方法和 CancelEdit 方法的末尾调用 AfterEdit 方法。

internal bool CommitEdit(bool keepFocus)
{
    if (this.CellState != CellState.Edited)
        return true;

    if (IsDirty)
    {
        HandyContainer parentContainer = 
            HandyContainer.GetParentContainer(this);
        if (parentContainer != null)
        {
            CellValidatingEventArgs cancelEventArgs = 
              new CellValidatingEventArgs(this.CurrentValue, 
              this.DirtyValue, false);
            parentContainer._OnCurrentCellValidating(cancelEventArgs);
            if (cancelEventArgs.Cancel)
            {
                IsValid = false;
                return false;
            }

            OnCommitEdit();

            if (!isBindingValid)
            {
                IsValid = false;
                return false;
            }

            IsValid = true;
            IsDirty = false;

            parentContainer._OnCurrentCellValidated(EventArgs.Empty);
        }
    }

    if (this.CellState == CellState.Edited)

    {
        if (keepFocus)
        {
            VisualStateManager.GoToState(this, "Focused", true);
            this.CellState = CellState.Focused;
            bool gotFocus = this.Focus();
            Debug.Assert(gotFocus);
        }
        else
        {
            VisualStateManager.GoToState(this, "Standard", true);
            this.CellState = CellState.Standard;
        }

        AfterEdit();

        HandyContainer parentContainer = 
              HandyContainer.GetParentContainer(this);
        parentContainer._OnCurrentCellEndEdit(EventArgs.Empty);

    }

    return true;
}

internal virtual bool CancelEdit(bool keepFocus)
{
    if (this.CellState != CellState.Edited)
        return true;

    if (IsDirty)
    {
        OnCancelEdit();

        IsValid = true;
        IsDirty = false;
        ContainerItem parentRow = ContainerItem.GetParentContainerItem(this);
        if (parentRow != null)
            parentRow.IsDirtyCount--;
    }

    if (this.CellState == CellState.Edited)
    {
        if (keepFocus)
        {
            VisualStateManager.GoToState(this, "Focused", true);
            this.CellState = CellState.Focused;
            bool gotFocus = this.Focus();
            Debug.Assert(gotFocus);

        }
        else
        {
            VisualStateManager.GoToState(this, "Standard", true);
            this.CellState = CellState.Standard;
        }

        AfterEdit();

        HandyContainer parentContainer = 
           HandyContainer.GetParentContainer(this);
        parentContainer._OnCurrentCellEndEdit(EventArgs.Empty);

    }

    return true;
}

重写 AfterEdit 方法

现在让我们在 TextCell 类中重写 AfterEdit 方法,并从 TextContainerElement 中移除 TextBox

protected override void AfterEdit()
{
    if ((textBoxElement != null) && (textContainerElement != null))
    {
        if (textBoxElement == FocusManager.GetFocusedElement())
            this.Focus();
        textContainerElement.Children.Remove(textBoxElement);
        textBoxElement.TextChanged -= 
          new TextChangedEventHandler(textBoxElement_TextChanged);

        textBoxElement = null;
    }
}

9. 完善 TextCell

双击支持

引言

大多数情况下,Web 应用程序不支持双击。然而,大多数用户希望当他们双击一个 TextCell 时,单元格的全部内容能被选中。让我们来实现这个功能。

模拟双击事件

由于 Silverlight 没有实现任何双击事件,我们将不得不自己模拟它。为了实现这个功能,我们将需要修改 TextCell 类的 OnMouseLeftButtonDown 方法。

private long lastTick;
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
    base.OnMouseLeftButtonDown(e);

    if (this.CellState == CellState.Focused)
        BeginEdit();

    if (this.CellState == CellState.Edited)
    {
        e.Handled = true;
        //we do not want that the ContainerItem
        //OnMouseLeftButtonDown is called

        if (DateTime.Now.Ticks - lastTick < 2000000)
        //It is a double click
            DoubleClick();
    }

    lastTick = DateTime.Now.Ticks;        
}

每次调用此方法时,我们将方法被调用的时间存储在 lastTick 字段中。如果该方法在短时间内被调用两次,我们将认为该单元格被双击了。实现一个完整的双击支持将需要更多的检查和条件。例如,在上面的代码中,我们没有处理用户在短时间内点击单元格三次的情况。这算是一次双重双击吗?然而,我们的基本实现足以处理我们的特定情况。

DoubleClick 方法

DoubleClick 方法的实现很简单。

private void DoubleClick()
{
    if (textBoxElement != null)
        textBoxElement.SelectAll();
}

让我们试试我们的改动。

  • 启动应用程序
  • 点击一个单元格使其成为当前单元格。
  • 双击另一个单元格。

单元格进入编辑状态,并且单元格的文本被选中。然而,以下情况不起作用:

  • 点击一个单元格使其成为当前单元格。
  • 双击当前单元格。

在这种情况下,单元格的文本没有被选中。这是因为 textBoxElement 在两次点击之间被显示出来了。在第一次调用 OnMouseLeftButtonDown 方法时,BeginEdit 方法被调用,并且 TextBox 被显示在单元格内。然后,OnMouseLeftButtonDown 方法没有被第二次调用,因为第二次点击被 TextBox 而不是 TextCell 捕获了。

CellTextBox

为了处理 TextBox 在两次点击之间显示的情况,让我们创建我们自己的 TextBox

using System.Windows.Controls;
using System;
namespace Open.Windows.Controls
{
    public class CellTextBox: TextBox
    {
        internal long LastTick
        {
            get;
            set;
        }

        protected override void OnMouseLeftButtonDown(
                  System.Windows.Input.MouseButtonEventArgs e)
        {
            base.OnMouseLeftButtonDown(e);

            if (DateTime.Now.Ticks - LastTick < 2000000)
            //It is a double click
                DoubleClick();

            LastTick = DateTime.Now.Ticks;
        }

        private void DoubleClick()
        {
            this.SelectAll();
        }

    }
}

CellTextBox 实现了一个 LastTick 属性,我们将用 TextCelllastTick 值来填充它。让我们修改 TextCelltextBoxElement 字段和 OnBeginEdit 方法,以便在我们的 TextCell 中使用 CellTextBox 而不是标准的 TextBox

private CellTextBox textBoxElement;
protected override bool OnBeginEdit()
{
    if (textContainerElement != null)
    {
        if (textBoxElement == null)
        {
            textBoxElement = new CellTextBox();
            textBoxElement.Style = 
              ResourceHelper.FindResource("CellTextBoxStyle") as Style;
            textBoxElement.Text = this.Text;

让我们也修改 TextCell 类的 OnMouseLeftButtonDown 方法,以便初始化 CellTextBoxLastTick 属性。

protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
    base.OnMouseLeftButtonDown(e);

    if (this.CellState == CellState.Focused)
        BeginEdit();

    if (this.CellState == CellState.Edited)
    {
        e.Handled = true;
        //we do not want that the ContainerItem OnMouseLeftButtonDown is called

        if (DateTime.Now.Ticks - lastTick < 2000000) //It is a double click
            DoubleClick();
    }

    lastTick = DateTime.Now.Ticks;
    if (textBoxElement != null)
        textBoxElement.LastTick = lastTick;
}

让我们启动应用程序来检查我们的更改。

  • 点击一个单元格使其成为当前单元格。
  • 双击当前单元格。

这一次,单元格的文本被选中了。

10. 使用编辑和验证功能

单元格的编辑过程

为了帮助你理解和使用网格的编辑过程,这里是它的示意图。

  • 绿色圆圈是可能的起点。
  • 绿色矩形是方法和内部过程。
  • 蓝色矩形是事件。
  • 一个过程由一个红色圆圈结束。

图中只显示了最常见的事件。

CellEditingProcess.jpg

项目的验证过程

ItemValidation.jpg

11. 结论

本文使我们能够在网格主体中实现编辑和验证功能。我们做了几个假设,例如:

  • 验证必须在焦点离开单元格时立即进行。
  • 如果一个单元格或项目无法被验证,则不允许导航到另一个单元格或项目。
  • 只有当前单元格可以被编辑。
  • 当用户点击一个 TextCell 时,它成为当前单元格。用户必须再次点击该单元格才能对其进行编辑。
  • 当用户点击一个 CheckBoxCell 时,它成为当前单元格并立即进入编辑状态。
  • ...

你可以修改这些默认行为(以及其他行为),以使数据网格符合你的愿望。

12. 历史记录

2009年6月6日

  • 更新了 AfterEdit 方法(添加了关于焦点的条件)。
© . All rights reserved.