• Microsoft .NET
  • WPF и Silverlight
  • Silverlight 2.0 – как использовать связывание для динамического изменения цвета фона отдельного ряда DataGrid

Silverlight 2.0 – Как использовать DataBinding вместе с ToolTipService

Статья рассматривает использование DataBinding вместе с ToolTipService

Простой стандартный тип ToolTipService, предлагаемый  Silverlight, предоставляет прикрепленное свойство под именем ToolTip(всплывающая подсказка), позволяющее присваивать всплывающее окно объекту зависимости.

<Button Content='Button 1'>
    <ToolTipService.ToolTip>
        <TextBlock Text='Это кнопка 1' />
    </ToolTipService.ToolTip>
</Button>

ToolTipService заботится обо всех деталях, задействованных в показе, отрисовке и скрытии окна всплывающей подсказки. Более того, графическое содержимое всплывающей подсказки практически безгранично, что позволяет легко добавить мощную обратную связь с пользователем.

К сожалению, DataContext владеющего FrameworkElement (кнопка 1) не наследуется визуальным деревом, присвоенным свойству ToolTipService.ToolTip, что затрудняет связывание свойств элемента всплывающей подсказки, где DataContext не доступен легко в вашем Xaml. Следующий Xaml не работает  так, как ожидается:

<Button Content='Button 1' DataContext='{StaticResource Person}'>
    <ToolTipService.ToolTip>
        <StackPanel Orientation='Vertical'>
            <TextBlock Text='{Binding Path=FirstName}' />
            <TextBlock Text='{Binding Path=LastName}' />
        </StackPanel>
    </ToolTipService.ToolTip>
</Button>

… и дает пустое окно всплывающей подсказки, так как связывания не разрешают источник.

В данном конкретном примере легко скопировать атрибут DataContext и декорировать StackPanel таким же атрибутом, но часто нет доступа к такому удобному ресурсу связывания, и так как Silverlight пока не поддерживает связывания элементов, то эту проблему трудно решить с помощью Xaml.

В случае сомнения пишите сами.

using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Diagnostics;

Сначала  надо создать статический класс управления, эффективно оборачивающий существующий ToolTipService. Такой тип должен уметь устанавливать DataContext визуального корня всплывающей подсказки в DataContext прикрепленного графического элемента при любом его изменении, и его использование из Xaml должно быть столь же легким, как и для существующего сервиса.

/// <summary>
/// Копия класса ToolTipService с поддержкой прикрепления DataContext от родительского объекта зависимости, если цель и источник
/// являются элементами каркаса. Фактическое управление всплывающей подсказкой передается ToolTipService
/// </summary>

public static class ToolTipManager {

    public static readonly DependencyProperty ToolTipProperty =
        DependencyProperty.RegisterAttached("ToolTip", typeof(FrameworkElement), typeof(ToolTipManager),
                            new PropertyMetadata(new PropertyChangedCallback(ToolTipManager.OnToolTipChanged)));

    public static FrameworkElement GetToolTip(DependencyObject obj) {
        return (FrameworkElement)obj.GetValue(ToolTipManager.ToolTipProperty);
    }

    public static void SetToolTip(DependencyObject obj, FrameworkElement element) {
        obj.SetValue(ToolTipManager.ToolTipProperty, element);
    }

    private static void OnToolTipChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) {

        var sfe = sender as FrameworkElement;
        var tfe = e.NewValue as FrameworkElement;

        if(object.ReferenceEquals(null, sfe) || object.ReferenceEquals(null, tfe)) return;

        ToolTipService.SetToolTip(sender, e.NewValue);
    }
}

Тип предоставляет прикрепленное свойство под именем ToolTip так же, как это делает базовый сервис, и мы регистрируем метод (OnToolTipChanged), вызываемый всегда, когда визуальное дерево всплывающей подсказки связывается с объектом зависимости. Получатель и установщик свойства присутствуют для каркаса.

Как видно в методе обратного вызова, на самом деле нам интересны лишь типы FrameworkElement, потому что они предоставляют свойство DataContext. Последняя строка кода метода регистрирует визуальное дерево подсказки в базовом ToolTipService, который позволяет ему делать всю реальную работу за нас.

