Thursday, April 21, 2022

Access a dictionary's keyvaluepair by position

I have some generic code that takes a dictionary with unknown generic types and returns all KeyValuePairs with keys that match a specific condition. The problem I have is that I cannot iterate through the collection of KeyValuePairs because that would require code like...

For Each o As KeyValuePair(Of T1, T2) In IDict

but I don't know T1 or T2 at design time. Similarly the following lines throw invalid cast errors.

For Each o As KeyValuePair(Of Object, Object) In IDict

For Each o As KeyValuePair(Of Object, Object) In IDict.Cast(Of KeyValuePair(Of Object, Object))

There is the option to iterate over a DictionaryEntry collection which does not require me to know the generic types at design time

For Each o As DictionaryEntry In IDict

Now I can examine the keys and values but I can't find a way to convert the DictionaryEntry back to the equivalent KeyValuePair. But I can track the index of the current DictionaryEntry and use IDict.AsQueryable(index) to get the matching KeyValuePair.

Here's an example in Visual Basic and WPF. Just click the [Find Y] button.

Start a Visual Basic WPF application using .Net Framework.

<Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:GetKeyValuePair"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <RoutedCommand x:Key="FindYCommand"/>
    </Window.Resources>
    <Window.CommandBindings>
        <CommandBinding Command="{StaticResource FindYCommand}" Executed="FindY_Executed"/>
    </Window.CommandBindings>
    <StackPanel Orientation="Horizontal" Height="22">
        <ComboBox ItemsSource="{Binding TheDictionary}"
                  SelectedItem="{Binding TheSelection}"
                  DisplayMemberPath="Key" Width="100"/>
        <Button Content="Find Y" Command="{StaticResource FindYCommand}"/>
    </StackPanel>
</Window>

-------------------------------------------------------------------------------

Imports System.ComponentModel
Imports System.Runtime.CompilerServices
 
Class MainWindow
    Implements INotifyPropertyChanged
 
    Private _TheDictionary As New Dictionary(Of String, Boolean?) From {{"MAYBE", Nothing}, {"YES", True}, {"NO", False}}
    Public ReadOnly Property TheDictionary
        Get
            Return _TheDictionary
        End Get
    End Property
 
    Private _TheSelection As KeyValuePair(Of String, Boolean?)
 
    Public Property TheSelection As KeyValuePair(Of String, Boolean?)
        Get
            Return _TheSelection
        End Get
        Set(value As KeyValuePair(Of String, Boolean?))
            SetProperty(_TheSelection, value)
        End Set
    End Property
 
    Private Sub FindY_Executed(sender As Object, e As ExecutedRoutedEventArgs)
        TheSelection = FindY(TheDictionary)
    End Sub
 
    Private Function FindY(IDict As IDictionary) As Object
        ' Let's assume that we don't know TheDictionary generic types.
        ' We can't iterate through the KeyValuePairs because requires us to know the types at design time
        ' Find the first dictionary entry with a key that contains Y.
        ' AsQueryable returns a list of KeyValuePairs
 
        Dim index As Integer = 0
 
        If IDict.Count = 0 Then Return Nothing
        For Each o As DictionaryEntry In IDict
            If o.Key.ToString.Contains("Y") Then
                Return IDict.AsQueryable(index)
            End If
            index += 1
        Next
        Return Nothing
    End Function
 
    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
    Public Function SetProperty(Of T)(ByRef storage As T, value As T, <CallerMemberName> Optional PropertyName As String = "") As Boolean
        If Object.Equals(storage, value) Then Return False
        storage = value
        NotifyPropertyChanged(PropertyName)
        Return True
    End Function
    Public Sub NotifyPropertyChanged(PropertyName As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(PropertyName))
    End Sub
End Class