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?
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 yourAdornedElement
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.
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
精彩评论