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