Embed a ListView inside another ListView for one to many relationships

In the old days (about 16 years ago), FoxPro’s object, containership and inheritance models made it easy to put objects inside other objects.

So, for example, a FoxPro grid of Customers could have a column containing a grid of each customer’s orders, and each order, in turn, could have a grid of the order detail items.

Windows Presentation Foundation (WPF) has a similar capability.  This sample will put an ItemsControl (such as a TreeView, Menu, ListView) inside a column of a ListView.

The sample includes the Browse class from this post:  https://blogs.msdn.com/calvin_hsia/archive/2007/12/06/6684376.aspx  which I’ve included for completeness below.

The data used is obtained from the assembly that contains the type “Integer”, which happens to be MSCorlib.DLL.

The code uses all the types inside MSCorLib and shows, their Events, Properties, and Methods as columns in a Browse.

The Browse class can handle simple types, such as Integers and Strings, but ignores other types, such as a Collection of Methods.

MakeNewColumn and myValueConverter add a new column, binding it to a type in the Linq query from the Browse.

A column must be able to generate lots of containers for the data, so it uses a FrameworkElementFactory to generate a template .

The ValueConverter gets called to convert a Collection(Of Methods) to an IEnumerable

Start Visual Studio 2010 File->New Project->VB WPF Application.

Edit the file MainWindow.Xaml.vb. Replace the contents with the code below. Hit F5

<Code Sample>

Imports System.Reflection

Class MainWindow

    Private Sub MainWindow_Loaded(ByVal sender As Object, ByVal e As RoutedEventArgs) Handles Me.Loaded

        Me.WindowState = Windows.WindowState.Maximized

        Dim asm = Assembly.GetAssembly(GetType(Integer)) ' get assembly containing Integer (MSCorLib)

        Me.Title = asm.FullName

        Dim q = From a In asm.GetExportedTypes

                Select

                TypeName = a.FullName,

                [Event] = a.GetMembers.Where(Function(obj) obj.MemberType = MemberTypes.Event),

                [Property] = From mem In a.GetMembers Where mem.MemberType = MemberTypes.Property,

                Field = From mem In a.GetMembers Where mem.MemberType = MemberTypes.Field,

                NestedType = From mem In a.GetMembers Where mem.MemberType = MemberTypes.NestedType,

                Method = a.GetMembers.Where(Function(obj) obj.MemberType = MemberTypes.Method)

       Order By TypeName

        Dim br = New Browse(q)

        With CType(br.View, GridView)

            .Columns.Add(MakeNewColumn(Of ListView)("Method"))

            .Columns.Add(MakeNewColumn(Of TreeView)("Property"))

            .Columns.Add(MakeNewColumn(Of ListBox)("Field"))

            .Columns.Add(MakeNewColumn(Of ListView)("Event"))

            .Columns.Add(MakeNewColumn(Of Menu)("NestedType"))

        End With

        Me.Content = br

    End Sub

    'constrain the generic type to an ItemsControl

    Public Function MakeNewColumn(Of T As ItemsControl)(ByVal colName As String) As GridViewColumn

        Dim elementFactory As New FrameworkElementFactory(GetType(T))

        Dim binder As New Binding(colName) With {

            .Mode = BindingMode.OneTime

        }

        binder.Converter = New myConverter(colName)

        elementFactory.SetBinding(ItemsControl.ItemsSourceProperty, binder)

        'Set max height for row, so 300 members don't get too unwieldy

        elementFactory.SetValue(ItemsControl.MaxHeightProperty, 100.0) ' float

        elementFactory.SetValue(ItemsControl.WidthProperty, 300.0) ' float

        Select Case GetType(T)

            Case GetType(ListView)

                elementFactory.SetValue(ItemsControl.BackgroundProperty, Brushes.LightCoral)

                Dim innerBinding = New Binding(colName) With {

                    .Mode = BindingMode.OneTime,

                    .Converter = New myConverter(colName)

                }

                elementFactory.SetBinding(ItemsControl.ItemTemplateProperty, innerBinding)

                elementFactory.SetBinding(

                    ItemsControl.ItemsSourceProperty,

                    New Binding(colName) With {

                        .Mode = BindingMode.OneTime,

                        .Converter = Nothing

                    }

                )

            Case GetType(TreeView)

                elementFactory.SetValue(ItemsControl.FontFamilyProperty, New FontFamily("Courier New"))

                elementFactory.SetValue(ItemsControl.BackgroundProperty, Brushes.AliceBlue)

                elementFactory.SetValue(ItemsControl.FontSizeProperty, 8.0)

            Case Else

        End Select

        Dim datTemplate = New DataTemplate With

            {

                .VisualTree = elementFactory

            }

        Dim newcol = New GridViewColumn With {

            .Header = colName,

            .CellTemplate = datTemplate

        }

      Return newcol

    End Function

End Class

Public Class myConverter

    Implements IValueConverter

    Private _colName As String

    Public Sub New(ByVal colName As String)

        _colName = colName

    End Sub

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

        Dim q As Object = Nothing

        Select Case targetType

            Case GetType(IEnumerable)

  Dim r = CType(value, IEnumerable)

                Select Case _colName

                    Case "Method"

                        q = From a In r

                                Select

                                MethodName = a.Name,

     ModName = a.Module.Name,

                                MemType = a.MemberType.ToString,

                                FullName = a

                    Case "Property"

                        q = From a In r

                  Select

                                PropertyName = a.Name,

                                ModName = a.Module.Name,

                                MemType = a.MemberType.ToString,

                                FullName = a

             Case "Field"

                        q = From a In r

                                Select

                                FieldName = a.Name,

                                ModName = a.Module.Name,

                                MemType = a.MemberType.ToString,

                                FullName = a

                    Case Else

                        q = From a In r

                                Select

                                a.Name,

                             ModName = a.Module.Name,

                                MemType = a.MemberType.ToString,

                                FullName = a

                End Select

            Case GetType(DataTemplate)

                Dim XAMLdt = _

