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

WPF 开发人员必读:WPF 控件测试平台

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2022 年 3 月 21 日

公共领域

12分钟阅读

viewsIcon

8699

WpfTestbench 帮助您用少量代码编写 WPF 控件的复杂测试窗口。

引言

WpfControlTestbench 帮助您快速编写复杂的测试窗口,用于测试您的控件或您想深入了解其行为的任何控件。只需十几行 XAML 代码即可创建如下 Window

它在左下角显示您要测试的控件,在 Window 的上半部分是用于您控件所有属性的 Controls。您可以在运行时更改它们的值。在右下角,您会看到一个跟踪查看器,显示任何已更改属性的值,以及持有您的控件的 WPF 容器和您的控件如何进行控件的布局。

WPF 控件支持令人惊叹的布局(MarginBorderPaddingWidth/Height、对齐等)功能,因此开发起来颇具挑战性。更困难的是测试控件在众多可能场景下的行为是否正确。WpfControlTestbench 让您可以直观地、交互式地进行测试,您可以立即看到属性值更改对控件的效果。开箱即用,WpfControlTestbench 会显示从 FrameworkElementControl 继承的所有重要属性。当然,您可以轻松添加更多控件来检查您控件的特定属性。

深入了解您的控件及其 WPF 父控件的交互

控件的行为取决于其在逻辑树中的 Parent,即它们被放置在哪一个 WPF ContentControlPanel 中。Canvas 可能会提供您的控件所需的所有空间,而 StackPanel 会限制您的控件的宽度或高度。要让不同 WPF ContentControls 与您的控件之间的交互正常工作可能相当具有挑战性。幸运的是,WpfControlTestbench 可以非常轻松地在运行时将您的控件放置到不同的 ContentControls 中。

调试测量、排列和渲染问题

编写直接渲染到屏幕的 WPF Control 是一种“黑魔法”,但它提供了极大的灵活性和最高的速度。使用 Visual Studio 调试它很困难,因为无法使用断点,当 VS 尝试切换到您的窗口时,它们会被不断触发,然后调用您的方法,会停在断点处并再次显示 VS。您可以使用 System.Diagnostics.Debug.WriteLine() 来跟踪这些信息,但这可能是一项繁重的工作,而且您无法为父控件执行此操作。但问题通常是由与父控件的交互引起的。但不要害怕,WpfControlTestbench 可以为您跟踪所有这些信息,甚至更多。

在上图中,您可以看到 CanvasContainer,它是要测试的 Control(称为 StackPanel)的父控件。父控件获得 877 像素的水平空间和 381 像素的垂直空间来排列其子控件。子 StackPanel 只收到 0 像素,因为在测量(图中未显示)过程中,它没有请求任何空间,因为它为空。这里有趣的是,子控件在父控件的 Arrange() 调用期间就已经执行了 Render()

WpfControlTestbench 使控件的内部工作透明化。它实时跟踪属性何时被更改,以及父控件和您的控件如何就此更改进行布局交互。

编写自己的控件时,至关重要的是它应与 Microsoft 的控件行为相似。但 Microsoft 的文档缺乏必要的信息。要检查 Microsoft 控件的行为,请将其托管在 WpfControlTestbench 中。为自己的控件执行相同的操作,然后验证您的控件在各种场景下都与 Microsoft 的控件行为相同。

控件尺寸和定位

编写一个控件使其内容占满所有可用空间是困难的,因为可用空间取决于许多因素,例如主机提供的空间、边距、对齐方式等等。子控件所需的屏幕宽度的一个简单公式如下所示:

Required Width = LeftMargin + LeftBorder + LeftPadding + Content Width + 
                 RightPadding + RightBorder + RightMargin

当然,所需的宽度通常与主机可提供的宽度不同。

  • 空间不足时会发生什么?空间过多时会发生什么?
  • 当设置 Width 属性(请注意,它不在上面的公式中)时会发生什么?
  • Width 未定义时会发生什么?
  • HorizontalAlignmentLeft 更改为 CenterRightStretch 时会发生什么?
  • Font.Size 更改时会发生什么?
  • 是父控件还是您的控件处理 MarginPadding

