Search code examples
wpfxamldata-binding

Binding ToolTip in UserControl


I tried to bind a ToolTip text in a UserControl this way:

<Grid.ToolTip>
    <TextBlock
        Text="{
            Binding Path=InfoTT,  
            RelativeSource={
                RelativeSource Mode=FindAncestor, 
                AncestorType={x:Type UserControl}
            }
        }" />
</Grid.ToolTip>

And it doesn't work, the Tooltip was empty and in logs, I saw:

System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.UserControl', AncestorLevel='1''. BindingExpression:Path=InfoTT; DataItem=null; target element is 'TextBlock' (Name=''); target property is 'Text' (type 'String')*

But when I did:

<Grid
    ToolTip="{
        Binding Path=InfoTT,  
        RelativeSource={
            RelativeSource Mode=FindAncestor, 
            AncestorType={x:Type UserControl}
        }
    }">
</Grid>

It worked. Can anyone explain why the first way doesn't work?


Solution

  • When Binding.RelativeSource doesn't resolve, you can always be sure that the Binding.Target is not part of the visual tree.

    In your first example you are explicitly defining the tree structure of the ToolTip. You are explicitly creating the content e.g. by adding the TextBlock. The content of the ToolTip is not part of the visual tree and therefore the Binding.RelativeSource can't be resolved.

    In your second example, you let the FrameworkElement implicitly create the ToolTip content.
    Now FrameWorkElement will first resolve the Binding, which resolves, as the FrameworkElement is still part of the visual tree. The resolved value is taken, ToString invoked, a TextBlock created and the string value assigned to TextBlock.Text.

    Solution

    To solve the binding problem, when implementing the ToolTip explicitly, you can implement a Binding Proxy as suggested in a comment by @Mark Feldman which makes use of the StaticResource markup to provide a Binding.Source to elements that are not part of the visual tree.
    It's basically a bindable ObjectDataProvider.

    A similar solution to the binding proxy is to define the content as a resource of the Grid and then reference it via DynamicResource using a ContentPresnter:

    <UserControl>
      <Grid>
        <Grid.Resources> 
          <!-- The proxy -->   
          <TextBlock x:Key="ToolTipText" 
                     Text="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=InfoTT}" />
        <Grid.ToolTip>
          <ToolTip>
            <ContentPresenter Content="{DynamicResource ToolTipText}" />
          </ToolTip>
        </Grid.ToolTip>
      </Grid>
    </UserControl>
    

    But you could also make use of the fact that the DataContext is still inherited. Bindings to the DataContext will still resolve.

    In your scenario, where you want to bind the content of the ToolTip to a property of the parent UserControl, you could bind this property to a property of the view model, which is the current DataContext of Grid (and therefore for its ToolTip). I only recommend this, when binding to business data and not layout data:

    <UserControl InfoTT="{Binding ViewModelInfoTT}">
      <UserControl.DataContext>
        <ViewModel />
      </UserControl.DataContext>
    
      <Grid>
        <Grid.ToolTip>
          <ToolTip>
            <TextBlock Text="{Binding ViewModelInfoTT}" />
          </ToolTip>
        </Grid.ToolTip>
      </Grid>
    </UserControl>
    

    If you don't use view models and host the data directly in the control, you may like to set the DataContext to the control itself. This way you simplify all bindings and of course can now bind to the UserControl from within the ToolTip:

    // Constructor
    public MyUserControl()
    {
      InitializeComponent();
    
      // Set the UserControl's DataContext to the control itself
      this.DataContext = this;
    }
    
    <UserControl>
      <Grid>
        <Grid.ToolTip>
          <ToolTip>
            <TextBlock Text="{Binding InfoTT}" />
          </ToolTip>
        </Grid.ToolTip>
      </Grid>
    </UserControl>
    

    Alternatively override the DataContext. Of course you'll lose access to the current context:

    <UserControl>
      <Grid DataContext="{Binding RelativeSource={RelativeSource AncestoType=UserControl}>
        <Grid.ToolTip>
          <ToolTip>
            <TextBlock Text="{Binding InfoTT}" />
          </ToolTip>
        </Grid.ToolTip>
      </Grid>
    </UserControl>