Почему мой обожатель не повторяет рендер, когда элемент, который он применил к изменениям?

В пользовательском интерфейсе, который я создаю, я хочу украсить панель, когда один из элементов управления на панели имеет фокус. Поэтому я обрабатываю событие IsKeyboardFocusWithinChanged и добавляю adorner к элементу, когда он получает фокус и удаляет рекламодателя, когда он теряет фокус. Кажется, что это работает нормально.

Проблема, с которой я сталкиваюсь, заключается в том, что adorner не получает повторного отображения, если границы украшенного элемента изменяются. Например, в этом простом случае:

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

adorner правильно украшает границы WrapPanel, когда TextBox получает фокус, но по мере ввода текста текст TextBox расширяется под краем adorner. Конечно, как только я делаю все, что заставляет adorner визуализировать, например ALT-TAB, из приложения или отдавать другой фокус, он исправляет себя. Но как я могу получить его для повторной рендеринга при изменении границ стилизованного элемента?

Ответы

Ответ 1

WPF имеет встроенный механизм, который заставляет все Adorners переоцениваться, переупорядочиваться и переписываться всякий раз, когда соответствующий AdornedElement изменяет размер, положение или преобразование. Этот механизм требует, чтобы вы следовали определенным правилам при кодировании вашего adorner, не все из которых документированы так четко, как они должны быть.

Сначала я отвечу на ваш титульный вопрос , почему ваш adorner не будет повторно перерисовываться, а затем объяснит лучший способ его исправить.

Почему adorner не реинжинирует

Всякий раз, когда AdornerLayer получает уведомление LayoutChanged, он сканирует каждый из своих Adorners, чтобы увидеть, изменился ли AdornedElement по размеру, положению или преобразованию. Если это так, он устанавливает флаги, чтобы заставить Adorner измерять, упорядочивать и отображать снова - примерно эквивалентно InvalidateMeasure(); InvaliateArrange(); InvalidateVisual();.

Что обычно происходит в этой ситуации, так это то, что контроль сначала измеряется, затем упорядочивается, затем отображается. На самом деле WPF пытается сделать это наиболее распространенным случаем, потому что это наиболее эффективная последовательность. Однако есть много ситуаций, когда контроль может быть перегруппирован и/или удален, прежде чем он будет переоценен. Это законный порядок событий в WPF (для обеспечения гибких методов компоновки), но он не является обычным явлением, поэтому его часто не тестируют.

Правильно реализованный Adorner или другой UIElement будет осторожно вызывать InvalidateVisual() в любое время, когда рендеринг может быть затронут, если только свойства зависимостей AffectsRender не были изменены.

В вашем случае размер вашего adorner явно влияет на рендеринг. Свойства размера не являются AffectsRender свойствами зависимостей, поэтому при их изменении необходимо вручную вызвать InvalidateVisual(). Если вы этого не сделаете, WPF, возможно, никогда не узнает о том, чтобы повторно рекламировать вашего adorner.

Что происходит в вашей ситуации, возможно, это:

  • Макет завершается, а событие LayoutChanged запускается
  • AdornerLayer обнаруживает изменение размера на AdornedElement
  • AdornerLayer назначает вашего рекламодателя для повторной оценки, повторной компоновки и повторной рендеринга
  • Что-то вызывает вызов Arrange(), который вызывает повторную компоновку и повторную визуализацию до повторной оценки. Это заставляет WPF думать, что рекламодатель больше не нуждается в повторной компоновке или повторной обработке.
  • Механизм компоновки обнаруживает, что рекламодатель нуждается в измерении и вызывает Measure
  • Adorner MeasureOverride пересчитывает желаемый размер, но ничего не говорит WPF, что рекламодатель нуждается в повторной рендеринге
  • Механизм компоновки решает, что больше нечего делать, и поэтому adorner никогда не переделывает

Что вы можете сделать, чтобы исправить его

Разумеется, решение состоит в том, чтобы исправить ошибку в Adorner, вызывая InvalidateVisual() всякий раз, когда элемент управления повторно измеряется, например:

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

Выполнение этого приведет к тому, что ваш Adorner будет последовательно подчиняться всем правилам WPF, поэтому он будет работать как ожидалось во всех ситуациях. Это также наиболее эффективное решение, так как InvalidateVisual() ничего не сделает, кроме тех случаев, когда это действительно необходимо.

Ответ 2

Вам нужно вызвать диспетчера на панели. Добавить обработчик события TextBox SizeChanged:

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

В основном это сообщение: http://geekswithblogs.net/NewThingsILearned/archive/2008/08/25/refresh--update-wpf-controls.aspx