答案

  • 空间不足:如果您的控件渲染到给定空间之外,它将取决于剪裁是否显示。空间过多:取决于对齐方式。如果不拉伸,父控件将相应地放置内容。如果拉伸,子控件应使用所有可用空间。
  • 当设置 Width 时,您的控件应精确使用该空间。如果对齐方式为拉伸,则不应发生拉伸,但父控件会居中放置您的控件。
  • Width 未定义时,您的控件应自行确定需要多少空间。
  • HorizontalAlignmentLeft 更改为 CenterRight 对您的控件无关紧要,渲染效果相同,但父控件的放置方式会不同。将 HorizontalAlignmentLeft 更改为 Stretch 会要求您的控件渲染时使用所有可用大小,而不仅仅是它认为应该使用的那些大小。
  • Font.Size(或 Font.Family 或……)更改时,您的控件的内容可能需要更多或更少的空间,即需要执行 Measure()Arrange()Render()
  • 您的控件的主机处理 Margin。您的控件需要处理 BorderPadding

如果您知道所有这些问题的答案,那真是恭喜您。如果不知道,很遗憾,您很难在 Microsoft 的文档中找到答案。但是,作为控件的开发者,您需要精确理解这一切是如何工作的。

要快速获得此类问题的答案,请使用 WpfControlTestbench,更改一些值,然后观察 Microsoft 控件的反应。然后为您的控件实现相同的行为。TestBench 中的 Show Template 按钮也很有助于更好地理解 Microsoft 控件,它会显示 ControlTemplate XAML。如果您真的想了解细节,还可以查看 GitHub 上的 WPF 源代码:https://github.com/dotnet/wpf。但请注意,它可能很复杂。

由于正确处理尺寸和定位需要检查许多场景,而且多一个或少一个像素都可能产生差异,WpfControlTestbench 可以轻松更改宽度、对齐方式等。它显示 MarginBorderPadding 的虚线,这些虚线可以用鼠标移动。甚至总可用空间也可以通过拖动分隔线轻松更改。

测试标准属性

您也可以通过键盘输入值来更改属性,这可能更精确,但耗时也更多。

如果您的控件继承自 FrameworkElementWpfControlTestbench 会显示 WidthHeightAlignmentMinMaxMargin 等属性,用户可以更改这些属性。DesiredWidthRenderWidth 是计算值,无法更改。如果您的控件继承自 Control,还会显示 BorderPadding 的属性。

您还可以更改颜色和字体

您可以更改 BackgroundForeground(即 Font)和 Border 的颜色。更改其他字体属性很有趣,因为您的控件所需的尺寸可能还取决于 FontFamilyFont.Size

点击 Reset 会将所有标准属性的值恢复到窗口打开时的值。

标准测试

由于 Control 有如此多的属性,并且应该测试所有可能的组合值,因此需要执行许多测试。如果您手动设置这些值,您可能会花费数小时,并在许多天里一次又一次地重复。但不要害怕,WpfControlTestbench 在这里也能满足您的需求。它提供了 111 个预定义的测试设置,涵盖了标准属性值的各种有趣组合。最棒的是,您可以在一两分钟内完成所有这些测试!

您只需不断点击 **Next** (Alt + N)。通常,一眼就能看出是否一切正常。当然,您也可以轻松地为您的控件特有的属性添加自己的测试,这将在下面详细介绍。

试用 WpfControlTestbench

在继续阅读如何编写自己的测试窗口(这是一个枯燥的阅读过程)之前,我建议您从 Github 下载 WpfControlTestbench 并试用一下。我用 VS 2022 和 .NET 6 编写了它。如果您还没有使用 VS 2022,也可以安装一个。您可以在 PC 上运行不同的 VS 版本,它们不会相互干扰。

