Elmish.WPF
Elmish.WPF copied to clipboard
How to make ListBox multiselect work with Elmish.WPF
I am trying to implement use of ListBox with SelectionMode="Extended". I have implemented something I would have thought should work, but there are obvious problems with it. Perhaps somebody can have a look. It's a very simple example.
The repo is here: https://github.com/BentTranberg/ExploreElmishWpf
After start, this particular example is in the Multiselect pane.
Some of the items in the listbox should be "checked" after start, but they're all "not checked". Furthermore, when clicking on an item, this exception is raised : System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
(edit: What I know, is that the binding of DisplayText won't work when the ListBox.ItemContainerStyle is present in the XAML below.)
The source is so short I think I'll post a compressed version here.
What I am trying to achieve is simply to bind ListBoxItem.IsSelected to Marked in the submodel.
<ListBox ItemsSource="{Binding Lines}" SelectionMode="Extended">
<ListBox.ItemTemplate>
<DataTemplate>
<Label Content="{Binding DisplayText}"/>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding Marked, Mode=OneWayToSource}"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
type Line = { Id: Guid; Marked: bool; DisplayText: string }
type Model = { Lines: Line list }
type Msg =
| SetMarked of Guid * bool
let init () =
{
Lines = [
{ Id = Guid.NewGuid(); Marked = false; DisplayText = "Zero" }
{ Id = Guid.NewGuid(); Marked = true; DisplayText = "One" }
{ Id = Guid.NewGuid(); Marked = false; DisplayText = "Two" }
{ Id = Guid.NewGuid(); Marked = true; DisplayText = "Three" }
]
}, Cmd.none
let update msg m =
match msg with
| SetMarked (id, marked) ->
let lines = m.Lines |> List.map (fun line ->
if line.Id = id then { line with Marked = marked } else line)
{ m with Lines = lines }, Cmd.none
let bindings () : Binding<Model, Msg> list = [
"Lines" |> Binding.subModelSeq((fun m -> m.Lines), (fun line -> line), fun () -> [
"Id" |> Binding.oneWay (fun (_, line) -> line.Id)
"Marked" |> Binding.twoWay ((fun (m, line) -> line.Marked),
(fun marked (m, line) -> SetMarked (line.Id, marked)))
"DisplayText" |> Binding.oneWay (fun (_, line) ->
line.DisplayText + (if line.Marked then " is checked" else " is not checked"))
])]
My next to last revision in that repo contains a version that works better, in a way. It doesn't crash. But the binding to the model ("Marker") in XAML doesn't work as long as the triggers are defined.
I have implemented something I would have thought should work, but there are obvious problems with it. Perhaps somebody can have a look. It's a very simple example.
I am not convinced that it is very simple. It seems very complicated to me. There seems to be many things included in those commits that are unrelated to multiselection. This is consistent with your README.md, which essentially says that this code is intended to do many things.
Can you provide a minimal working example (also known as a SSCCE)?
In particular, you have 11 broken bindings, and I am unsure if they are related to your multiselection problem.
Thanks for looking into this, and for the guidance. I have created a new repo that is more suitable for this kind of research. https://github.com/BentTranberg/ElmiDemos
The repo contains a VS solution with two projects (SSCCE I believe) that demonstrate the two ways in which I've tried to implement multiselect. The readme in the repo gives a brief overview, and there are also comments in the XAML that reveals more details.
Maybe I should have asked first if there are already samples somewhere that solves what I'm trying to do. But I have googled and researched this a lot without finding anything.
Just now found a stupid mistake that snuck into the source at some point. It wasn't there in my original research, so I'm slightly baffled at how the heck this happened. The function for getting id should of course be line -> line.Id and not just line -> line.
I'll get back here when I've checked whether this fixes things for me.
Indeed things started working now that I found that mistake. I now have three projects, of which two seems to work well.
The one that still doesn't quite work - MultiSel - had the smell of a hack anyway, and really isn't of interest even though I know approximately what's wrong.
The second - MultiSel2 - is the ideal one.
The third - MultiSel3 - uses dxmvvm:Interaction.Behaviors from DevExpress. It's a free library. I can't find any other library with Interaction.Behaviors that works with .NET Core at this time. (edit: This one also has a bad smell, since it goes through the checkbox to update the model from ListBoxItem.IsSelected.)
I'd like to suggest that MultiSel2 is used as basis for a demo in Elmish.WPF of how to do multiselect in ListBox, which I an reasonably sure will also cover ListView, possibly TreeView, maybe DataGrid, and ItemsControl I guess. Multiselect is such an important and central feature in GUIs that I think a demo like this should have a prominent place, even though in general you do not demo controls in the Elmish.WPF repo. In short, I suspect it's a big selling point when people look at the demos.
Maybe I should have asked first if there are already samples somewhere that solves what I'm trying to do. ... Multiselect is such an important and central feature in GUIs that I think a demo like this should have a prominent place, even though in general you do not demo controls in the Elmish.WPF repo.
We don't have a multiselect sample now, and I have never seen an Elmish.WPF program that does multiselect. I am in favor of adding such a sample.
Just now found a stupid mistake that snuck into the source at some point. ... Indeed things started working now that I found that mistake.
We all make "stupid" mistakes, and SSCCE help to locate them. Good job finding yours :)
I now have three projects, of which two seems to work well.
Excellent. I will take a look (eventually...not sure when).
The third - MultiSel3 - uses
dxmvvm:Interaction.Behaviorsfrom DevExpress. It's a free library. I can't find any other library withInteraction.Behaviorsthat works with .NET Core at this time.
Microsoft.Xaml.Behaviors.Wpf supports .NET Core. Does it also contain the features you want?
Microsoft.Xaml.Behaviors.Wpf supports .NET Core. Does it also contain the features you want?
It certainly looks that way from one of the examples in that repo. Googling lead me to another library from MS that wasn't .NET Core compatible, and I never saw this one. Thanks. I'll just put this potential change in my backlog for now. Btw that free DevExpress library in MultiSel3 comes from NuGets main feed, so there's no hassle with it if you open the solution. It's just one tiny DLL.
I don't have time to look thoroughly into this right now, but it seems that ListBox supports SelectedItems but not any kind of SelectedValues (and SelectedValuePath). So it is possible that this might be best solved by implementing a subModelSelectedItems binding similar to subModelSelectedItem.
Again, I haven't looked at your code @BentTranberg, I'm just mentioning this to get it out there.
FYI: The EventBindingsAndBehaviors sample uses Microsoft.Xaml.Behaviors.Wpf.
might be best solved by implementing a
subModelSelectedItemsbinding
I've been thinking the same for quite some time, and it would solve most or all of my use cases in a very elegant way. I will be using multiselect a lot, and could do away with lots of boilerplate in XAML and F# with that one.
But would this be the most elegant solution for the functionality in the MultiSel2 demo? I suspect not. In that demo the submodel has a binding directly to ListBoxItem.IsSelected, and so can instantly update other parts of the submodel view, or do other stuff behind the scenes without involving the parent model. I have a feeling that use of subModelSelectedItems would only complicate matters in this particular case.
I guess this is as it should be. I would be using subModelSelectedItems in most cases, but sometimes use the MultiSel2 way of doing it, in place of or in addition to using subModelSelectedItems.
But would this be the most elegant solution for the functionality in the MultiSel2 demo? I suspect not. In that demo the submodel has a binding directly to
ListBoxItem.IsSelected
Oh, I see. If that's already possible, why not always do that? Why would subModelSelectedItems ever be needed? Are there any cases where controlling each item separately is not the best solution?
I would say yes, in most cases. A typical use case of mine would be that I select items, and then click a button to have something done with the selected items. That's when subModelSelectedItems would come in very handy, and it corresponds to using SelectedItems rather than item.IsSelected in MVVM or Code Behind. The related logic would be very simple and entirely associated with the parent model, apart from coordinating the type of the Id with the submodel. In XAML there would only be the binding to ListBox.SelectedItems. In the parent model I would typically have SelectedItems: Guid list, and a message for subModelSelectedItems to update SelectedItems.
Compare this to a solution borrowing from the MultiSel2 demo. A ListBox.ItemContainerStyle of five lines is used in XAML, and a field corresponding to ListBoxItem.IsSelected is kept in the submodel. A binding to that field is used in the submodel bindings, and a message is used by that binding to update the submodel. In other words, the submodel is now burdened with doing view level logic for the parent model that has nothing to do with business. That doesn't smell right to me.
In the MultiSel2 demo the source is justified as is because there the business of the submodel is to use IsSelected to keep the checkbox and DisplayText updated.
Admittedly the current solution to getting selected items isn't much more source than would be if a subModelSelectedItems existed, and it's not really a serious practical problem for me. However I do feel that something is missing from Elmish.WPF without subModelSelectedItems, and I'm slightly worried by that.
I have used the pattern from MultiSel2 demo also with ListView now, and it works, which of course is no surprise. I am guessing that DataGrid and ItemsControl follow the same pattern, but I don't know about TreeView. Won't look at it until I need it.
I don't really see the problem. It seems to me to be just as easy to set IsSelected on a ListBoxItem (which is also done once in XAML) as it is to set SelectedItems on the ListBox, and with the subModelSeq overloads there is no problem having access to the list of selected items from the "parent model" in the "child models".
The child model can dispatch the same message type as the parent, and in many cases should, because using "Elm components" with their own MVU triplets is often an inefficient, cumbersome, verbose, and painful way to scale Elm applications. (This might not transfer completely to Elmish.WPF due to static views, but I think it does, and at least that it transfers well enough that the point holds.)
Also, using a selected prop on individual elements is AFAIK the normal way to control selections in Elm.
Personally I don't like the existing subModelSelectedItem. It's the most hacky and least type-safe binding (can throw at runtime), and was only implemented because some controls did not seem to support SelectedValue and SelectedValuePath (#77). At the time, I didn't think of the child items having an IsSelected property that could be used. So it might be that subModelSelectedItem is actually unnecessary. /cc @bender2k14
Update: To be clear, the below is what I think is so simple that I'm not sure of the value of subModelSelectedItem considering its drawbacks:
<ListView>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
</ListView>
I tried this with the current SubModelSelectedItem sample, and it's just as simple as when using Binding.subModelSelectedItem.
I'll follow your lead, @cmeeren.
I'm actually struggling this very minute with using both subModelSeq and subModelSelectedItem with dxg:GridControl (DevExpress) for the very first time, and for some reason it's extremely slow loading a great number of items compared to the bad implementation of mine from the Elmish.WPF v2 era. Don't know if that is to be expected. Anyway, I now intend to use exactly your snippet above and see if it improves matters. (edit: That control doesn't have SelectedValue or SelectedValuePath.)
Note: ListView and ListBox/"ListBoxItem" is mixed in that snippet.
Note: ListView and ListBox/"ListBoxItem" is mixed in that snippet.
It's not incorrect since ListBoxItem is the base class for ListViewItem and similar for ListView/ListBox, though it may have been better to keep them consistent. Wouldn't have changed any functionality, though.
Personally I don't like the existing
subModelSelectedItem. It's the most hacky and least type-safe binding (can throw at runtime), and was only implemented because some controls did not seem to supportSelectedValueandSelectedValuePath(#77). At the time, I didn't think of the child items having anIsSelectedproperty that could be used. So it might be thatsubModelSelectedItemis actually unnecessary.
I don't like the existing subModelSelectedItem either. I think we can make it safer (such as by defining both bindings via a single function call), but this binding type is still making it very difficult for me to implement recursive binding behavior. It has been on my TODO list to create an issue where I initially "complain" about subModelSelectedItem and then try to (constructively) brainstorm improvements. I am glad to see that this issue seems to be headed in that direction.
I do feel that something is missing from Elmish.WPF without
subModelSelectedItems, and I'm slightly worried by that.
I don't think you have to worry. We want Elmish.WPF to have "good" solutions to every WPF binding problem. If we can't think of something better than subModelSelectedItems, then we will add this new binding type.
Are there any cases where controlling each item separately is not the best solution?
The worst thing I can think of is when multiselect is possible and all items are selected at once. For n items, this will cause n messages to be dispatched. If a control had a SelectedValues dependency property, then I would guess that the same situation would only result in one dispatched message. I think that this is a significant difference.
Update: To be clear, the below is what I think is so simple that I'm not sure of the value of
subModelSelectedItemconsidering its drawbacks:
When SelectionMode is Single, the downsides I see are:
- Five lines of XAML (https://github.com/elmish/Elmish.WPF/issues/188#issuecomment-558503179) instead of a single dependency property;
- Linear space (one Boolean per child) instead of constant space (one selected field in the parent);
- Possible to represent illegal state by having more than one Boolean set to
true; - Additional F# code to obtain the selected item from the list of all items.
I don't think that these downsides are very serious, but they are annoying enough that I would prefer if we could keep subModelSelectedItem while also removing its downsides.
...and for some reason it's extremely slow loading a great number of items compared to the bad implementation of mine from the Elmish.WPF v2 era. Don't know if that is to be expected.
I don't know much about the v2 era. However, subModelSeq is currently slower than it should be (c.f. #137). I hope to make progress on that issue soon.
Wops! the SelectedItems issue hit back at me this evening. It turns out that the DevExpress GridControl (and probably its siblings too) only has SelectedItems for multiselection, and no way to handle item level selection. DevExpress actually state this in a support issue, and there are other support issues pointing people in that direction following attempts to find triggers and stuff. There are events on the top level, but the arguments coming through there are the internal view model types of Elmish.WPF (as a result of using subModelSeq), so I believe that route is blocked too. By the way, it also only has SelectedItem for single selection.
That was a huge disappointment for me. As far as I can see this means I depend on SelectedItem and SelectedItems for using the really interesting controls of the DevExpress WPF library to the full. I do like that library, and for that reason hope there will be a SelectedItems in Elmish.WPF at some point not too far into the future. Luckily right now I only strictly need single selection for the advanced grid cases in my apps (or alternatively checkboxes), and I can use the lesser advanced standard WPF controls in cases where I need efficient multiselect, because those are simpler lists. So I'm doing ok for now.
Briefly off-topic: I'm amazed at how much more efficient, more stable, far simpler and smaller my source is after refactoring my most complicated older models+views in the last few days, taking advantage of version 3 and the docs and the tutorial. What I see now is that this makes it reasonably easy for me to create considerably more advanced user interfaces than I have originally planned for. Thanks!
It turns out that the DevExpress
GridControl(and probably its siblings too) only hasSelectedItemsfor multiselection, and no way to handle item level selection. DevExpress actually state this in a support issue...
Can you link to that support issue?
The support issue : GridControl: bind the row property IsSelected to a viewmodel property.
There are several support issues there dealing with the problems I faced while trying to find my way to a solution, but I didn't keep the links. Easy enough to find by googling again, I thought.
Trying to summarize the discussion so far: It seems we need some kind of subModelSelectedItems (plural) to make this work. Is that correct?
Yes.
I want to contribute slightly in this and similar discussions, which interest me a great deal, but I have been laid off for an undetermined duration during the corona crisis. I cannot engage in activity related to my work in this period, for legal reasons. To be on the safe side, which I absolutely need to be, I also should refrain from participating in this and certain other forums until I'm back at work. Look forward to joining your discussions again sometime in the future, not too long.
EDIT: I returned to work in 2021.