Lazy Rendering of DrawingVisual

574 views Asked by At

I'm trying to change a custom WPF element to use a visual tree, so that background layers can be changed without needing to throw away the whole retained render. At the same time, a layer might be affected by more than one property, so I'd like to redo the render lazily, in case multiple properties get changed in a single screen update cycle. Here's what I did.

protected override int VisualChildrenCount
{
    get
    {
        return 1;
    }
}

private readonly DrawingVisual textLayer = new DrawingVisual();
bool textLayerReady;
protected override Visual GetVisualChild(int index)
{
    switch (index)
    {
        case 0:
            if (!textLayerReady)
            {
                using (var textContext = textLayer.RenderOpen())
                    RenderTextLayer(textContext);
            }
            return textLayer;
        default:
            throw new ArgumentOutOfRangeException("index");
    }
}

It seems to run correctly, but in the designer I get:

InvalidOperationException: Cannot call this API during the OnRender callback. During OnRender, only drawing operations that draw the content of the Visual can be performed.

I suppose that at runtime the layout process is calling GetVisualChild prior to the actual render, and the design canvas operates differently?

Is this a reasonable thing to attempt? How should I trigger the render of child drawingvisuals in order to ensure it happens at a legal time?

2

There are 2 answers

0
Ben Voigt On BEST ANSWER

Visuals themselves throw exceptions if modified during the render cycle, and the designers fetch visuals during the render cycle without going through the arrange processing beforehand.

However, it's perfectly legal to modify the contents of a DrawingGroup during render, even if that DrawingGroup is a member of a Visual.

One merely needs to add the DrawingGroup to the DrawingVisual once in the constructor:

DrawingGroup textLayer = new DrawingGroup();
DrawingVisual textVisual = new DrawingVisual();
using (DrawingContext textContext = textVisual.RenderOpen())
    textContext.DrawDrawing(textLayer);

Then later, one can replace the content lazily when it's actually needed for rendering, while still keeping the advantages of retained mode (not reconstructing the "vector graphics instruction lists" as WPF terms it, for unchanged layers).

protected override Visual GetVisualChild(int index)
{
    switch (index)
    {
        case 0:
            if (!textLayerReady)
            {
                using (var textContext = textLayer.Open())
                    RenderTextLayer(textContext);
            }
            return textVisual;

        default:
            throw new ArgumentOutOfRangeException("index");
    }
}
0
pushpraj On

You can perhaps prevent rendering at design time by detecting it accordingly

so if I modify your code a bit it should behave fine in designer too

private readonly DrawingVisual textLayer = new DrawingVisual();
bool textLayerReady;
protected override Visual GetVisualChild(int index)
{
    if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(this)) 
    {
        return; //do not perform custom logic during design time
    }

    switch (index)
    {
        case 0:
            if (!textLayerReady)
            {
                using (var textContext = textLayer.RenderOpen())
                    RenderTextLayer(textContext);
            }
            return textLayer;
        default:
            throw new ArgumentOutOfRangeException("index");
    }
}