当您启动 WpfControlTestbench 时,会出现一个小窗口

左列有两个 WPF 提供的控件的测试,右列是我编写的一个控件。您可以编写自己的应用程序或将测试窗口添加到此窗口中。我希望其他人能为其他 WPF 控件编写测试窗口,并在 Github 上与我们分享。可以看看 TextBox 测试,它相当复杂,但我花了 2 天时间才写完。

准备您的控件以在 TestBench 中使用

只有对如何编写自己的测试窗口的精细细节感兴趣时,才阅读本章和下一章。

在跟踪布局过程时,一个挑战是它发生在 Measure()Arrange() 方法中。由于这些不是事件,TestBench 无法跟踪它们的执行。为了实现这一点,您必须从您的控件继承一个新类,并重写 MeasureOverride() 等方法。

新类需要实现简单的 WpfControlTestbench 接口 ITraceNameIIsTracing 以支持跟踪。

/// <summary>
/// Provides a name for tracing
/// </summary>
public interface ITraceName {

  /// <summary>
  /// Name to be used for tracing
  /// </summary>
  string TraceName { get; }
}

/// <summary>
/// Control can decide if it should get traced
/// </summary>
public interface IIsTracing: ITraceName {

  /// <summary>
  /// Controls if trace should get written
  /// </summary>
  public bool IsTracing { get; set; }
}

跟踪方面有一个棘手的问题需要解决。跟踪应显示您的控件的构造何时开始,然后设置了哪些属性,最后显示控件构造完成的跟踪。这里的挑战是如何在构造函数执行之前写入跟踪?就像 StackPanel 的例子一样,没有办法在其构造函数的开头添加跟踪指令。在继承类的构造函数中编写开始跟踪也没有帮助,因为它只在 StackPanel 构造函数完成后执行。解决方案是使用以下继承:

YourControl => YourControlWithConstructor => YourControlTraced

public class YourControlWithConstructor: YourControl { 
  public YourControlWithConstructor(object? _) : base() { }
}

public class YourControlTraced: YourControlWithConstructor, ITraceName {

  public YourControlTraced() : this("YourControl") { }

  public YourControlTraced(string traceName) : 
         base(TraceWPFEvents.TraceCreateStart(traceName)) {
    TraceName = traceName;
    TraceWPFEvents.TraceCreateEnd(traceName);
   }
}

在 XAML 中,您的测试控件应如此放置:

<local:YourControlTraced"/>

此 XAML 创建了一个 C# 行,调用 YourControlTraced 的无参数构造函数,它按如下方式调用其他构造函数:

1) YourControlTraced()
     this("YourControl")
    
     2) YourControlTraced(string traceName) : 
        base(TraceWPFEvents.TraceCreateStart(traceName))
        
        3) YourControlWithConstructor(object? _) : 
           base()

           4)YourControl ()
  1. YourControlTraced 的无参数构造函数调用带有参数 TraceNameYourControlTraced 构造函数。请注意,WPF 的 Name 属性不能用于跟踪,因为它只在构造函数完成后才赋值。
  2. 带有 TraceNameYourControlTraced 构造函数通过调用 TraceWPFEvents.TraceCreateStart(traceName) 方法来跟踪构造函数的开始,该方法返回 null
  3. YourControlWithConstructor 构造函数有一个参数,但没有使用它!它最终调用 YourControl 构造函数。
  4. YourControl 构造函数在已经写入开始跟踪后执行。

以下是准备将 StackPanel 用于 TestBench 的完整代码示例:

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

namespace WpfTestbench {

  public class StackPanelWithConstructor: StackPanel {
    public StackPanelWithConstructor(object? _) : base() { }
  }

  public class StackPanelTraced: StackPanelWithConstructor, ITraceName {

    public string TraceName { get; private set; }

    public StackPanelTraced() : this("StackPanel", true) { }

