单元测试自定义WPF UIElement的呈现

本文关键字:UIElement 自定义 WPF 单元测试 | 更新日期: 2023-09-27 17:57:27

在我的应用程序中,我使用一个自定义的UIElement,它处理自己的布局和渲染。虽然我可以对其中的大部分进行单元测试,但我无法正确地对渲染进行单元测试。原因是渲染是通过OnRender方法完成的,但我没有办法测试实际渲染的内容,因为DrawingContext是一个带有内部构造函数的抽象类,所以我不能出于测试目的从它派生。

我知道如何做的唯一测试是尝试不同的场景(基于实现的代码),并检查是否没有抛出异常。有什么方法可以测试更多(除了使用TypeMock Isolator或JustLock)吗?

单元测试自定义WPF UIElement的呈现

我在这个线程中写下了答案:单元测试自定义OnRender方法

但尽管如此,我也复制了这个线程的答案(如果不需要,有人可能会删除它)。

解决方案是从DrawingGroup创建DrawingContext

public class TestingMyControl : MyControl
{
    public DrawingGroup Render()
    {
        var drawingGroup = new DrawingGroup();
        using (var drawingContext = drawingGroup.Open())
        {
             base.OnRender(drawingContext);
        }
        return drawingGroup;
    }
}

因此,固定装置将看起来像:

    [Test]
    public void Should_render()
    {
        var controlToTest = new TestingMyControl();
        var drawingGroup = controlToTest.Render();
        var drawing = drawingGroup.Children[0] as GeometryDrawing;
        Assert.That(drawing.Brush, Is.EqualTo(Brushes.Black));
        Assert.That(drawing.Pen.Brush, Is.EqualTo(Brushes.SeaGreen));
        Assert.That(drawing.Pen.Thickness, Is.EqualTo(0.6));
        Assert.That(drawing.Bounds.X, Is.EqualTo(5));
        Assert.That(drawing.Bounds.Y, Is.EqualTo(15));
        Assert.That(drawing.Bounds.Width, Is.EqualTo(25));
        Assert.That(drawing.Bounds.Height, Is.EqualTo(35));
    }

这需要以下生产代码:

public class MyControl : Canvas
{
    protected override void OnRender(DrawingContext dc)
    {
        dc.DrawRectangle(Brushes.Black, new Pen(Brushes.SeaGreen, 0.6), new Rect(5, 15, 25, 35));
    }
}

不幸的是,您必须调用InvalidateVisual,它在内部调用InvalidateArrange。OnRender方法是作为排列阶段的一部分调用的,因此您需要告诉WPF重新排列控件(InvalidateArrange会这样做),并且它需要重新绘制(InvalidateVisual会这样做。

FrameworkPropertyMetadata.AffectsRender选项只是简单地告诉WPF在相关属性更改时调用InvalidateVisual

如果您有一个覆盖OnRender并包含多个子控件的控件(让我们称之为MainControl),那么调用InvalidateVisual可能需要重新排列甚至重新测量子控件。但我相信WPF已经进行了优化,以防止子控件在可用空间不变的情况下被重新排列。

您可以通过将渲染逻辑移动到一个单独的控件(例如NestedControl)来解决此问题,该控件将是MainControl的可视子控件。MainControl可以自动将其添加为可视子级,也可以作为其ControlTemplate的一部分,但它需要是z顺序中最低的子级。然后,您可以在MainControl上公开一个InvalidateNestedControl类型的方法,该方法将在NestedCntrol上调用InvalidateVisual。

以下是我所做的。为了测试这一点,我创建了这个子类。。。

public class TestPanel : DockPanel
{
    protected override Size MeasureOverride(Size constraint)
    {
        System.Console.WriteLine("MeasureOverride called for " + this.Name + ".");
        return base.MeasureOverride(constraint);
    }
protected override System.Windows.Size ArrangeOverride(System.Windows.Size arrangeSize)
{
    System.Console.WriteLine("ArrangeOverride called for " + this.Name + ".");
    return base.ArrangeOverride(arrangeSize);
}
protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
    System.Console.WriteLine("OnRender called for " + this.Name + ".");
    base.OnRender(dc);
}
}

我这样布局(注意它们是嵌套的):

<Button Content="Test" Click="Button_Click" DockPanel.Dock="Top" HorizontalAlignment="Left" />
<l:TestPanel x:Name="InnerPanel" Background="Red" Margin="16" />

当我调整窗口大小时,我得到了。。。

为MainTestPanel调用了MeasureOverride。为InnerPanel调用了MeasureOverride。为MainTestPanel调用了ArrangeOverride。已为InnerPanel调用ArrangeOverride。为InnerPanel调用了OnRender。OnRender调用了MainTestPanel。但当我在"MainTestPanel"上调用InvalidateVisual(在按钮的"Click"事件中)时,我得到了这个。。。

为MainTestPanel调用了ArrangeOverride。OnRender调用了MainTestPanel。请注意,没有调用任何测量覆盖,只调用了外部控件的ArrangeOverride。

这并不完美,就好像你在子类中的ArrangeOverride中有一个非常繁重的计算(不幸的是,我们确实这样做了),仍然会被(重新)执行,但至少孩子们不会陷入同样的命运。

但是,如果您知道没有一个子控件具有AffectsParentArrange位集的属性(同样,我们也是这样做的),您可以更好地使用Nullable Size作为标志来禁止ArrangeOverride逻辑重新进入,除非需要,比如…

public class TestPanel : DockPanel
{
    Size? arrangeResult;
protected override Size MeasureOverride(Size constraint)
{
    arrangeResult = null;
    System.Console.WriteLine("MeasureOverride called for " + this.Name + ".");
    return base.MeasureOverride(constraint);
}
protected override System.Windows.Size ArrangeOverride(System.Windows.Size arrangeSize)
{
    if(!arrangeResult.HasValue)
    {
        System.Console.WriteLine("ArrangeOverride called for " + this.Name + ".");
        // Do your arrange work here
        arrangeResult = base.ArrangeOverride(arrangeSize);
    }
    return arrangeResult.Value;
}
protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
    System.Console.WriteLine("OnRender called for " + this.Name + ".");
    base.OnRender(dc);
}
}

现在,除非有什么特别需要重新执行排列逻辑(就像对MeasureOverride的调用一样),否则您只能获得OnRender,如果您想显式强制执行排列逻辑,只需将大小设为null,即可调用InvalidateVisual。