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

介绍

WpfControlTestbench帮助您为您的控件或您想要调查其行为的任何控件编写快速复杂的测试窗口。只需十几行XAML即可创建以下Window内容:

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

WPF控件支持范围广泛的布局(MarginBorderPaddingWidthHeight、对齐等)功能,因此开发起来具有挑战性。更难的是测试控件在许多可能的情况下是否正常运行。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控件的行为,请将其托管在WpfControlTestbench中。对您自己的控件执行相同操作,然后验证您的控件在Microsoft等每个场景中的行为是否相同。

控制尺寸和定位

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

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

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

  • 当空间太少时会发生什么?空间太大?
  • 如果Width属性被设置会发生什么(注意它不是上面公式的一部分)?
  • 如果Width未定义会发生什么?
  • 如果HorizontalAlignmentLeft变为CenterRightStretch会发生什么?
  • 发生Font.Size变化时会发生什么?
  • 父级控件或您的控制是否处理MarginPadding?

答案

  • 空间太少:如果您的控件呈现在给定空间之外,则取决于剪辑是否显示。空间太大:取决于对齐方式。如果未拉伸,则父级(!)将相应地放置内容。如果拉伸,子级应该使用所有可用空间。
  • 设置Width时,您的控件应该完全使用该空间。如果对齐被拉伸,则不应发生拉伸,但父级将使您的控件居中。
  • Width何时未定义,您的控件应该自己确定它需要多少空间。
  • HorizontalAlignmentLeft更改为CenterRight对您的控件无关紧要,渲染是相同的,但父级会以不同的方式放置。HorizontalAlignment Left更改为Stretch将要求您的控件使用所有可用大小进行渲染,而不仅仅是它认为应该使用的大小。
  • Font.Size(Font.Family …)发生变化时,您的控件内容可能需要更多或更少的空间,即Measure(),Arrange()Render()需要执行。
  • 控件的宿主处理Margin。您的控件需要处理BorderPadding

如果您知道所有这些问题的答案,那么恭喜您。如果没有,很遗憾你很难在Microsoft的文档中找到答案。但作为控件的开发人员,您需要准确了解其工作原理。

要快速获得此类问题的答案,请使用WpfControlTestbench、更改一些值并查看Microsoft控件的反应。然后为您的控件实现相同的行为。TestBench 中的Show Template按钮也有助于更好地理解Microsoft控件,它显示ControlTemplate XAML。如果您真的想了解详细信息,还可以在GitHub - dotnet/wpf: WPF is a .NET Core UI framework for building Windows desktop applications.查看WPF源代码。但请注意,它可能很复杂。

由于获得正确的尺寸和定位需要检查许多场景,并且像素或多或少可能会有所不同,WpfControlTestbench因此可以轻松更改宽度、对齐方式等。它为可以用鼠标移动的MarginBorderPadding显示虚线。即使是总可用空间也可以通过鼠标拖动分割线轻松更改。

测试标准属性

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

如果您的控件继承自FrameworkElement,则WpfControlTestbench显示WidthHeightAlignmentMinMaxMargin的属性,用户可以更改这些属性。DesiredWidthRenderWidth是计算值,不能更改。如果您的控件继承自Control,则还会显示BorderPadding的属性。

您还可以更改颜色和字体:

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

单击重置将所有标准属性的值设置回窗口打开时的值。

标准测试

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

您只需继续单击下一步(Alt + N)。通常,快速浏览一下就会告诉您一切是否正常。当然,您也可以轻松地为特定于您的控件的属性添加自己的测试,这将在下面进行详细说明。

试用WpfControlTestbench

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

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

左列是WPF提供的2个控件的2个测试,右列是我编写的控件。您可以编写自己的应用程序或将测试窗口添加到此窗口。我希望其他人会为其他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创建一个调用YourControlTraced的无参数构造函数的C#行,该构造函数调用其他构造函数,如下所示:

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

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

这是一个完整的代码示例,准备在TestBench中使用StackPanel

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,您可以在其中添加类似StackPanel或包含Labels,TextBoxes等的Grid内容来测试控件的属性
  • 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中定义绑定或隐藏代码来完成。容易,对吧?

在设置WPF绑定时,我编写了WpfBinding()代码以使我的代码看起来更好:

/// <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,它将验证测试是否成功或抛出异常。由于布局发生在稍后的时间,因此无法在测试功能中验证测试是否成功。当用户按下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文章:

我的Github项目可能对您来说很有趣:

  • WpfWindowsLib:用于数据输入的WPF控件,检测所需数据是否丢失或数据已更改
  • TracerLib:它的一部分用于WpfControlTestbench中。快速跟踪内存中的异常、错误和信息,一些条目可以由后台线程写入文件。很高兴记录发生异常之前发生的事情。
  • StorageLib:仅C#库,可在RAM中提供快速的面向对象数据存储,并在本地硬盘上为单用户应用程序提供长期存储。不需要数据库。
  • MasterGrabMasterGrab是一款WPF游戏,其中人类玩家与多个计算机玩家(=机器人)进行游戏。您可以使用C#编写自己的机器人。从6年前开始,我每天都玩它。只需大约10分钟。非常适合在开始编程之前预热我的大脑。

https://www.codeproject.com/Articles/5327985/Must-Read-for-WPF-Developers-WPF-Control-Test-Benc

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值