Handling list view selection in Avalonia FuncUI
20 Aug 2025After a brief summer hiatus, I am back! I wish this pause was due to exciting vacation plans, but unfortunately, the main reason was that I had a gas leak in my apartment, which ended up disrupting my routine quite a bit. Anyways, I am looking forward to enjoying simple pleasures of life like warm showers or home cooking again hopefully soon.
Today’s post is not anything fancy. I have been working on deskop applications
in F# recently, using Avalonia FuncUI, and getting the ListBox to do
what I wanted it to do was a bit more involved than I expected. This post is
intended mainly as notes to myself, documenting some of the details that
tripped me up.
Today’s post will focus on handling selection. I intend to have a follow-up post soon, covering dynamic updates. Until that is published, you can take a look at the full code example on GitHub.
The ListBox in Avalonia FuncUI
The ListBox in Avalonia is a control intended to display a collection of
items, and track which item is selected. The documentation gives a pretty
good description of its basic usage in FuncUI:
ListBox.create [
ListBox.dataItems [ "Linux"; "Mac"; "Windows" ]
ListBox.selectedItem state.os
ListBox.onSelectedItemChanged (fun os -> dispatch ChangeOs)
]
ListBox.dataItemsexpects a collection of Items to display, which would typically coming from theState,ListBox.onSelectedItemChangedtracks changes of selection,ListBox.selectedItemdrives which Item should visually appear as selected in the list.
I will focus only on single-item selection in this post. Multi-selection is also supported, but I haven’t dug into that very much, because this wasn’t something I needed. The use case I am after is very basic:
- Present a list of items to the user in a
ListBox, - Allow the user to edit the item currently selected,
- Highlight the item currently selected in the
ListBox.
As it turns out, this was less straightforward than I expected. Let’s dig into it!
Displaying the Selected Item
The first struggle I had was with handling item selection for “complex” items. In the documentation example mentioned previously, the items are simple strings. What if our items are more complex entities?
To simplify the question of identity, I decided to assign a unique ID, a Guid, to each item. In this example, we will work with the simplest possible Item, a record that looks like this:
type Item = {
Id: Guid
Name: string
}
Our State then has a collection of Items, and maintains which item is
selected, by tracking the corresponding ID as an Option<Guid>, so we can also
handle the situation where no item is selected:
type State = {
Items: Item []
SelectedItemId: Option<Guid>
}
We can then add a ListBox to the view, like so:
let view (state: State) dispatch =
ListBox.create [
ListBox.dataItems (state.Items)
ListBox.selectedItem (
match state.SelectedItemId with
| None -> null
| Some itemId ->
state.Items
|> Array.tryFind (fun item -> item.Id = itemId)
|> function
| None -> null
| Some item -> box item
)
]
The selectedItem part tripped me up quite a bit. The issue, as I understand
it, is that the ListBox is not generic, but operates on objects. As an
example, the signature of ListBox.selectedItem offers a hint of that:
static member selectedItem:
item: obj
-> IAttr<'t> (requires :> SelectingItemsControl)
As a result, we need to convert our SelectedItemId, an Option<Guid>, to
either a null if nothing is selected, or box the selected Item otherwise,
converting it to a System.Object.
Without boxing, the UI will only reflect the item selected by the user, by
clicking on items on screen. What boxing buys us is that if we change the
SelectedItemId on the State, via code, the change will be properly
reflected visually on the ListBox. As an example, in our init function, we
can pre-select the first item of the list on initialization, like so:
let init (): State * Cmd<Msg> =
let items =
Array.init 10 (fun i ->
{
Id = Guid.NewGuid()
Name = $"Item {i}"
}
)
{
Items = items
SelectedItemId = Some (items.[0].Id)
Filter = ""
},
Cmd.none
Changing the Selected Item
In a fashion similar to how we handle highlighting the selected item, we can
handle changes in selection sending the corresponding ID, an Option<Guid>, to
the State. We create a message for that purpose, like so:
type Msg =
| SelectedItemIdChanged of Option<Guid>
// | ... other messages
This allows us to signal that nothing is selected (None), or that some ID has
been selected (Some itemID). To signal that the selected item has changed, we
use ListBox.onSelectedItemChanged, which, as in the previous example, expects
an object:
static member onSelectedItemChanged:
func : (obj -> unit) *
?subPatchOptions: SubPatchOptions
-> IAttr<'t> (requires :> SelectingItemsControl))
I might have over-complicated things a little, but below is the code I ended up
with. We check if the selected object is indeed of type Item, and if so, if
its ID is different from the selected one. Otherwise, no message is needed:
let view (state: State) dispatch =
ListBox.create [
// same as before, omitted for brevity
ListBox.onSelectedItemChanged (
(fun selected ->
match selected with
| :? Item as selectedItem ->
match state.SelectedItemId with
| None ->
selectedItem.Id
|> Some
|> SelectedItemIdChanged
|> dispatch
| Some currentlySelectedId ->
if currentlySelectedId <> selectedItem.Id
then
selectedItem.Id
|> Some
|> SelectedItemIdChanged
|> dispatch
else ignore ()
| _ ->
None
|> SelectedItemIdChanged
|> dispatch
),
SubPatchOptions.Always
)
]
The SubPatchOptions.Always is possibly un-necessary, but after having been
bitten a couple of times with closures having unanticipated effects in events,
I have become a little paranoid!
Parting thoughts
Part of me is wondering what it would take to make a generic version of the
ListBox (or other Avalonia controls). In the end, the code I wrote does the
job, but it feels a bit more noisy than it should.
Anyways, that’s where I will leave things for today! I plan on a follow up
soon, going over dynamically updating a ListBox, to perform actions such as:
- Adding an Item to the ListBox,
- Deleting an Item from the ListBox,
- Changing the name of an Item in the ListBox,
- Dynamically filtering the contents of the ListBox.
I am also hoping to follow up with a TreeView example. There is just one
problem - I still haven’t gotten it to work the way I want it to :) If the
ListBox was a bit tricky, the TreeView feels like the boss fight of working
with collections within Avalonia FuncUI.
In the meantime, you can get a preview of what is coming next here, with the full ListBox code example on GitHub. Warning: this repository is my playground where I experiment with various controls and explore ideas, so it is a little messy, don’t judge too harshly!