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





5.00/5 (7投票s)
WpfTestbench 帮助您用少量代码编写 WPF 控件的复杂测试窗口。
引言
WpfControlTestbench
帮助您快速编写复杂的测试窗口,用于测试您的控件或您想深入了解其行为的任何控件。只需十几行 XAML 代码即可创建如下 Window
它在左下角显示您要测试的控件,在 Window
的上半部分是用于您控件所有属性的 Controls
。您可以在运行时更改它们的值。在右下角,您会看到一个跟踪查看器,显示任何已更改属性的值,以及持有您的控件的 WPF 容器和您的控件如何进行控件的布局。
WPF 控件支持令人惊叹的布局(Margin
、Border
、Padding
、Width
/Height
、对齐等)功能,因此开发起来颇具挑战性。更困难的是测试控件在众多可能场景下的行为是否正确。WpfControlTestbench
让您可以直观地、交互式地进行测试,您可以立即看到属性值更改对控件的效果。开箱即用,WpfControlTestbench
会显示从 FrameworkElement
和 Control
继承的所有重要属性。当然,您可以轻松添加更多控件来检查您控件的特定属性。
深入了解您的控件及其 WPF 父控件的交互
控件的行为取决于其在逻辑树中的 Parent
,即它们被放置在哪一个 WPF ContentControl
或 Panel
中。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
未定义时会发生什么? - 当
HorizontalAlignment
从Left
更改为Center
、Right
或Stretch
时会发生什么? - 当
Font.Size
更改时会发生什么? - 是父控件还是您的控件处理
Margin
?Padding
?
答案
- 空间不足:如果您的控件渲染到给定空间之外,它将取决于剪裁是否显示。空间过多:取决于对齐方式。如果不拉伸,父控件将相应地放置内容。如果拉伸,子控件应使用所有可用空间。
- 当设置
Width
时,您的控件应精确使用该空间。如果对齐方式为拉伸,则不应发生拉伸,但父控件会居中放置您的控件。 - 当
Width
未定义时,您的控件应自行确定需要多少空间。 - 将
HorizontalAlignment
从Left
更改为Center
或Right
对您的控件无关紧要,渲染效果相同,但父控件的放置方式会不同。将HorizontalAlignment
从Left
更改为Stretch
会要求您的控件渲染时使用所有可用大小,而不仅仅是它认为应该使用的那些大小。 - 当
Font.Size
(或Font.Family
或……)更改时,您的控件的内容可能需要更多或更少的空间,即需要执行Measure()
、Arrange()
和Render()
。 - 您的控件的主机处理
Margin
。您的控件需要处理Border
和Padding
。
如果您知道所有这些问题的答案,那真是恭喜您。如果不知道,很遗憾,您很难在 Microsoft 的文档中找到答案。但是,作为控件的开发者,您需要精确理解这一切是如何工作的。
要快速获得此类问题的答案,请使用 WpfControlTestbench
,更改一些值,然后观察 Microsoft 控件的反应。然后为您的控件实现相同的行为。TestBench
中的 Show Template
按钮也很有助于更好地理解 Microsoft 控件,它会显示 ControlTemplate
XAML。如果您真的想了解细节,还可以查看 GitHub 上的 WPF 源代码:https://github.com/dotnet/wpf。但请注意,它可能很复杂。
由于正确处理尺寸和定位需要检查许多场景,而且多一个或少一个像素都可能产生差异,WpfControlTestbench
可以轻松更改宽度、对齐方式等。它显示 Margin
、Border
和 Padding
的虚线,这些虚线可以用鼠标移动。甚至总可用空间也可以通过拖动分隔线轻松更改。
测试标准属性
您也可以通过键盘输入值来更改属性,这可能更精确,但耗时也更多。
如果您的控件继承自 FrameworkElement
,WpfControlTestbench
会显示 Width
、Height
、Alignment
、Min
、Max
和 Margin
等属性,用户可以更改这些属性。DesiredWidth
和 RenderWidth
是计算值,无法更改。如果您的控件继承自 Control
,还会显示 Border
和 Padding
的属性。
您还可以更改颜色和字体
您可以更改 Background
、Foreground
(即 Font
)和 Border
的颜色。更改其他字体属性很有趣,因为您的控件所需的尺寸可能还取决于 FontFamily
和 Font.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
接口 ITraceName
或 IIsTracing
以支持跟踪。
/// <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 ()
YourControlTraced
的无参数构造函数调用带有参数TraceName
的YourControlTraced
构造函数。请注意,WPF 的Name
属性不能用于跟踪,因为它只在构造函数完成后才赋值。- 带有
TraceName
的YourControlTraced
构造函数通过调用TraceWPFEvents.TraceCreateStart(traceName)
方法来跟踪构造函数的开始,该方法返回null
。 YourControlWithConstructor
构造函数有一个参数,但没有使用它!它最终调用YourControl
构造函数。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
方法,这些方法:
- 在跟踪中标记重写方法的开始
- 调用重写的方法
- 在跟踪中标记重写方法的结束
如果您想知道为什么不只写一行跟踪,原因在于,例如父控件的 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
,您可以在其中添加诸如StackPanel
或Grid
之类的元素,其中包含Labels
、TextBoxes
等,以测试您的控件的属性。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));
TestFunctions
是 TestBench
中的一个 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 文章:
- 使用绑定进行 WPF DataGrid 格式化的指南
- WPF DataGrid:解决排序、滚动到视图、刷新和焦点问题
- WPF 颜色、颜色空间、颜色选择器和为普通人创建自己颜色的权威指南
- 用于数据输入的基类 WPF 窗口功能
我的 Github 项目可能对您有用:
- WpfWindowsLib:用于数据输入、检测必需数据是否缺失或数据是否已更改的 WPF 控件。
- TracerLib:部分内容用于
WpfControlTestbench
。内存中异常、错误和信息的快速跟踪,某些条目可以由后台线程写入文件。非常适合记录异常发生前刚刚发生的事情。 - StorageLib:纯 C# 库,为单用户应用程序提供内存中快速面向对象的(OO)数据存储以及本地硬盘上的长期存储。无需数据库。
- MasterGrab:MasterGrab 是一款 WPF 游戏,人类玩家与多个计算机玩家(=机器人)对战。您可以自己用 C# 编程机器人。六年来,我每天都在玩它。大约只需要 10 分钟。非常适合在开始编程之前让大脑热身。