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>
Comments
- Anonymous
January 16, 2014
www.kettic.com/.../listview.shtml