Why does my adorner not re-render when the element it's applied to changes?

Robert Rossney picture Robert Rossney · Mar 15, 2010 · Viewed 16.9k times · Source

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?

Answer

Ray Burns picture Ray Burns · Mar 26, 2010

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.