tags:

views:

455

answers:

2

In a UI I'm building, I want to adorn a panel whenever one of the controls in the panel has the focus. So I handle the IsKeyboardFocusWithinChanged event, and add an adorner to the element when it gains the focus and remove the adorner when it loses focus. This seems to work OK.

The problem I'm having is that the adorner isn't getting re-rendered if the bounds of the adorned element changes. For instance, in this simple case:

<WrapPanel Orientation="Horizontal"
           IsKeyboardFocusChanged="Panel_IsKeyboardFocusChanged">
   <Label>Caption</Label>
   <TextBox>Data</TextBox>
</WrapPanel>

the adorner correctly decorates the bounds of the WrapPanel when the TextBox receives the focus, but as I type in text, the TextBox expands underneath the edge of the adorner. Of course as soon as I do anything that forces the adorner to render, like ALT-TAB out of the application or give another panel the focus, it corrects itself. But how can I get it to re-render when the bounds of the adorned element change?

A: 

You need to invoke the dispatcher on the panel. Add a handler to the TextBox SizeChanged event:

    private void myTextBox_SizeChanged(object sender, SizeChangedEventArgs e)
    {
        panel.Dispatcher.Invoke((Action)(() => 
        {
            if (panel.IsKeyboardFocusWithin)
            {
                // remove and add adorner to reset
                myAdornerLayer.Remove(myAdorner);
                myAdornerLayer.Add(myAdorner);
            }
        }), DispatcherPriority.Render, null);
    }

This basically comes from this post: http://geekswithblogs.net/NewThingsILearned/archive/2008/08/25/refresh--update-wpf-controls.aspx

Scott J
This feels a little bit like swatting a fly with a sledgehammer: It works but is awkward and inefficient and doesn't address the underlying cause of the problem, which is that your daughter left the screen door open. :-) The reason this works is that removing the adorner from the layer causes it to temporarily lose its PresentationSource which turns on the "needs render" bit. See my answer for more details and a cleaner way to fix this.
Ray Burns
+7  A: 

WPF has a built-in mechanism to cause all Adorners to be remeasured, rearranged, and rerendered whenever the corresponding AdornedElement changes size, position, or transform. This mechanism requires you to follow certain rules when coding your adorner, not all of which are documented as clearly as they ought to be.

I will first answer your title question of why your adorner doesn't consistenty re-render, then explain the best way to fix it.

Why the adorner doesn't re-render

Whenever an AdornerLayer receives a LayoutChanged notification it scans each of its Adorners to see if the AdornedElement has changed in size, position or transform. If so, it sets flags to force the Adorner to measure, arrange, and render again -- roughly equivalent to InvalidateMeasure(); InvaliateArrange(); InvalidateVisual();.

What normally happens in this situation is that the control is first measured, then arranged, then rendered. In fact, WPF tries to make this the most common case because it is the most efficient sequence. However there are many situations where a control can end up being rearranged and/or rerendered before it is remeasured. This is a legitimate order of events in WPF (to allow flexible layout techniques), but it is not common so it is often not tested.

A correctly implemented Adorner or other UIElement will be careful to call InvalidateVisual() any time the rendering may be affected unless only AffectsRender dependency properties were changed.

In your case, your adorner's size clearly affect rendering. The size properties are not AffectsRender dependency properties, so it is necessary to manualy call InvalidateVisual() when they change. If you don't, WPF may never know to re-render your adorner.

What is happening in your situation is probably this:

  • Layout completes and the LayoutChanged event fires
  • AdornerLayer discovers the size change on your AdornedElement
  • AdornerLayer schedules your adorner for re-measure, re-layout, and re-render
  • Something causes Arrange() to be called which causes the re-layout and re-render to happen before the re-measure. This causes WPF to think the adorner no longer needs a re-layout or re-render.
  • The layout engine detects that the adorner needs measuring and calls Measure
  • The adorner's MeasureOverride recomputes the desired size but does nothing to tell WPF the adorner needs to re-render
  • The layout engine decides there is nothing more to be done and so the adorner never re-renders

What you can do to fix it

The solution is, of course, to fix the bug in the Adorner by calling InvalidateVisual() whenever the control is re-measured, like this:

protected override Size MeasureOverride(Size constraint)
{
  var result = base.MeasureOverride(constraint);
  // ... add custom measure code here if desired ...
  InvalidateVisual();
  return result;
}

Doing this will cause your Adorner to consistently obey all the rules of WPF, so it will work as expected in all situations. This is also the most efficient solution, since InvalidateVisual() will do nothing at all except in those cases where it is really needed.

Ray Burns
How strange that the size properties don't affect rendering -- hadn't run across this before, but seems like a nasty little gotcha and I'm surprised it doesn't come up more often! Do you know (or can you speculate) why the WPF designers built it this way? In any case, thank you for such a clear, detailed and informative answer.
itowlson
@itowlson: You probably never ran across it before because in the most common case - where RenderSize is updated only within ArrangeCore and render doesn't depend on any other size - rerendering is consistently triggered without needing the InvalidateVisual() call. Log a sequence of ArrangeOverride and OnRender calls to get a better feel for how this works. My guess is the designers didn't want to automatically schedule an OnRender every time RenderSize is set because they envisioned scenarios where RenderSize would be changed and then immediately changed back.
Ray Burns