monadoboi
monadoboi

Reputation: 1811

WPF - Adding a Tooltip to a GridViewColumn with DisplayMemberBinding

I have a TextBlock inside a GridViewColumn that requires a DisplayMemberBinding. The TextBlock is completely unaccounted for as DisplayMemberBinding takes precedence over CellTemplate. However, the Textblock has a ToolTip that I'd like to be displayed that's specific to the column itself. I was able to move most Style settings outside the CellTemplate as they are generic for all columns, but the ToolTip cannot be taken outside as it requires a binding and is unique to each column.

Here is one of the columns. Everything within the GridViewColumn.CellTemplate tags is removable due to the DisplayMemberBinding taking precedence.

            <GridViewColumn Header="Templates"
                            Width="200" 
                            DisplayMemberBinding="{Binding Path=Name}>
              <GridViewColumn.CellTemplate> 
                <DataTemplate DataType="{x:Type request:ModelDocument}">
                  <TextBlock ToolTip="{Binding Name}"/>
                </DataTemplate>
              </GridViewColumn.CellTemplate>
            </GridViewColumn>

The way I've done other Style properties is as follows. This was done before the GridView (the GridView is a child of the ListView):

        <ListView.Resources>
          <Style TargetType="TextBlock">
            <Setter Property="TextTrimming" Value="CharacterEllipsis" />
            <Setter Property="TextWrapping" Value="NoWrap"/>
            <Setter Property="FontFamily" Value="Segue UI Light" />
            <Setter Property="FontSize" Value="13" />
          </Style>
        </ListView.Resources>

How can I add a unique ToolTip to each column without removing the DisplayMemberBinding?

Upvotes: 2

Views: 1845

Answers (2)

Corentin Pane
Corentin Pane

Reputation: 4943

Even though there are more appropriate solutions like using a DataGrid instead of a ListView, I'd like to show an answer that lets you do exactly what you want with a ListView, DisplayMemberBinding and a ToolTip on your TextBlocks.

In the end I have the following XAML:

<ListView ItemsSource="{Binding MyList}">
    <ListView.Resources>
        <local:GridRowPresenterToDataContextPropertiesConverter x:Key="GridRowPresenterToDataContextPropertiesConverter"></local:GridRowPresenterToDataContextPropertiesConverter>
        <Style TargetType="TextBlock">
            <Setter Property="TextTrimming" Value="CharacterEllipsis" />
            <Setter Property="TextWrapping" Value="NoWrap"/>
            <Setter Property="FontFamily" Value="Segue UI Light" />
            <Setter Property="FontSize" Value="13" />

            <!--THIS IS THE NEW PART-->

            <Setter Property="ToolTip">
                <Setter.Value>
                    <MultiBinding Converter="{StaticResource GridRowPresenterToDataContextPropertiesConverter}"
                                  ConverterParameter="Name, Value">
                        <Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=GridViewRowPresenter}"/>
                        <Binding RelativeSource="{RelativeSource Mode=Self}"/>
                    </MultiBinding>
                </Setter.Value>
            </Setter>

            <!--END OF THE NEW PART-->

        </Style>
    </ListView.Resources>
    <ListView.View>
        <GridView>
            <GridViewColumn Header="Templates" Width="200" DisplayMemberBinding="{Binding Name}"/>
            <GridViewColumn Header="Templates" Width="200" DisplayMemberBinding="{Binding Value}"/>
        </GridView>
    </ListView.View>
</ListView>

I added a simple view model class and another GridViewColumn for testing purposes:

public class MyViewModel {
    public string Name { get; set; } = "Hello.";
    public string Value { get; set; } = "world.";
}

This XAML will set the Name property of the DataContext as ToolTip for all TextBlocks in the first column, and the Value property of the DataContext as ToolTip for all TextBlocks in the second column. You can handle additional columns by adding to the ConverterParameter="Name, Value" ordered list of properties.

The key is in the definition of GridRowPresenterToDataContextPropertiesConverter (which is one of the least straightforward and most hacky IMultiValueConverter I might have ever written):

public class GridRowPresenterToDataContextPropertiesConverter : IMultiValueConverter {
    public static T FindVisualSelfOrChildren<T>(DependencyObject parent) where T : DependencyObject {
        if (parent == null) return null;
        if (parent is T) return parent as T;
        IEnumerable<DependencyObject> children = Enumerable.Range(0, VisualTreeHelper.GetChildrenCount(parent)).Select(i => VisualTreeHelper.GetChild(parent, i));
        foreach (var child in children) {
            T result = FindVisualSelfOrChildren<T>(child);
            if (result != null) return result as T;
        }
        return null;
    }

    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
        var presenter = (GridViewRowPresenter)values[0];
        var targetElement = values[1] as FrameworkElement;
        var sourceProperties = ((string)parameter).Split(',');

        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(presenter); i++) {
            var child = VisualTreeHelper.GetChild(presenter, i);
            if (FindVisualSelfOrChildren<TextBlock>(child) == targetElement) {
                var dataContext = targetElement.DataContext;
                return dataContext.GetType().GetProperty(sourceProperties[i].Trim()).GetValue(dataContext);
            }
        }
        return "";
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotImplementedException();
}

This converter takes the target element (here, it's gonna be each TextBlock), figures out in which column of the GridView is lives and extracts the property at the correct index from the ConverterParameter.

This answer is more written for fun than for use in production code though, and it will need some smarter code to handle re-ordering of columns which is absolutely not supported now.

Upvotes: 1

SamTh3D3v
SamTh3D3v

Reputation: 9944

As it was already suggested in the comments by @icebat, a DataGrid is more suited for such scenarios, for the sake of clarification here a simple demo implementation:

   <DataGrid ItemsSource="{Binding Collection}" AutoGenerateColumns="False">
    <DataGrid.Resources>
        <Style TargetType="TextBlock" x:Key="TbStyle">
            <Setter Property="TextTrimming" Value="CharacterEllipsis" />
            <Setter Property="TextWrapping" Value="NoWrap"/>
            <Setter Property="FontFamily" Value="Segue UI Light" />
            <Setter Property="FontSize" Value="13" />
        </Style>

    </DataGrid.Resources>
    <DataGrid.Columns>
        <DataGridTemplateColumn  Header="Templates" SortMemberPath="Name"
                         Width="200"  >
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate >
                    <TextBlock Style="{StaticResource TbStyle}" ToolTip="{Binding Name}" Text="{Binding Name}"/>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>

        </DataGridTemplateColumn>
    </DataGrid.Columns>
</DataGrid>

Upvotes: 3

Related Questions