    public StackPanelTraced(string traceName) : 
           base(TraceWPFEvents.TraceCreateStart(traceName)) {
      TraceName = traceName;
      TraceWPFEvents.TraceCreateEnd(traceName);
    }

    protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) {
      TraceWPFEvents.OnPropertyChanged(this, e, base.OnPropertyChanged, IsTracing);
    }

    protected override Size MeasureOverride(Size constraint) {
      return TraceWPFEvents.MeasureOverride(this, constraint, base.MeasureOverride, IsTracing); 
    }

    protected override Size ArrangeOverride(Size finalSize) {
      return TraceWPFEvents.ArrangeOverride(this, finalSize, base.ArrangeOverride, IsTracing);
    }

    protected override void OnRender(DrawingContext drawingContext) {
      TraceWPFEvents.OnRender(this, drawingContext, base.OnRender, IsTracing);
    }
  }
}

这里的特殊之处在于,在布局重写中调用了 TraceWPFEvents 方法,这些方法:

  1. 在跟踪中标记重写方法的开始
  2. 调用重写的方法
  3. 在跟踪中标记重写方法的结束

如果您想知道为什么不只写一行跟踪,原因在于,例如父控件的 Measure() 调用子控件的 Measure(),然后看起来是这样的:

Trc 17:54:44.785 GridStarContainer.Measure(605, 517)
Trc 17:54:44.786     StackPanel.Measure(585, 497)
Trc 17:54:44.786     StackPanel.Measure(0, 0) end
Trc 17:54:44.786 GridStarContainer.Measure(20, 20) end

正是这种显示父子控件交互的跟踪信息有助于解决布局问题。

编写自己的测试窗口

最终的测试窗口将是这样的:

只需几行 XAML 代码(参见 *ControlWindow.xaml*)即可完成。

您可以将任何内容添加到您的窗口中。在此示例中,窗口仅包含一个 TestBench,其中包含所有标准测试属性和跟踪器。您可以向 TestBench 添加:

  • TestProperties,您可以在其中添加诸如 StackPanelGrid 之类的元素,其中包含 LabelsTextBoxes 等,以测试您的控件的属性。
  • TestControl,您可以在其中放置要测试的控件。这里也是设置您的控件属性值的好地方。

注意:如果在 XAML 中更改 TestControl,则需要刷新设计器才能看到新控件。

在代码隐藏文件中,您只需编写几行代码:

using System.Windows;
using System.Windows.Data;
using System.Windows.Media;

namespace WpfTestbench {
  public partial class ControlWindow: Window {

    public ControlWindow() {
      InitializeComponent();

      WpfBinding.Setup(TextTextBox, "Text", TestControlTraced,
        ControlTraced.TextProperty, BindingMode.TwoWay);

      FillStandardColorComboBox.SetSelectedBrush(TestControlTraced.Fill??Brushes.Transparent);
      WpfBinding.Setup(FillStandardColorComboBox, "SelectedColorBrush", TestControlTraced,
        ControlTraced.FillProperty, BindingMode.TwoWay);
    }
  }
}

您所要做的就是让您的特殊属性正常工作,这通常可以通过在 XAML 或代码隐藏中定义绑定来完成。很简单,对吧?

我编写了 WpfBinding() 来使设置 WPF 绑定时的代码更整洁。

/// <summary>
/// Helper class for setting up WPF bindings
/// </summary>
public static class WpfBinding {

  /// <summary>
  /// Allows the setup of a WPF binding with 1 line of code
  /// </summary>
  public static BindingExpression Setup(
    object sourceObject, string sourcePath,
    FrameworkElement targetFrameworkElement, DependencyProperty tragetDependencyProperty,
    BindingMode bindingMode,
    IValueConverter? converter = null,
    string? stringFormat = null) 
  {
    var newBinding = new Binding(sourcePath) {
      Source = sourceObject,
      Mode = bindingMode,
      Converter = converter,
      StringFormat = stringFormat
    };
    return (BindingExpression)targetFrameworkElement.SetBinding
                              (tragetDependencyProperty, newBinding);
  }
}

