2

Currently I am trying to bind the IsOpen property of a ToolTip to a property of my backing view model. Additionally The binding mode is set to 'OneWayToSource'.

This is the Style that is applied to a TreeViewItem and contains the ToolTip definition:

<Style TargetType="TreeViewItem">
    <Setter Property="ToolTip">
        <Setter.Value>
            <ToolTip IsOpen="{Binding IsToolTipOpen, Mode=OneWayToSource}">
                <StackPanel Orientation="Vertical">
                    <TextBlock Text="{Binding Name}"/>
                    <TextBlock Text="{Binding CurrentValue, StringFormat={}Value: {0}}"/>
                    <TextBlock Text="{Binding UnitName, StringFormat={}Unit: {0}}"
                               Visibility="{Binding HasUnit, Converter={StaticResource BooleanToVisibilityConverter}}"/>
                </StackPanel>
            </ToolTip>
        </Setter.Value>
    </Setter>
</Style>

Here is the code for the property that it is binding to:

public bool IsToolTipOpen
{
    get
    {
       return mIsToolTipOpen;
    }
    set
    {
        PegasusContext.Current.LogMessage( new PegasusMessage( string.Format( "IsTooltipOpen: {0}", value ), LogLevel.Debug ) );

        if( mIsToolTipOpen == value ) return;
        mIsToolTipOpen = value;

        if( mIsToolTipOpen )
        {
            BackingIO.BeginWatching();
        }
        else
        {
            BackingIO.StopWatching();
        }
    }
}

When the ToolTip is opened for the first time, it will invoke the IsToolTipOpen property setting it's value to false. Additionally when the ToolTip closes, it will set the value of IsToolTipOpen to false...again. Every subsequent time, the value will get set as expected. After opening the first ToolTip, it will perform strange behavior on other items with the ToolTip attached. For instance it will set the IsToolTipOpen property to true and then back to false almost immediately. Again every subsequent time the ToolTip is opened, it works normally.

Here is report from the logging code you can see on the first line of my IsToolTipOpen property set method (with some additional comments I handwrote in):

TreeViewItem A:
IsTooltipOpen: False <-- ToolTip Opened
IsTooltipOpen: False <-- ToolTip Closed
IsTooltipOpen: True  <-- ToolTip Opened
IsTooltipOpen: False <-- ToolTip Closed

TreeViewItem B:
IsTooltipOpen: True <-- ToolTip Open
IsTooltipOpen: False <-- ToolTip Open, occured at the same time as the previous entry.
IsTooltipOpen: False <-- ToolTip Closed
IsTooltipOpen: True <-- ToolTip Opened
IsTooltipOpen: False <-- ToolTip Closed

So I was curious if anyone had any idea what was going on? and possible solutions?

Dave Clemmer
  • 3,741
  • 12
  • 49
  • 72
deloreyk
  • 1,248
  • 15
  • 24

2 Answers2

1

It appears that the events are working as intended with the ToolTip class. So I created an attached property to fix the problem I was having.

I have a attached property to enable the registration of events:

public static readonly DependencyProperty EnableWatchProperty = DependencyProperty.RegisterAttached(
        "EnableWatch",
        typeof( bool ),
        typeof( ToolTipFix ),
        new PropertyMetadata( default( bool ), OnEnableWatchPropertyChanged ) );

    private static void OnEnableWatchPropertyChanged( DependencyObject d, DependencyPropertyChangedEventArgs e )
    {
        var toolTip = d as ToolTip;
        if( toolTip == null ) return;

        var newValue = (bool) e.NewValue;

        if( newValue )
        {
            toolTip.Opened += OnTooltipOpened;
            toolTip.Closed += OnTooltipClosed;
        }
        else
        {
            toolTip.Opened -= OnTooltipOpened;
            toolTip.Closed -= OnTooltipClosed;
        }
    }

I also have an attached property that can be binded to, and it indicates the current state of the ToolTip:

public static readonly DependencyProperty IsOpenProperty = DependencyProperty.RegisterAttached(
        "IsOpen",
        typeof( bool ),
        typeof( ToolTipFix ),
        new PropertyMetadata( default( bool ) ) );

