Как установить PlacementTarget для всплывающей подсказки WPF, не перепутав DataContext? - PullRequest
4 голосов
/ 22 июля 2011

У меня есть типичная настройка MVVM для Listbox и vm + DataTemplate и элемента vm. Шаблоны данных имеют всплывающие подсказки, в которых есть элементы, связанные с элементом vm. Все отлично работает.

Теперь я бы хотел, чтобы подсказка была размещена относительно самого списка. Он довольно большой и мешает при случайном наведении мышки на список. Поэтому я решил сделать что-то подобное в DataTemplate:

<Grid ...>
    <TextBlock x:Name="ObjectText"
        ToolTipService.Placement="Left"
        ToolTip="{StaticResource ItemToolTip}"
        ToolTipService.PlacementTarget="{Binding RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}">
    </TextBlock>
...

... со статическим ресурсом ...

<ToolTip x:Key="ItemToolTip">
    <StackPanel>
        <TextBlock Text="{Binding DisplayName.Name}"/>
        <TextBlock Text="{Binding Details}" FontStyle="Italic"/>
        ...
    </StackPanel>
</ToolTip>

Вот моя проблема. Когда я использую этот PlacementTarget, я получаю сообщение об ошибке привязки, что DisplayName.Name и Details не являются обязательными. Объект, к которому он пытается привязаться, это не элемент vm, а общий список List vm.

Итак, мой вопрос: как я могу установить ToolTipService.PlacementTarget для всплывающей подсказки, но сохранить DataContext, унаследованный от его владельца?

Ответы [ 3 ]

7 голосов
/ 27 июля 2011

Хорошо, друг на работе в основном понял это для меня. Этот способ очень чистый, не чувствуешь себя счастливым.

Вот основная проблема: как упоминалось пользователем 164184, всплывающие подсказки являются всплывающими окнами и, следовательно, не являются частью визуального дерева. Так что есть какая-то магия, которую делает WPF. DataContext для всплывающего окна получен из PlacementTarget, так как привязки работают большую часть времени, несмотря на то, что всплывающее окно не является частью дерева. Но при изменении PlacementTarget это переопределяет значение по умолчанию, и теперь DataContext исходит из нового PlacementTarget, каким бы он ни был.

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

Во всяком случае, на решение! Как и во всех забавных трюках WPF, на помощь приходят прикрепленные свойства. Мы собираемся добавить два прикрепленных свойства, чтобы мы могли напрямую установить DataContext всплывающей подсказки при ее создании.

public static class BindableToolTip
{
    public static readonly DependencyProperty ToolTipProperty = DependencyProperty.RegisterAttached(
        "ToolTip", typeof(FrameworkElement), typeof(BindableToolTip), new PropertyMetadata(null, OnToolTipChanged));

    public static void SetToolTip(DependencyObject element, FrameworkElement value) { element.SetValue(ToolTipProperty, value); }
    public static FrameworkElement GetToolTip(DependencyObject element) { return (FrameworkElement)element.GetValue(ToolTipProperty); }

    static void OnToolTipChanged(DependencyObject element, DependencyPropertyChangedEventArgs e)
    {
        ToolTipService.SetToolTip(element, e.NewValue);

        if (e.NewValue != null)
        {
            ((ToolTip)e.NewValue).DataContext = GetDataContext(element);
        }
    }

    public static readonly DependencyProperty DataContextProperty = DependencyProperty.RegisterAttached(
        "DataContext", typeof(object), typeof(BindableToolTip), new PropertyMetadata(null, OnDataContextChanged));

    public static void SetDataContext(DependencyObject element, object value) { element.SetValue(DataContextProperty, value); }
    public static object GetDataContext(DependencyObject element) { return element.GetValue(DataContextProperty); }

    static void OnDataContextChanged(DependencyObject element, DependencyPropertyChangedEventArgs e)
    {
        var toolTip = GetToolTip(element);
        if (toolTip != null)
        {
            toolTip.DataContext = e.NewValue;
        }
    }
}

А потом в XAML:

