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

4

26

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.

Monies answered 18/10, 2011 at 2:5 Comment(0)
M
12

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!

Monies answered 18/10, 2011 at 7:43 Comment(4)
David, please refrain from saying things like ‘this is not the correct way...’ especially without saying what’s wrong with someone else’s approach. For instance, while different than your proposed answer, this is a perfectly valid way and follows all WPF rules. Also, adding your answer is enough to make your point without adding comments to all the other answers, again, saying they are wrong without pointing out why. Simply say ‘Here’s a way I feel is better", then explain why.Monies
This method does not prevent InvalidateVisual() from triggering layout on other controls, and re-calling OnRender() on other controls to recreate the visual-graph. If you performance test this method against updating a DrawingGroup, you will find it much slower. Though performance testing on WPF is a tricky business because OnRender doesn't actually draw.Cytaster
Also, from the MSDN docs on InvalidateVisual() : "This method is not generally called from your application code...Calling this method is necessary only for advanced scenarios. One such advanced scenario is if you are creating a PropertyChangedCallback for a dependency property that is not on a Freezable or FrameworkElement derived class that still influences the layout when it changes."Cytaster
As stated in my original question, we are drawing the contents of our control so your comment about other controls doesn't apply because they don't exist here. Still this is good information for knowledge-sharing. But back to my original comment, even here you are putting that information in the comments. Instead you should be saying this information in your answer so people can find it. They generally will not read comment chains and instead will go through the answers. Format your answer as 'Here's another approach that addresses the following concerns with some of the other answers.'Monies
G
20

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.

