Thursday, June 11, 2015

Tooltips on DataGrid headers

I have a DataGridTextColumn which has a dynamically generated tooltip on each cell (binding the cell's tooltip via a multi-value converter) but I want a static tooltip on the column header that explains what the user is looking at. It turns out to be quite easy once you know how to do it.

Here is the original DataGridTextColumn definition...

<DataGridTextColumn Header="Total Pending" Width="60" Binding="{Binding Path=TotalQuantityPending, StringFormat=0.00;;#}" ElementStyle="{StaticResource TextBlockRightAlign}" IsReadOnly="true">
    <DataGridTextColumn.CellStyle>
        <Style TargetType="DataGridCell" BasedOn="{StaticResource DataGridCell}">
            <Setter Property="ToolTip">
                <Setter.Value>
                    <MultiBinding Converter="{StaticResource ConversionConverter}">
                        <Binding Path="UORCode"/>
                        <Binding Path="UOICode"/>
                        <Binding Path="Conversion"/>
                        <Binding Path="TotalQuantityReturned"/>
                    </MultiBinding>
                </Setter.Value>
            </Setter>
        </Style>
    </DataGridTextColumn.CellStyle>
</DataGridTextColumn>

Now to add a static tooltip to the header we take advantage of the fact that the header can be any content control. So we make it a textblock with it's own tooltip. Simple when you think of it.

<DataGridTextColumn Width="60" Binding="{Binding Path=TotalQuantityPending, StringFormat=0.00;;#}" ElementStyle="{StaticResource TextBlockRightAlign}" IsReadOnly="true">
    <DataGridTextColumn.Header>
        <TextBlock Text="Total Pending" ToolTip="Total received on this line on all Pending receipts."></TextBlock>
    </DataGridTextColumn.Header>
    <DataGridTextColumn.CellStyle>
        <Style TargetType="DataGridCell" BasedOn="{StaticResource DataGridCell}">
            <Setter Property="ToolTip">
                <Setter.Value>
                    <MultiBinding Converter="{StaticResource ConversionConverter}">
                        <Binding Path="UORCode"/>
                        <Binding Path="UOICode"/>
                        <Binding Path="Conversion"/>
                        <Binding Path="TotalQuantityReturned"/>
                    </MultiBinding>
                </Setter.Value>
            </Setter>
        </Style>
    </DataGridTextColumn.CellStyle>
</DataGridTextColumn>

Tuesday, June 2, 2015

Daisy-chaining converters

For WPF 4.0. Credit to Gareth Evans.

I have a converter that returns true if the current user has the specified security code. I have another converter that returns Visible if I pass in a true value otherwise Collapsed. I have a menu control that I want to only show if the user has a specified security code. I wish I could join my two converters so I didn't have to write a third one. Well it turns out I can...

Let's start with the two converters I have already written. Note I removed error handling for simplicity.

<ValueConversion(GetType(String), GetType(Boolean))>
Public Class SecurityToBooleanConverter
    Implements IValueConverter

    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As Globalization.CultureInfo) As Object Implements IValueConverter.Convert

        Return Application.Current.Properties.User_SecurityCodes.ContainsKey(parameter) 
    End Function

    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As Globalization.CultureInfo) As Object Implements IValueConverter.ConvertBack
        Throw New System.NotImplementedException("SecurityConverter")
    End Function
End Class

<ValueConversion(GetType(Boolean), GetType(Visibility))>
Public Class BooleanToVisibilityConverter
    Implements IValueConverter

    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As Globalization.CultureInfo) As Object Implements IValueConverter.Convert

        Return IIf(CBool(value), Visibility.Visible, Visibility.Collapsed)

    End Function

    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As Globalization.CultureInfo) As Object Implements IValueConverter.ConvertBack
        Throw New NotImplementedException
    End Function
End Class

In my resources I have created StaticResources like this...
    <local:SecurityToBooleanConverter x:Key="SecurityToBooleanConverter"/>
    <local:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>

Now here is the clever bit. In my list of converter classes I add the following generic code. This is the new class that will allow me to string as many converters together as I need.

Public Class ValueConverterGroup
    Inherits Collections.Generic.List(Of IValueConverter)
    Implements IValueConverter

    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As Globalization.CultureInfo) As Object Implements IValueConverter.Convert
        Return Me.Aggregate(value, Function(current, converter) converter.Convert(current, targetType, parameter, culture))
    End Function

    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As Globalization.CultureInfo) As Object Implements IValueConverter.ConvertBack
        Throw New NotImplementedException()
    End Function
End Class

Now I add a resource that describes how I want to string the converters together. In my example I want to convert from a security code to a boolean and then to a visibility. The new resource to do this looks like this. It could be defined globally in the Application.XAML or on a single page's XAML.

    <local:ValueConverterGroup x:Key="SecurityToVisibilityConverter">
        <local:SecurityToBooleanConverter></local:SecurityToBooleanConverter>
        <local:BooleanToVisibilityConverter></local:BooleanToVisibilityConverter>
    </local:ValueConverterGroup>

When InitializeComponent finds this resource it creates an instance of the ValueConverterGroup and populates the list of converters with a SecurityToBooleanConverter and a BooleanToVisibilityConverter. The ValidConverterGroup has a very clever use of Aggregate that essentially calls each converter in turn passing the value output from the previous converter and with the parameter and culture that was passed to the ValueConverterGroup.

The binding of my control's visibility looks like this...

    <MenuItem Header="Requisitions" Visibility="{Binding Converter={StaticResource SecurityToVisibilityConverter}, ConverterParameter='Requisition'}"/>

The only drawback I see with this solution is that you cannot pass different parameters to different steps in the conversion.