添加自己的测试

您可以在测试窗口的构造函数中添加类似这样的行:

TestBench.TestFunctions.Add(("Green Fill", fillGreen));
TestBench.TestFunctions.Add(("Red Fill", 
          ()=>{ TestControlTraced.Fill = Brushes.Red; return null;}));
TestBench.TestFunctions.Add(("Width", testWidth));
TestBench.TestFunctions.Add(("Reset Properties", resetProperties));

TestFunctionsTestBench 中的一个 List。每个条目都是一个测试。此时,它是空的。TestBench 稍后会添加其 100 多个测试。

public readonly List<(string Name, Func<Action?> Function)> TestFunctions;

测试由名称和一个执行测试的函数组成,该函数可能返回另一个 Action,该 Action 将验证测试是否成功或抛出异常。由于布局在稍后发生,因此无法在测试函数中验证测试是否成功。当用户按下 NextButton 执行下一个测试时,将执行验证操作。

private Action? testWidth() {
  oldWidth = TestControlTraced.Width;
  TestControlTraced.Width = 200;
  return verifyWidth;
}

private void verifyWidth() {
  if (double.IsNaN(TestControlTraced.Width)) return;

  if (TestControlTraced.ActualWidth==TestControlTraced.Width) {
    throw new InvalidOperationException($"Actual width should be {TestControlTraced.Width} " +
      $"but was {TestControlTraced.ActualWidth}.");
  }
}

失败的测试将在 EventTracer 中显示如下:

Trc 11:20:05.228 Test: Width
Trc 11:20:05.228 Control.Width=200 
…
Trc 11:20:05.636 Control.ActualWidth=123

Err 11:20:06.448 Test Error: Actual width should be 200 but was 123.
System.InvalidOperationException
================================
Actual width should be 200 but was 123.
Data: System.Collections.ListDictionaryInternal
Source: WpfControlTestbench
HResult: -2146233079
   at WpfTestbench.ControlWindow.verifyWidth() in 
   C:\Users\Peter\source\repos\WpfControlTestbench\WpfControlTestbench\ControlWindow.xaml.cs:
   line 98
   at WpfTestbench.TestBench.nextTestButton_Click(Object sender, RoutedEventArgs e) in 
   C:\Users\Peter\source\repos\WpfControlTestbench\WpfControlTestbenchLib\TestBench.cs:line 895

如果用户能够重置由您的测试更改的属性值,那将很有用。为此,请在测试窗口的构造函数中添加这些行(完整代码请参阅 *ControlWindow.xaml.cs*):

resetFill = TestControlTraced.Fill;
TestBench.ResetAction = () => TestControlTraced.Fill = resetFill;

当用户按下 ResetButton 时,将执行 ResetAction

WpfControlTestbench 源代码

推荐阅读

如果您一直读到这里,再次恭喜您。为了更好地理解 WPF 中的布局工作原理,我强烈建议您阅读我在 CodeProject 上的文章(2022 年 2 月最佳文章,二等奖)。

我在 CodeProject 上写的其他一些评分很高的 WPF 文章:

我的 Github 项目可能对您有用:

  • WpfWindowsLib:用于数据输入、检测必需数据是否缺失或数据是否已更改的 WPF 控件。
  • TracerLib:部分内容用于 WpfControlTestbench。内存中异常、错误和信息的快速跟踪,某些条目可以由后台线程写入文件。非常适合记录异常发生前刚刚发生的事情。
  • StorageLib:纯 C# 库,为单用户应用程序提供内存中快速面向对象的(OO)数据存储以及本地硬盘上的长期存储。无需数据库。
  • MasterGrab:MasterGrab 是一款 WPF 游戏,人类玩家与多个计算机玩家(=机器人)对战。您可以自己用 C# 编程机器人。六年来,我每天都在玩它。大约只需要 10 分钟。非常适合在开始编程之前让大脑热身。
© . All rights reserved.