Gyro answered 18/10, 2011 at 6:9 Comment(9)
I'm not sure that's 100% correct. Take a look at the very first sentence in MSDN regarding the AffectsRender flag... msdn.microsoft.com/en-us/library/… Specifically it says "...affects the general layout in some way that does not specifically influence arrangement or measurement, but would require a redraw.' So the mechanism is there without an arrange. Doing the faux-DP mentioned above would be better than resorting to changing the visual tree. (I'm going to see if digging through Reflector will uncover anything.)Monies
@MarqueIV - Actually, I used Reflector when looking into this. I almost never rely on the documentation. You can see OnRender is called from Arrange, and that FrameworkElement.OnPropertyChanged ultimately checks for the AffectsRender flag and calls InvalidateVisual if set.Gyro
Touche' to the documentation comment. I've been burned by that on way more than one occasion and I should have known better! :) That said, time for a test app! I'll create a DP with the AffectsRender bit set and subclass MeasureOverride and ArrangeOverride and see if they get called when I change it. If you're right and it does, then my next step will be to set a state flag and cache the last-called return values from the overloads, set the flag and call InvalidateVisual. In the overrides, if the flag is set, I simply return the cached values. If not, I let them do their thing.Monies
@MarqueIV - They may not be called, but using AffectsRender would be the same as calling InvalidateVisual. There's alot of code in there to try to prevent arranging/measuring needlessly.Gyro
I'm starting to wonder if I should start a WPF blog instead of littering SO with such lengthy and verbose answers as I did below. ...or do you think things like that help here? (I'm asking because you're at 14K and I'm at a lowly 1K, so you have more SO experience than me. Thoughts?)Monies
@MarqueIV - I'm not sure that qualifies me to give such advice :-) I would say SO is great for getting help for specific issues, but blog posts are better suited for explaining something that was confusing/broken/etc and it's solution. As this discussion is outside the scope of SO, feel free to email me "anything at my domain".Gyro
Well as an experienced SO person, look at my posted answer below. IYO, yes or no question: that too long for here? Should I have just posted the solution and not the reason? (I'm told my questions can be too long. Wondering if my answers are too.)Monies
@MarqueIV - Nope, doesn't seem too long and fits well.Gyro
This answer is not correct. You can trigger an efficient redraw without an expensive re-layout. See my answer.Cytaster
M
12

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!

Monies answered 18/10, 2011 at 7:43 Comment(4)
David, please refrain from saying things like ‘this is not the correct way...’ especially without saying what’s wrong with someone else’s approach. For instance, while different than your proposed answer, this is a perfectly valid way and follows all WPF rules. Also, adding your answer is enough to make your point without adding comments to all the other answers, again, saying they are wrong without pointing out why. Simply say ‘Here’s a way I feel is better", then explain why.Monies
This method does not prevent InvalidateVisual() from triggering layout on other controls, and re-calling OnRender() on other controls to recreate the visual-graph. If you performance test this method against updating a DrawingGroup, you will find it much slower. Though performance testing on WPF is a tricky business because OnRender doesn't actually draw.Cytaster
Also, from the MSDN docs on InvalidateVisual() : "This method is not generally called from your application code...Calling this method is necessary only for advanced scenarios. One such advanced scenario is if you are creating a PropertyChangedCallback for a dependency property that is not on a Freezable or FrameworkElement derived class that still influences the layout when it changes."Cytaster
As stated in my original question, we are drawing the contents of our control so your comment about other controls doesn't apply because they don't exist here. Still this is good information for knowledge-sharing. But back to my original comment, even here you are putting that information in the comments. Instead you should be saying this information in your answer so people can find it. They generally will not read comment chains and instead will go through the answers. Format your answer as 'Here's another approach that addresses the following concerns with some of the other answers.'Monies
C
6

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
}
Cytaster answered 8/6, 2017 at 2:23 Comment(11)
An interesting approach, and I've upvoted, although I don't agree with your blanket statement "You shouldn't be calling InvalidateVisual() unless the size of your control changes". It would depend on the complexity of the control's arrange logic, whether the control has any nested children, how often InvalidateVisual is being called, and so on. This technique may be overkill in many scenarios. I'm assuming the fact MS included no standard way in WPF to 'repaint' a control without also forcing an arrange pass means the performance hit wasn't that much of an issue.Objectivity
@StevenRands from the MSDN docs on InvalidateVisual() : "This method is not generally called from your application code...Calling this method is necessary only for advanced scenarios. One such advanced scenario is if you are creating a PropertyChangedCallback for a dependency property that is not on a Freezable or FrameworkElement derived class that still influences the layout when it changes."Cytaster
@StevenRands There is zero reason to InvalidateVisual() when the size of your control stays the same. Layout is a very-expensive process in WPF, so avoid it at all costs. If only the content changes, re-render should be triggered with a dependency property with AffectsRender, or through a DrawingGroup as I outlined above.Cytaster
The AffectsRender option on a DP ultimately calls InvalidateVisual anyway, according to this. It's just a convenience mechanism to avoid having to call the method yourself when a DP's value changes. Also, how expensive the layout pass is will depend largely on whether the control has any children, whether those children have any children, and so on: for a custom FrameworkElement derivitive with no children, the cost of the layout call would be minimal.Objectivity
That's good to know about AffectsRender. As for the performance, in my trivial app with only a single control, the app framerate was 5x worse than than Windows.Forms when using InvalidateVisual(). My app just draws a graph with a trivial but quickly updated set of lines on it and InvalidateVisual() was completely unviable. My first success at good performance was using my method above with WriteableBitmap, RenderTargetBitmap, and then finally with DrawingGroup.Cytaster
We've used WritableBitmap with great success ourselves. It's essentially what all the WPF drawing code ends up with in the end. You're just bypassing all of that. Granted, the issue is the primitive drawing support, but it suffices in a lot of cases. For those where it doesn't, this DrawingGroup seems like it may cover the other cases.Monies
Question though... what is the purpose of having the separate Render(DrawingContext) call if you're just calling it from the parameterless Render() method? Why not put your drawing calls there? This would make sense if it was shared rendering code between multiple contexts, but that isn't the case shown here.Monies
It's just personal preference. I like to separate the code which sets up the rendering context from the code which actually draws into it. This way my Render(DrawingContext) method feels more like a normal render method.Cytaster
Could I see a sample of using RenderTargetBitmap. I think this is what I need but I am unsure of when/where to initialize the RenderTargetBitmapBacteriostat
You replace DrawingGroup with RenderTargetBitmap, so this becomes your backing store.... You can see an example of drawing to the backing store here.. msdn.microsoft.com/en-us/library/…Cytaster
Because RenderTargetBitmap has an actual size, you have to dimension it according to the size of the control, and you have to hook size changes of the control and create a properly sized RenderTargetBitmap... otherwise your drawing will get scaled and and be blurry.Cytaster
S
1

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.

Schnorrer answered 18/10, 2011 at 8:11 Comment(5)
Just looked at that, but I'm not sure that does what he thinks. I just called 'MainTestPanel.Dispatcher.Invoke(DispatcherPriority.Render, EmptyDelegate)' but my OnRender was not called. Thoughts? (This looked very promising, but the lack of a response kinda vetoed my enthusiasm. Also, to be sure the delegate was called, I actually added a Console.WriteLine in it which did output so I know that's not the issue.)Monies
Yes, having reread it I think you are right. It just forces the delegate that is already queued. Sorry!Schnorrer
Just looked at it again. This won't work. The reason this isn't actually forcing a render. It's forcing things that are queued with the render priority to execute, but it doesn't force the control onto the render queue if that makes sense. Put another way, this says 'Hey... you can render right now, no waiting needed!' but the control is saying 'Thanks, but I don't have to!' In his example however, it does need to render since he set the content of the label which does mark the control to be rendered. Cool trick, but solves a different issue.Monies
Ya beat me to it! :) I'm still voting you up because several people coming here for an answer may be very well looking for that solution, and it is kinda cool.Monies
This both doesn't work, and is not the correct way to update a control without re-layout. See my answer.Cytaster

© 2022 - 2024 — McMap. All rights reserved.