The event handlers just set the IsOpen property to true or false depending on the event that was invoked.

Here is how I used the the attached property in XAML:

<ToolTip local:ToolTipFix.EnableWatch="True" 
         local:ToolTipFix.IsOpen="{Binding IsToolTipOpen, Mode=OneWayToSource}">
    ...
</ToolTip>

If anyone has another solution, or reason for this problem, I would very much appreciate it.

deloreyk
  • 1,248
  • 15
  • 24
1

This was confusing the heck out of me, and I couldn't help but poke at it for while to try and understand what's going on. I'm not entirely sure I arrived at a complete understanding, but I'm further than when I started =D

I replicated your behaviour for a very simple ToolTip in a Grid, I had a good look at events and the ToolTipService and got nowhere, then started using WPF Snoop (no affiliation) to examine the DataContext on the ToolTip when the application was first started.

Seemingly, the ToolTip has no DataContext. As soon as you open it for the first time. No Context

The ToolTip inherits the DataContext. Context

So what I assume is happening (this is where I'm hazy), is that when you first mouse over the ToolTip, the DataContext has not yet been correctly bound, so your get/set can't fire correctly; the reason you see any messages at all is a result of the behaviour of the OneWayToSource binding mode explained here: OneWayToSource Binding seems broken in .NET 4.0. If you choose TwoWay binding, you'll notice the ToolTip doesn't open at all the first time round, but will function correctly after the late binding of the DataContext.

The reason the ToolTip does this, is a result of the way it's implemented, and the possible sharing of ToolTips that can occur; it doesn't live within the visual tree:

A tooltip (and everything in it) is not part of the visual tree - it is not in a parent-child relationship with the Image, but rather in a property-value relationship.

When a tooltip is actually displayed, WPF does some special work to propagate the values of inheritable properties from the placement-target into the tooltip,

So, I think that's sounding plausible, but is there anything you could actually do about it?

Well, some people have wanted to bind the ToolTip to things on the visual tree, and given the difficulties faced in locating the parent objects, a few workarounds have arisen (I cameacross these while looking for a solution: http://blogs.msdn.com/b/tom_mathews/archive/2006/11/06/binding-a-tooltip-in-xaml.aspx Getting a tooltip in a user control to show databound text and stay open

Both rely on a neat property of the ToolTip, PlacementTarget, which "Gets or sets the UIElement relative to which the ToolTip is positioned when it opens. MSDN.

So, in summary, to get the IsOpen property to bind and behave correctly, I did this in my mockup:

<Window x:Class="WpfApplication1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfApplication1"
    Title="MainWindow" 
    DataContext="{Binding RelativeSource={RelativeSource Self}}" Background="#FF95CAFF" 
    >
<Grid>
    <TextBlock Text="{Binding Path=TestObject.Name}">
        <TextBlock.ToolTip>
            <ToolTip IsOpen="{Binding DataContext.TestObject.IsToolTipOpen}" 
                DataContext="{Binding Path=PlacementTarget,
                    RelativeSource={RelativeSource Self}}" >
                <StackPanel Orientation="Vertical">
                    <!-- Show me what the `DataContext` is -->
                    <TextBlock Text="{Binding Path=DataContext}" />
                </StackPanel>
            </ToolTip>
        </TextBlock.ToolTip>
    </TextBlock>
</Grid>

Context found on program load. Happy Context

In this case, the DataContext is WpfApplication1.MainWindow, and I'm binding to a property on that I'm using for testing, called TestObject. You would probably have to fiddle with the syntax a little bit to locate your desired `DataContext item in your template (it might be easier to do it the way you've already done it depending on your structure).

If I've said anything drastically incorrect, let me know.

Community
  • 1
  • 1
Chris
  • 8,268
  • 3
  • 33
  • 46
  • The behavior makes total since now. It's still a strange, but I think you have nailed it. I'll probably give it a shot when I get some free time on this project. Thanks for the reply! – deloreyk Nov 12 '13 at 23:23
  • Good luck, there's something useful in there somewhere =D – Chris Nov 13 '13 at 09:04