<Grid ...>
    <TextBlock x:Name="ObjectText"
        ToolTipService.Placement="Left"
        ToolTipService.PlacementTarget="{Binding RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"
        mystuff:BindableToolTip.DataContext="{Binding}">
        <mystuff:BindableToolTip.ToolTip>
            <ToolTip>
                <StackPanel>
                    <TextBlock Text="{Binding DisplayName.Name}"/>
                    <TextBlock Text="{Binding Details}" FontStyle="Italic"/>
                    ...
                </StackPanel>
            </ToolTip>
        </mystuff:BindableToolTip.ToolTip>
    </TextBlock>
...

Просто переключите ToolTip на BindableToolTip.ToolTip, а затем добавьте новый BindableToolTip.DataContext, который указывает на то, что вы хотите. Я просто устанавливаю его в текущий DataContext, поэтому он наследует модель представления, привязанную к DataTemplate.

Обратите внимание, что я встроил всплывающую подсказку вместо использования StaticResource. Это была ошибка в моем первоначальном вопросе. Очевидно, должен быть создан уникальный для каждого элемента. Другим вариантом будет использование триггера в стиле ControlTemplate.

Одним из улучшений может быть регистрация BindableToolTip.DataContext для уведомлений об изменении всплывающей подсказки, а затем я мог бы избавиться от BindableToolTip.ToolTip. Задача на другой день!

5 голосов
/ 27 июля 2011

Подсказки не являются частью визуального дерева, так как они основаны на всплывающих окнах. Таким образом, ваша цель размещения размещения (которая использует поиск по дереву визуальных), чтобы получить относительный предок не будет работать. Почему бы вместо этого не использовать ContentHacking? Таким образом можно взломать визуальное дерево с помощью логических элементов, таких как ContextMenu, Popups, ToolTip и т. Д. *

  1. Объявите StaticResource, который является любым FrameworkElement (нам нужна поддержка контекста данных).

    <UserControl.Resources ...>
            <TextBlock x:Key="ProxyElement" DataContext="{Binding}" />
    </UserControl.Resources>
    
  2. Укажите элемент управления содержимым в дереве визуалов и установите этот статический ресурс "ProxyElement" в качестве его содержимого.

    <UserControl ...>
            <Grid ...>
                    <ItemsControl x:Name="MyItemsControl"
                                  ItemsTemplate="{StaticResource blahblah}" .../>
                    <ContentControl Content="{StaticResource ProxyElement}"
                                    DataContext="{Binding ElementName=MyItemsControl}" Visibility="Collapsed"/>
    

Что сделали вышеуказанные шаги, чтобы «ProxyElement» был подключен к ItemsControl (который служит в качестве DataContext), и он доступен как SaticResource для использования в любом месте.

  1. Теперь используйте этот StaticResource в качестве источника для любых привязок, которые не работают в вашей подсказке ...

    <Grid ...>
            <TextBlock x:Name="ObjectText"
                       ToolTipService.Placement="Left"
                       ToolTip="{StaticResource ItemToolTip}"
                       PlacementTarget="{Binding Source={StaticResource ProxyElement}, Path=DataContext}" ... /> <!-- This sets the target as the items control -->
    

и

    <ToolTip x:Key="ItemToolTip">
            <StackPanel DataContext="{Binding Source={StaticResource ProxyElement}, Path=DataContext.DataContext}"><!-- sets data context of the items control -->
                    <TextBlock Text="{Binding DisplayName.Name}"/>
                    <TextBlock Text="{Binding Details}" FontStyle="Italic"/> ...
            </StackPanel>
    </ToolTip>

Дайте мне знать, если это поможет ...

0 голосов
/ 22 июля 2011

Как я понимаю [но я, вероятно, ошибаюсь (без вреда при попытке)], вы можете инициализировать свои элементы со ссылкой на объекты, которые использовались в предке DataContext, т.е.

public class ItemsVM<T> : VMBase
{
     public T parentElement;
     public ItemsVM (T _parentElement)
     {
         this.parentElement = _parentElement;
     }

     ...

}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...