<DataTemplate

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    >

    <StackPanel Orientation="Vertical">

        <TextBlock

            Name=<%= _colName %>

            Text=<%= "{Binding}" %>

            Background="Yellow"

            >

        </TextBlock>

    </StackPanel>

</DataTemplate>

                Dim dt = CType(System.Windows.Markup.XamlReader.Load(XAMLdt.CreateReader), DataTemplate)

                q = dt

            Case Else

                Throw New NotImplementedException(targetType.ToString)

        End Select

        Return q

    End Function

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

        Throw New NotImplementedException

    End Function

End Class

' see https://blogs.msdn.com/calvin\_hsia/archive/2007/12/06/6684376.aspx

Class Browse

    Inherits ListView

    Sub New(ByVal Query As Object, Optional ByVal Parent As Object = Nothing)

        Dim gv As New GridView

        Me.View = gv

        Me.ItemsSource = Query

        If Not Parent Is Nothing Then

            If Parent.GetType.BaseType Is GetType(Window) Then

                CType(Parent, Window).Title = "# items = " + Me.Items.Count.ToString

            End If

        End If

        Me.AddHandler(GridViewColumnHeader.ClickEvent, New RoutedEventHandler(AddressOf HandleHeaderClick))

        If Query.GetType.GetInterface(GetType(IEnumerable(Of )).FullName).GetGenericArguments(0).Name = "XElement" Then ' It's XML

            Dim Elem1 = CType(Query, IEnumerable(Of XElement))(0).Elements ' Thanks Avner!

            For Each Item In Elem1

                Dim gvc As New GridViewColumn

              gvc.Header = Item.Name.LocalName

                gv.Columns.Add(gvc)

                Dim bind As New Binding("Element[" + Item.Name.LocalName + "].Value")

                gvc.DisplayMemberBinding = bind

                gvc.Width = 180

            Next

  Else ' it's some anonymous type like "VB$AnonymousType_1`3". Let's use reflection to get the column names

            For Each mem In From mbr In _

            Query.GetType().GetInterface(GetType(IEnumerable(Of )).FullName) _

            .GetGenericArguments()(0).GetMembers _

            Where mbr.MemberType = Reflection.MemberTypes.Property

                Dim datatype = CType(mem, Reflection.PropertyInfo)

                Dim coltype = datatype.PropertyType.Name

                Select Case coltype

  Case "Int32", "String", "Int64"

                        Dim gvc As New GridViewColumn

                        gvc.Header = mem.Name

                        gv.Columns.Add(gvc)

                        If coltype <> "String" Then

         gvc.Width = 80

                            Dim XAMLdt = _

                            <DataTemplate

                                xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

                                xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

                                >

                                <StackPanel Orientation="Horizontal">

                                    <TextBlock Name="tb"

                                        Text=<%= "{Binding Path=" + mem.Name + "}" %>

                                        Foreground="Black"

                                        FontWeight="Bold"

                                        Background="SpringGreen">

                                 </TextBlock>

                                </StackPanel>

                            </DataTemplate>

                            gvc.CellTemplate = System.Windows.Markup.XamlReader.Load(XAMLdt.CreateReader)

                        Else

               gvc.DisplayMemberBinding = New Binding(mem.Name)

                            gvc.Width = 180

                        End If

                End Select

            Next

        End If

        Dim XAMLlbStyle = _

        <Style

            xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

            xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

            TargetType="ListBoxItem">

            <Setter Property="Foreground" Value="Blue"/>

            <Style.Triggers>

    <Trigger Property="IsSelected" Value="True">

                    <Setter Property="Foreground" Value="White"/>

                    <Setter Property="Background" Value="Aquamarine"/>

                </Trigger>

                <Trigger Property="IsMouseOver" Value="True">

                    <Setter Property="Foreground" Value="Red"/>

                </Trigger>

            </Style.Triggers>

        </Style>

        Me.ItemContainerStyle = Windows.Markup.XamlReader.Load(XAMLlbStyle.CreateReader)

  End Sub

    Dim _Lastdir As System.ComponentModel.ListSortDirection = ComponentModel.ListSortDirection.Ascending

    Dim _LastHeaderClicked As GridViewColumnHeader = Nothing

    Sub HandleHeaderClick(ByVal sender As Object, ByVal e As RoutedEventArgs)

  If e.OriginalSource.GetType Is GetType(GridViewColumnHeader) Then

            Dim gvh = CType(e.OriginalSource, GridViewColumnHeader)

            Dim dir As System.ComponentModel.ListSortDirection = ComponentModel.ListSortDirection.Ascending

        If Not gvh Is Nothing AndAlso Not gvh.Column Is Nothing Then

                Dim hdr = gvh.Column.Header

                If gvh Is _LastHeaderClicked Then

                    If _Lastdir = ComponentModel.ListSortDirection.Ascending Then

               dir = ComponentModel.ListSortDirection.Descending

                    End If

                End If

                Sort(hdr, dir)

                _LastHeaderClicked = gvh

                _Lastdir = dir

            End If

        End If

    End Sub

    Sub Sort(ByVal sortby As String, ByVal dir As System.ComponentModel.ListSortDirection)

        Me.Items.SortDescriptions.Clear()

        Dim sd = New System.ComponentModel.SortDescription(sortby, dir)

        Me.Items.SortDescriptions.Add(sd)

     Me.Items.Refresh()

    End Sub

End Class

</Code Sample>