Use LINQ with WPF : Styles and DataTemplates in code
Using Windows Presentation Foundation (WPF) and LINQ is easy in code. We'll create a query and display it in a WPF ListBox
Using XAML is harder: the ObjectDataProvider class works with non-anonymous types.
Start Visual Studio 2008 (or 2005 with Visual Studio 2005 extensions for .NET Framework 3.0. Linq is only in 2008)
Choose File->New Project->Visual Basic->WPF Application
You can use the WPF Forms designer, or you can write your code in a program.
Double click the form designer to bring up the Window1.xaml.vb file. Replace the contents with the code below.
Class Window1
Private Sub Window1_Loaded(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles MyBase.Loaded
Me.Width = 800
Me.Height = 800
Dim Query = From proc In System.Diagnostics.Process.GetProcesses _
Select proc.Id, proc.ProcessName, ThreadCount = proc.Threads.Count, proc.MainWindowTitle
Dim MyListBox As New ListBox
MyListBox.ItemsSource = Query
' lb.ItemsSource = New String() {"one", "two"} ' or a simple array
Me.Content = MyListBox
End Sub
End Class
Hit F5, and now you have a ListBox on a form which displays multiple rows like so:
{ Id = 2312, ProcessName = notepad, ThreadCount =1, MainWindowTitle = t.txt – Notepad, }
Let's make the display a little more friendly by using a DataTemplate.
Add this code before the End Sub:
Dim dt As New DataTemplate
Dim factSP = New FrameworkElementFactory(GetType(StackPanel))
dt.VisualTree = factSP
factSP.SetValue(StackPanel.OrientationProperty, Orientation.Horizontal)
Dim factTb = New FrameworkElementFactory(GetType(TextBlock))
factTb.SetBinding(TextBlock.TextProperty, New Binding("Id"))
factTb.SetValue(TextBlock.WidthProperty, 40.0)
factTb.SetValue(TextBlock.FontWeightProperty, FontWeights.Bold)
factSP.AppendChild(factTb)
factTb = New FrameworkElementFactory(GetType(TextBlock))
factTb.SetBinding(TextBlock.TextProperty, New Binding("ProcessName"))
factTb.SetValue(TextBlock.FontStyleProperty, FontStyles.Italic)
factTb.SetValue(TextBlock.WidthProperty, 80.0)
factSP.AppendChild(factTb)
factTb = New FrameworkElementFactory(GetType(TextBlock))
factTb.SetBinding(TextBlock.TextProperty, New Binding("ThreadCount"))
factTb.SetValue(TextBlock.WidthProperty, 30.0)
factSP.AppendChild(factTb)
factTb = New FrameworkElementFactory(GetType(TextBlock))
factTb.SetBinding(TextBlock.TextProperty, New Binding("MainWindowTitle"))
factSP.AppendChild(factTb)
MyListBox.ItemTemplate = dt
This code uses a Data Template to map data names with UI elements.
Because there are multiple rows, the template declaratively specifies how the data is presented.
In this case, the multiple rows of the query mapped to multiple ListBoxItems in the ListBox. Each ListBoxItem displays as a factory generated Horizontal StackPanel. The StackPanel contains a factory generated TextBlock for each field. WPF allows all sorts of controls inside other controls, so we could have a movie or even another listbox inside the listbox item.
Some of the TextBlocks have their width or FontStyle set. However, this code is hardcoded for the field names and data types of the query. Let's make it more generic. We can use refection to get the data types and field names of the anonymous type created by the query:
Dim QueryAnonType = _
Query.GetType().GetInterface(GetType(IEnumerable(Of )).FullName).GetGenericArguments()(0)
For Each mem In QueryAnonType.GetMembers
If mem.MemberType = Reflection.MemberTypes.Property Then
Select Case CType(mem, Reflection.PropertyInfo).PropertyType.Name
Case "Int32"
Dim factTb = New FrameworkElementFactory(GetType(TextBlock))
factTb.SetBinding(TextBlock.TextProperty, New Binding(mem.Name))
factTb.SetValue(TextBlock.WidthProperty, 40.0)
factTb.SetValue(TextBlock.FontWeightProperty, FontWeights.Bold)
factSP.AppendChild(factTb)
Case "String"
Dim factTb = New FrameworkElementFactory(GetType(TextBlock))
factTb.SetBinding(TextBlock.TextProperty, New Binding(mem.Name))
factTb.SetValue(TextBlock.WidthProperty, 130.0)
factTb.SetValue(TextBlock.FontStyleProperty, FontStyles.Italic)
factTb.SetValue(TextBlock.ForegroundProperty, Brushes.CornflowerBlue)
factSP.AppendChild(factTb)
End Select
End If
Next
Putting it all together, adding some style triggers for fun, the entire code sample is below.
Next time, we'll add headers that sort the columns when clicked and some
<Code Sample>
Class Window1
Private Sub Window1_Loaded(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles MyBase.Loaded
Me.Width = 800
Me.Height = 800
Dim Query = From proc In System.Diagnostics.Process.GetProcesses _
Select proc.Id, proc.ProcessName, ThreadCount = proc.Threads.Count, proc.MainWindowTitle
Dim MyListBox As New ListBox
MyListBox.ItemsSource = Query
' lb.ItemsSource = New String() {"one", "two"} ' or a simple array
Me.Content = MyListBox
Dim dt As New DataTemplate
Dim factSP = New FrameworkElementFactory(GetType(StackPanel))
dt.VisualTree = factSP
factSP.SetValue(StackPanel.OrientationProperty, Orientation.Horizontal)
Dim QueryAnonType = _
Query.GetType().GetInterface(GetType(IEnumerable(Of )).FullName).GetGenericArguments()(0)
For Each mem In QueryAnonType.GetMembers
If mem.MemberType = Reflection.MemberTypes.Property Then
Select Case CType(mem, Reflection.PropertyInfo).PropertyType.Name
Case "Int32"
Dim factTb = New FrameworkElementFactory(GetType(TextBlock))
factTb.SetBinding(TextBlock.TextProperty, New Binding(mem.Name))
factTb.SetValue(TextBlock.WidthProperty, 40.0)
factTb.SetValue(TextBlock.FontWeightProperty, FontWeights.Bold)
factSP.AppendChild(factTb)
Case "String"
Dim factTb = New FrameworkElementFactory(GetType(TextBlock))
factTb.SetBinding(TextBlock.TextProperty, New Binding(mem.Name))
factTb.SetValue(TextBlock.WidthProperty, 130.0)
factTb.SetValue(TextBlock.FontStyleProperty, FontStyles.Italic)
factTb.SetValue(TextBlock.ForegroundProperty, Brushes.CornflowerBlue)
factSP.AppendChild(factTb)
End Select
End If
Next
MyListBox.ItemTemplate = dt
Dim MyListboxStyle As New Style(GetType(ListBoxItem))
Dim tr = New Trigger
tr.Property = ListBoxItem.IsSelectedProperty
tr.Value = True
tr.Setters.Add(New Setter(ListBoxItem.ForegroundProperty, Brushes.Bisque))
MyListboxStyle.Triggers.Add(tr)
tr = New Trigger
tr.Property = ListBoxItem.IsMouseOverProperty
tr.Value = True
tr.Setters.Add(New Setter(ListBoxItem.ForegroundProperty, Brushes.Red))
tr = New Trigger
tr.Property = ListBoxItem.IsMouseOverProperty
tr.Value = True
tr.Setters.Add(New Setter(ListBoxItem.BackgroundProperty, Brushes.Aquamarine))
MyListboxStyle.Setters.Add(New Setter(ListBoxItem.ForegroundProperty, Brushes.Black))
MyListboxStyle.Triggers.Add(tr)
MyListBox.ItemContainerStyle = MyListboxStyle
End Sub
End Class
</Code Sample>