Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I manually tell an owner-drawn WPF Control to refresh/redraw without executing measure or arrange passes?

We are doing custom drawing in a control subclass's OnRender. This drawing code is based on an external trigger and data. As such, whenever the trigger fires, we need to re-render the control based on that data. What we're trying to do is find out how to force the control to re-render but without going through an entire layout pass.

As stated above, most answers I've seen revolve around invalidating the Visual which invalidates the layout which forces new measure and arrange passes which is very expensive, especially for very complex visual trees as ours is. But again, the layout does not change, nor does the VisualTree. The only thing that does is the external data which gets rendered differently. As such, this is strictly a pure rendering issue.

Again, we're just looking for a simple way to tell the control that it needs to re-execute OnRender. I have seen one 'hack' in which you create a new DependencyProperty and register it with 'AffectsRender' which you just set to some value when you want to refresh the control, but I'm more interested in what's going on inside the default implementation for those properties: what they call to affect that behavior.


Update:

Well, it looks like there isn't any such call as even the AffectsRender flag still causes an Arrange pass internally (as per CodeNaked's answer below) but I've posted a second answer that shows the built-in behaviors as well as a work-around to suppress your layout pass code from running with a simple nullable size as a flag. See below.

like image 506
Mark A. Donohoe Avatar asked Oct 18 '11 02:10

Mark A. Donohoe


4 Answers

Unfortunately, you must call InvalidateVisual, which calls InvalidateArrange internally. The OnRender method is called as part of the arrange phase, so you need to tell WPF to rearrange the control (which InvalidateArrange does) and that it needs to redraw (which InvalidateVisual does).

The FrameworkPropertyMetadata.AffectsRender option simply tells WPF to call InvalidateVisual when the associated property changes.

If you have a control (let's call this MainControl) that overrides OnRender and contains several descendant controls, then calling InvalidateVisual may require the descendant controls to be rearranged, or even remeasured. But I believe WPF has optimizations inplace to prevent descendant controls from being rearranged if their available space is unchanged.

You may be able to get around this by moving your rendering logic to a separate control (say NestedControl), which would be a visual child of MainControl. The MainControl could add this as a visual child automatically or as part of it's ControlTemplate, but it would need to be the lowest child in the z-order. You could then expose a InvalidateNestedControl type method on MainControl that would call InvalidateVisual on the NestedControl.

like image 103
CodeNaked Avatar answered Nov 20 '22 19:11

CodeNaked


Ok, I'm answering this to show people why CodeNaked's answer is correct, but with an asterisk if you will, and also to provide a work-around. But in good SO-citizenship, I'm still marking his as answered since his answer led me here.

Update: I've since moved the accepted answer to here for two reasons. One, I want people to know there is a solution to this (most people only read the accepted answer and move on) and two, considering he has a rep of 25K, I don't think he'd mind if I took it back! :)

Here's what I did. To test this, I created this subclass...

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);
    }

}

...which I laid out like this (note that they are nested):

<l:TestPanel x:Name="MainTestPanel" Background="Yellow">

    <Button Content="Test" Click="Button_Click" DockPanel.Dock="Top" HorizontalAlignment="Left" />

    <l:TestPanel x:Name="InnerPanel" Background="Red" Margin="16" />

</l:TestPanel>

When I resized the window, I got this...

MeasureOverride called for MainTestPanel.
MeasureOverride called for InnerPanel.
ArrangeOverride called for MainTestPanel.
ArrangeOverride called for InnerPanel.
OnRender called for InnerPanel.
OnRender called for MainTestPanel.

but when I called InvalidateVisual on 'MainTestPanel' (in the button's 'Click' event), I got this instead...

ArrangeOverride called for MainTestPanel.
OnRender called for MainTestPanel.

Note how none of the measuring overrides were called, and only the ArrangeOverride for the outer control was called.

It's not perfect as if you have a very heavy calculation inside ArrangeOverride in your subclass (which unfortunately we do) that still gets (re)executed, but at least the children don't fall to the same fate.

However, if you know none of the child controls have a property with the AffectsParentArrange bit set (again, which we do), you can go one better and use a Nullable Size as a flag to suppress the ArrangeOverride logic from re-entry except when needed, like so...

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);
    }

}

Now unless something specifically needs to re-execute the arrange logic (as a call to MeasureOverride does) you only get OnRender, and if you want to explicitly force the Arrange logic, simply null out the size, call InvalidateVisual and Bob's your uncle! :)

Hope this helps!

like image 38
Mark A. Donohoe Avatar answered Nov 20 '22 19:11

Mark A. Donohoe


You shouldn't be calling InvalidateVisual() unless the size of your control changes, and even then there are other ways to cause re-layout.

To efficiently update the visual of a control without changing it's size. Use a DrawingGroup. You create the DrawingGroup and put it into the DrawingContext during OnRender() and then anytime after that you can Open() the DrawingGroup to change it's visual drawing commands, and WPF will automatically and efficiently re-render that portion of the UI. (you can also use this technique with RenderTargetBitmap if you'd prefer to have bitmap which you can make incremental changes to, rather than redrawing every time)

This is what it looks like:

DrawingGroup backingStore = new DrawingGroup();

protected override void OnRender(DrawingContext drawingContext) {      
    base.OnRender(drawingContext);            

    Render(); // put content into our backingStore
    drawingContext.DrawDrawing(backingStore);
}

// I can call this anytime, and it'll update my visual drawing
// without ever triggering layout or OnRender()
private void Render() {            
    var drawingContext = backingStore.Open();
    Render(drawingContext);
    drawingContext.Close();            
}

private void Render(DrawingContext drawingContext) {
    // put your render code here
}
like image 5
David Jeske Avatar answered Nov 20 '22 18:11

David Jeske


Here is another hack: http://geekswithblogs.net/NewThingsILearned/archive/2008/08/25/refresh--update-wpf-controls.aspx

In short, you call invoke some dummy delegate at priority DispatcherPriority.Render, which will cause anything with that priority or above to be invoked too, causing a rerender.

like image 1
GazTheDestroyer Avatar answered Nov 20 '22 19:11

GazTheDestroyer