Пока DataContext графического элемента подсказки не был установлен.

Есть несколько разных способов сделать это. При установке в обратном вызове DataContext отправителя, вероятно, не будет известен, так как элемент не будет загружен. Поэтому можно отсрочить установку контекста, прикрепив обработчик к его событию Загружено, но даже при этом (кроме ряда крайних случаев, когда он вообще не будет работать) он не считывает дальнейшие изменения, вносимые в DataContext.

FrameworkElement имеет событие DataContextChanged, но оно является внутренним по области действия, поэтому не может быть прицеплено. Более того, Silverlight поддерживает прикрепление делегатов уведомления об изменении к DependencyProperty только при их первоначальном создании.

Нестандартное мышление позволяет побороть эту проблему, хотя каркас вынуждает работать усердней, чем нужно. Добавим новое DependencyProperty по имени DataContext, но с закрытой областью действия.

/// <summary>
/// Скрытое свойство зависимости, позволяющее получать уведомления, когда контекст данных источника меняется и
/// должен быть отправлен в контекст подсказки
/// </summary>

private static readonly DependencyProperty DataContextProperty =
    DependencyProperty.RegisterAttached("DataContext", typeof(object), typeof(ToolTipManager),
                        new PropertyMetadata(new PropertyChangedCallback(ToolTipManager.OnDataContextChanged)));

Идея в том, что мы связываем новое свойство зависимости с прикрепляющим элементом и получаем уведомления об обратном вызове, когда связанное значение меняется. Теперь надо внести небольшое изменение в метод OnToolTipChanged, как показано ниже.

var sfe = sender as FrameworkElement;
var tfe = e.NewValue as FrameworkElement;

if(object.ReferenceEquals(null, sfe) || object.ReferenceEquals(null, tfe)) return;

tfe.DataContext = sfe.DataContext;

sfe.SetBinding(ToolTipManager.DataContextProperty, new Binding());

ToolTipService.SetToolTip(sender, e.NewValue);

Осталось обработать изменение DataContext при вызове обратного вызова.

public static void OnDataContextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) {

    var sfe = sender as FrameworkElement;
    var tfe = ToolTipManager.GetToolTip(sender) as FrameworkElement;

    Debug.Assert(!(object.ReferenceEquals(null, sfe) ||
                   object.ReferenceEquals(null, tfe)), "Неожиданная нулевая ссылка на прикрепленный FrameworkElement");

    tfe.DataContext = sfe.DataContext;

    // Было бы предпочтительно отсоединить подсказку без контекста данных и затем снова прикрепить ее, когда добавится новая подсказка, но
    // его переустановка выбрасывает внутреннее исключение hresult (видимо, так как он уже является частью визуального дерева?), поэтому так делать нельзя.
    // Минус в том, что подсказка может показывать пустое окно, если контекст данных неизвестен

    //if(object.ReferenceEquals(null, tfe.DataContext))
    //    ToolTipService.SetToolTip(sender, null);
    //else
    //    ToolTipService.SetToolTip(sender, tfe);
}

Одна строка кода извлекает DataContext из прикрепленного элемента и передает его в визуальный корень подсказки.

Из комментариев можно понять следующее. Сначала мы пытались отцепить подсказку от владеющего элемента, если DataContext был сброшен. К сожалению, когда мы пытались снова прикрепить дерево подсказки к владельцу при восстановлении контекста, каркас выбросил исключение. Хороший способ решения данной проблемы еще не найден, пока этот кусок кода закомментирован, и побочным эффектом является вероятность появления пустого окна подсказки.

Сейчас мы можем использовать новый сервис в Xaml так же легко, как и существующий сервис.

<Button Content='Button 1' DataContext='{StaticResource Person}'>
    <ip:ToolTipManager.ToolTip>
        <StackPanel Orientation='Vertical'>
            <TextBlock Text='{Binding Path=FirstName}' />
            <TextBlock Text='{Binding Path=LastName}' />
        </StackPanel>
    </ip:ToolTipManager.ToolTip>
</Button>

Пока не найдена ситуация, в которой этот код не работает, но, вероятно, здесь где-то спрятаны один-два необычных крайних случая ….. базовый сервис делал бы это в иных случаях.