I’m trying to write an automated test harness using WPF and F#. I’d like to display aggregate test results as rows in a multi-column list. Ideally, I would like to present rich content in each cell, e.g. highlighting issues with specific tests using color or making details available via tooltips. However, I cannot figure out how to put anything richer than a plain string into a multi-column ListView.
Furthermore, I’m not a fan of XAML or data binding and prefer to write vanilla F# code. The simplest program I have been able to write that displays a multi-column WPF ListView is:
open System.Windows
let gridView = Controls.GridView()
let listView = Controls.ListView(View=gridView)
type SystemParams = { Name: string; Value: obj }
[<System.STAThread>]
do
let column header binding =
let binding = Data.Binding binding
Controls.GridViewColumn(Header=header, DisplayMemberBinding=binding)
for header, binding in ["Name", "Name"; "Value", "Value"] do
column header binding
|> gridView.Columns.Add
for prop in typeof<SystemParameters>.GetProperties() do
if prop.PropertyType <> typeof<ResourceKey> then
{ Name = prop.Name; Value = prop.GetValue(null, null) }
|> listView.Items.Add
|> ignore
Application().Run(Window(Content=listView)) |> ignore
Although this works, I don’t like the way it requires the field names to be duplicated both in the type definition and as strings that are fed to WPF which presumably then uses reflection to resolve them at run-time (yuk!). Ideally, I would like to Add an obj array giving the WPF controls for each cell.
Is ListView capable of this? If so, how do you write a function that accepts a 2D array of controls and returns a ListView that visualizes them?
If not, I will probably use a Grid instead. I have tried DataGrid before and it is just a world of pain in comparison…
EDIT:
Thanks to the answers below, I have been able to come up with a solution. The multiColumnList function in the program below creates list of controls with the given headers and content with selectable rows:
open System.Windows
let multiColumnList columns contents onSelection =
let gridView = Controls.GridView()
let list = Controls.ListView(View=gridView)
let column index header =
let binding = Data.Binding(sprintf "[%d]" index)
Controls.GridViewColumn(Header=header, DisplayMemberBinding=binding)
|> gridView.Columns.Add
Seq.iteri column columns
list.ItemsSource <-
[|for row in contents ->
[|for elt in row ->
box elt|]|]
list.SelectionChanged.Add onSelection
list
[<System.STAThread>]
do
let columns = ["Name"; "Value"]
let contents =
[ for prop in typeof<SystemParameters>.GetProperties() do
if prop.PropertyType <> typeof<ResourceKey> then
yield [box prop.Name; prop.GetValue(null, null)] ]
Application().Run(Window(Content=multiColumnList columns contents ignore))
|> ignore
Your specific problem of duplicating the field names could be avoided by using an index based binding, instead of member name. See the changes to your example here:
Regarding giving the
ListViewa sequence of sequences of controls, that is somewhat lower level thanListViewis intended to be used.ListViewandDataGridboth assume that you have some roughly homogeneous collection of objects that you want show (generally as rows) and some idea of what information you want to show about those objects (the column definitions). Both controls will help in that situation, although I do agree that their general assumption that you want to use reflection over the members of a type can be annoying.If you want to be able to specify a grid of any controls, then as you mention the
Gridpanel layout is probably more suitable.