Thursday, April 24, 2014

NamedObject Error in Converters

This post is for WPF version 4.0

There is a subtle difference in the way IValueConverter and IMultiValueConverter handles DBNull that seems to confuse a lot of people. It certainly confused me.

I was intermittently getting an error in a MultiValueConverter where it was being passed a "NamedValue" object instead of the Boolean it was expecting. I spent quite a lot of time verifying the binding definitions in the XAML before I realized it was a data issue. To make things even more confusing a very similar ValueConverter (not multi) was working just fine.

Although this problem is more obvious in VB, it also has the potential to cause unexpected behavior in C#.

My example has a simple data grid that is bound to a datatable. It uses two converters - a multi value converter for the foreground and a simple converter for the background. Here's the XAML...


<Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication5"
        Title="MainWindow" Height="350" Width="525" 
        
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Window.Resources>
        <local:ForegroundConverter x:Key="ForegroundConverter"></local:ForegroundConverter>
        <local:BackgroundConverter x:Key="BackgroundConverter"></local:BackgroundConverter>
    </Window.Resources>
    <Grid>
        <DataGrid Name="DG" ItemsSource="{Binding Path=DT.DefaultView}" AutoGenerateColumns="False" CanUserAddRows="False">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Description" Binding="{Binding Path=Description}" IsReadOnly="true">
                    <DataGridTextColumn.ElementStyle>
                        <Style TargetType="{x:Type TextBlock}">
                            <Setter Property="Foreground">
                                <Setter.Value>
                                    <MultiBinding Converter="{StaticResource ForegroundConverter}">
                                        <Binding Path="IsError"></Binding>
                                        <Binding Path="IsWarning"></Binding>
                                    </MultiBinding>
                                </Setter.Value>
                            </Setter>
                            <Setter Property="Background" Value="{Binding Path=IsImportant, Converter={StaticResource BackgroundConverter}}"></Setter>
                        </Style>
                    </DataGridTextColumn.ElementStyle>
                </DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

Here are the converter classes...


Public Class ForegroundConverter
    Implements IMultiValueConverter

    Public Function Convert(values() As Object, targetType As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IMultiValueConverter.Convert

        Dim IsError As Boolean = values(0)
        Dim IsWarning As Boolean = values(1)

        If IsError Then
            Return New SolidColorBrush(System.Windows.Media.Colors.Red)
        End If

        If IsWarning Then
            Return New SolidColorBrush(System.Windows.Media.Colors.Blue)
        End If

        Return New SolidColorBrush(System.Windows.Media.Colors.Black)

    End Function

    Public Function ConvertBack(value As Object, targetTypes() As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object() Implements System.Windows.Data.IMultiValueConverter.ConvertBack
        Throw New NotImplementedException()
    End Function
End Class

Public Class BackgroundConverter
    Implements IValueConverter

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

        If CBool(value) Then
            Return New SolidColorBrush(System.Windows.Media.Colors.Yellow)
        Else
            Return New SolidColorBrush(System.Windows.Media.Colors.White)
        End If
    End Function

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


And here is the code for the MainWindow class that builds the datatable.

Imports System.Data

Class MainWindow

    Public Property DT As DataTable

    Public Sub New()

        DT = New DataTable

        DT.Columns.Add(New DataColumn("Description", Type.GetType("System.String")))
        DT.Columns.Add(New DataColumn("IsError", Type.GetType("System.Boolean")))
        DT.Columns.Add(New DataColumn("IsWarning", Type.GetType("System.Boolean")))
        DT.Columns.Add(New DataColumn("IsImportant", Type.GetType("System.Boolean")))

        Dim E As DataRow = DT.NewRow()
        E("Description") = "Error"
        E("IsError") = True
        E("IsWarning") = False
        E("IsImportant") = True
        DT.Rows.Add(E)

        Dim W As DataRow = DT.NewRow()
        W("Description") = "Warning"
        W("IsError") = False
        W("IsWarning") = True
        W("IsImportant") = False
        DT.Rows.Add(W)

        Dim I As DataRow = DT.NewRow()
        I("Description") = "Info"
        I("IsError") = False
        I("IsWarning") = False
        I("IsImportant") = True
        DT.Rows.Add(I)

        ' This call is required by the designer.
        InitializeComponent()

        ' Add any initialization after the InitializeComponent() call.

    End Sub
End Class

If you run this project you will see a simple grid like this...

 
Now let's break things and see what happens...
 
Let's start by breaking the background converter. Comment out the code line in MainWindow that contains
E("IsImportant") = True
Now run the project again. Your BackgroundConverter will throw an exception because the value that WPF passes to it is DBNull which cannot be converted to a Boolean.

Now uncomment that line and instead comment out the line above it. This will break the ForegroundConverter which is a multi value converter. Now when you run the project the ForegroundConverter throws an exception because the type "NamedObject" cannot be converted to a Boolean.

Why does a DBNull get passed correctly to a ValueConverter but get changed to a NamedObject for a MultiValueConverter? I don't know.

Tuesday, April 1, 2014

Adding security to a menu

For WPF 4.0

Most applications need to be able to hide/show or disable/enable menu items based on the current user's security. Our applications all use a home-grown security provider that, after an eight-table join, returns a list of security codes that control the user's access to features. The security codes have simple names like "CreatePO" and "Requisition". I need to be able to only show menu items when the user has the correct security code.

At first I thought the solution would be like the asp.net security provider which allows the developer to write an event handler that is called prior to rendering each menu item. The event handler decides whether the menu item should be displayed or hidden. However the WPF menu class does not seem to have such an event defined.

After a little thought and some coffee, I realized I could bind each menu item's visibility to the user's security codes using a converter and pass the desired security code(s) through the converter parameter. This turns out to be a pretty good solution.

The user's security codes and their descriptions are held as a dictionary of string, string in an application property. Here is a converter that takes a comma delimited list of security codes. If the user has any of them, the menu item will be visible. Otherwise it will be collapsed.

Public Class SecurityConverter
    Implements IValueConverter

    Public Function Convert(value As Object, targetType As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.Convert
        Dim Codes As Dictionary(Of String, String) = Application.Current.Properties("User_SecurityCodes")
        For Each p As String In parameter.ToString.Split({","c}, StringSplitOptions.RemoveEmptyEntries)
            If Codes.ContainsKey(p) Then
                Return Visibility.Visible
            End If
        Next
        Return Visibility.Collapsed
    End Function

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

Then we create a static resource so we can reference the converter. In the window's opening tag we add a reference to our namespace

     xmlns:local="clr-namespace:<insert namespace here>"

and add a static resource to the Resources so we can reference our converter.

    <local:SecurityConverter x:Key="SecurityConverter"/>

Now we can bind the Visibility of selected menu items to the user's security codes like this...

    Visibility="{Binding Converter={StaticResource SecurityConverter}, ConverterParameter='Requisition'}"