Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SubModelSelectItem] - Cascading Listboxes : how to do it in an ItemsControl? #618

Closed
YkTru opened this issue Nov 6, 2024 · 24 comments
Closed

Comments

@YkTru
Copy link

YkTru commented Nov 6, 2024

Here's a working example of hardcoded cascading ListBoxes using subModelSelectedItem:

Video_2024-11-06_030927.mp4

GitHub Source: https://github.com/YkTru/Cascading-ListBoxes---SubModelSelectedItem
(Some code was quickly assembled and includes GPT-generated segments, but it seems to work as expected.)


Problem: The ListBoxes are currently hardcoded (levels 0-1-2) to demonstrate the intended behavior. However, I need a dynamic, n-depth tree structure.

Goal: I’m aiming for an ItemsControl that can dynamically create ListBoxes based on selected items or added children, supporting an n-level depth.

What I Tried: I experimented a lot with recursive bindings and updates, but none of the attempts were effective enough to showcase here.

I can achieve the intended behavior at level 0, but selecting any child results in the following error:

2024-11-05 23:34:14 [Error] SubModelSelectedItem binding referenced binding "" but no binding was found with that name
2024-11-05 23:34:14 [Error] ["main"] Get FAILED: Binding "Some(SelectedNode)" could not be constructed

Help needed: Please feel free to add/replace/implement/adjust the code to complete the ItemsControl (and corresponding App + ViewModels files), which I believe represents the intended functionality:

        <ScrollViewer
            Grid.Column="1"
            Margin="10">
                <ItemsControl
                    d:ItemsSource="{Binding ?}">
                    <ItemsControl.ItemTemplate>
                        <HierarchicalDataTemplate
                            d:DataType="{x:Type vm:?}"
                            d:ItemsSource="{Binding ?}">
                            <GroupBox
                                Margin="5"
                                Header="Level">
                                <ListBox
                                    d:ItemsSource="{Binding ?}"
                                    d:SelectedItem="{Binding ?}"
                                    DisplayMemberPath="Name" />
                            </GroupBox>
                        </HierarchicalDataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>
        </ScrollViewer>

Community:
@awaynemd I know you’ve asked many similar questions before—have you ever found a fully satisfying way to use subModelSelectedItem recursively?

@xperiandri Do you happen to have any helpers in elmish.UNO that could assist with tasks like this?

@TysonMN (if you have time 😉), I believe I’ve read everything you’ve written about these "issues" in the past concerning recursive uses of subModelSelectedItem, but I’m still quite confused. Is there an idiomatic way to use them within an ItemsControl as in my example?

@marner2 In your static bindings API revision, is it part of your plan to make a simple helper similar to subModelSelectedItem, but with static typing and easy optional recursion?

Thank you all so much—I’ve been trying different approaches for the past two weeks, all ending in humbling failures😓

@xperiandri
Copy link

Nope.
I've just implemented a similar logic using a BreadcrumbBar backed by submodelSeq
And that logic looks extremely ugly. I'm going to rewrite it one day.
I see the best option is (anonymous types used for readability)

type Model = {
  Items : Item0 list
  SelectionModel : {|
    SelectedItem : Item0
    Items: Item1 list
    SelectionModel : {|
      SelectedItem : Item1
      Items: Item2 list
      SelectionModel : Item2 voption
    |} voption
  |} voption
}
with
  interface IModel with
  interface IReadonlyList<IModel> with
  interface IEnumerable<IModel> with
    member model.GetEnumerator() =
      (seq {
          yield model :> IModel
          yield! model.SelectionModel |> ValueOption.toList
          yield! model.SelectionModel |> ValueOption.map _.SelectionModel |> ValueOption.toList
      }).GetEnumerator()

@YkTru
Copy link
Author

YkTru commented Nov 7, 2024

I feel you..

Ramblings: as I barely understand what is the main issue with recursive subModelSelectedItem, my intuition is that all level should have a specific, unique subModelSelectedItem binding, as I did in the hardcoded example:

    let selectedLevel0Binding =
        Binding.subModelSelectedItem (
            "Level0Items_VM",
            (fun m -> m.SelectedLevel0),
            (fun selectedId model -> SelectLevel0 selectedId)
        )

    let selectedLevel1Binding =
        Binding.subModelSelectedItem (
            "Level1Items_VM",
            (fun m -> m.SelectedLevel1),
            (fun selectedId model -> SelectLevel1 selectedId)
        )

    let selectedLevel2Binding =
        Binding.subModelSelectedItem (
            "Level2Items_VM",
            (fun m -> m.SelectedLevel2),
            (fun selectedId model -> SelectLevel2 selectedId)
        )

Correspondingly, all items should have a "Selected option" (and/or "SelectedLevel"?) property (I'm not sure about this though..)


(I realize this might seem a bit out there, but I’m running out of options, and I lack proper understanding of ElmishWPF source code)

Proposition: To create a dynamic tree that allows adding and removing siblings or children, could it be possible to assign each newly added child at any level a unique subModelSelectedItem binding + SelectedLevel prop? This is what I had to do in the hardcoded example, so I’m wondering if it would be feasible and performant enough in practice?

Issues: From my understanding of Elmish.WPF's limitations, it seems we can't create bindings dynamically. However, is there truly no way to achieve similar functionality? Is it currently impossible to build a tree where selecting any item at any level would update properties in a synchronized property editor? ..I'm quite worried right now..

( @marner2 if you have time) Am I speaking non-sense?

@xperiandri
Copy link

Submodels can be created dynamically in submodels seq

@xperiandri
Copy link

module Bindings =

    let private viewModel = Unchecked.defaultof<SelectLocationViewModel>

    let pathBinding =

        let createViewModel (args : ViewModelArgs<obj, obj>) : IViewModel<obj, obj> =
            let modelType = args.InitialModel.GetType ()
            if modelType = typeof<Floor.Model> then
                Floor.FloorItemViewModel args
            elif modelType = typeof<Area.Model> then
                Area.AreaItemViewModel args
            elif modelType = typeof<Room.Model> then
                Room.RoomItemViewModel args
            else
                failwithf "Unknown model type: %A" modelType

        let mapVmMsg (index, msg : obj) : Msg =
            match msg with
            | :? Floor.Msg as m -> FloorMsg (index, m)
            | :? Area.Msg as m -> AreaMsg (index, m)
            | :? Room.Msg as m -> RoomMsg (index, m)
            | _ -> failwithf "Unknown message type: %A" msg

        BindingT.subModelSeq createViewModel (nameof viewModel.Path)
        |> Binding.mapModel (fun m -> m.Path)
        |> Binding.addLazy (fun m1 m2 -> m1.Path = m2.Path)
        |> Binding.mapMsg (fun (i, msg) -> mapVmMsg (i, msg))

type SelectLocationViewModel (args) =
    inherit ViewModelBase<Model, Msg> (args)

    member _.Path = base.Get (Bindings.pathBinding)

@YkTru
Copy link
Author

YkTru commented Nov 8, 2024

I think I understand the bulk of it;

  • one question though: how can you access the type SelectLocationViewModel in let private viewModel = Unchecked.defaultof<SelectLocationViewModel> since its defined after?

  • Everytime I tried to access args, I couldn't get anywhere;
    • ie I get no Intellisense (except GetType & ToString; I tried open Elmish.WPF.ViewModelArgs/ViewModel to no avail
    • and I get errors such as: The type 'ViewModelArgs<_,_>' does not define the field, constructor or member 'InitialModel'


Regarding my specific case

build a tree where selecting any item at any level would update properties in a synchronized property editor

  • How would you approach writing the "SelectedItem" functionality? I'm having difficulty figuring out how to apply recursion in this case, as all my attempts so far haven't been successful, and I don't have a clue on how to debug it.

  • Regarding the AFAIC "catastrophic scenario": Do you think it might be unfeasible with Elmish.WPF? I've gone through numerous issues, discussions, and topics highlighting the challenges of using subModelSelectedItem recursively, and, honestly, I'm left with the impression that this issue remains unresolved—at least as far as I understand it.

If I'm (hopefully) mistaken, I believe a sample for this scenario would be incredibly valuable, as tree structures combined with property editors are quite common in complex desktop applications. Honestly, I really hope I've simply missed or misunderstood the solutions suggested in the various discussions.. otherwise I'll have to give up my whole project..

@xperiandri
Copy link

  • one question though: how can you access the type SelectLocationViewModel in let private viewModel = Unchecked.defaultof<SelectLocationViewModel> since its defined after?

It is used for nameof only

@xperiandri
Copy link

  • Everytime I tried to access args, I couldn't get anywhere;
    • ie I get no Intellisense (except GetType & ToString; I tried open Elmish.WPF.ViewModelArgs/ViewModel to no avail
    • and I get errors such as: The type 'ViewModelArgs<_,_>' does not define the field, constructor or member 'InitialModel'

I've added such property in Elmish.Uno

@YkTru
Copy link
Author

YkTru commented Nov 9, 2024

I can barely think straight—I’ve hardly slept in three days trying to figure this out I'm exhausted; If I can’t resolve this issue, my whole project is doomed.. And there’s no way I’m going back to C#+MVVM+Prism and all the nulls/classes/interfaces/services/tests/files explosion madness. It's a shame MS refuse to add proper F#+WPF/MAUI implementation.. what a waste.


Anyway… would a less-than-ideal setup like this actually work? I can’t seem to make it happen, and with my lack of sleep and experience here, I’m struggling to judge it clearly.

AppY.fs: (all compiles, I add the 'Y' suffix to avoid collisions):

type Model =
        { Items: Parent list
          SelectedItem: Guid option
        }



        // Msg
        | UpdateSelectedItem of Guid option

        // update
        | UpdateSelectedItem itemId ->
            { model with
                SelectedItem = itemId
                Log = sprintf "Selected Item: %A" itemId },
            Cmd.none

    // function to call from C#.. if possible/relevant
    let dispatchUpdateSelectedItem (dispatch: Msg -> unit) itemId = 
        dispatch (UpdateSelectedItem itemId)

MainWindow.cs: And now the terrible, pathetic part (I can't find what to put in dispatch):

using Elmish;
using Elmish.WPF;

namespace TreeView_SelectedItem_Behaviors.WPF;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void TreeView_OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {

        Action<AppY.Msg> dispatch = ???

        if (e.NewValue is Parent_VM p)
        {
            AppY.dispatchUpdateSelectedItem(dispatch, p.Id);
        }
    }
}

.. I really can’t stand interop…

Well, I really hope someone can eventually help with this.. thanks.

@xperiandri
Copy link

Why don't you use submodelSeq
Get initial model via reflection for now

@YkTru
Copy link
Author

YkTru commented Nov 9, 2024

@xperiandri Would you be able (if you have time) to share a sample please, ideally as a GitHub repository or a zip file so I can better analyze, with just the essentials (Program file + XAML) to demonstrate exactly your proposed approach? (I'm "conceptually" confused by the recursive multi-level item selection part + the dynamic/static issues between ElmishWPF and WPF)

Or even better (if it's easier), you could submit a PR to the repo I shared earlier, focusing only on adding the specific bindings and XAML modifications (assuming my coding style is clear)?

This would be for sure very helpful.
Thank you

@xperiandri
Copy link

@YkTru see https://github.com/xperiandri/Elmish.Uno/tree/submodel-selected-item-cascading
SubModelSelectedItem.Cascading.fs

I have not finished UI, only logic

@xperiandri
Copy link

@marner2 I've rebased my code on top of your latest code. So no my changes are more easily pickable.

Pay attention to Fixed wrong initial binding value in static view models base class commit

@YkTru
Copy link
Author

YkTru commented Nov 11, 2024

@xperiandri Great, thanks! I'll take a closer look at this later in the week when I have time and work on adapting my sample with it.

Questions: (These are just "at first glance" questions/uneasiness)

  • OO: I noticed a fair amount of OO programming in your approach (e.g., in the Model). Is this mostly a stylistic choice, or is it essential to your design? I guess the Model type/class with its members is primarily/only meant to act as an API, encouraging a particular usage pattern (?).

  • Is there a particular reason why you don’t use as much of an OO-heavy approach in the other samples in your repo? I’m generally not inclined to use OO extensively in F#—it brings back (bad) memories of my C#/Prism days (though I know that might be more personal than rational). That said, I’m open to rethinking my style if there are specific advantages here.

  • Reflection:

Why don't you use submodelSeq Get initial model via reflection for now

Is there any risk associated with using reflection to get the InitialModel? Could someone potentially access and manipulate critical fields in the Model this way?

@xperiandri
Copy link

I don't care if it is OO or FP approach, whichever solves the problem better.
Model property and IEnumerable implementation can actually be omitted and a function transforming into a list can be used.
And IModel can be omitted too, you will just expose an obj list
So that sample can be simplified

@YkTru
Copy link
Author

YkTru commented Nov 11, 2024

I don't care if it is OO or FP approach, whichever solves the problem better. Model property and IEnumerable implementation can actually be omitted and a function transforming into a list can be used. And IModel can be omitted too, you will just expose an obj list So that sample can be simplified

• I totally agree: I am just curious why you opted for the OO approach in that sample and what advantages you see in it compared to the FP approach, which, all else being equal, seems easier to read and more concise to me. I’m not very familiar with F# OO, so I may not fully appreciate its benefits (as I mostly learned from Wlaschin, who rarely uses OO in his code snippets)

• Regarding using reflection to obtain the 'Initial Model,' sorry to insist but do you think it's safe enough in this specific case? I've often been advised to avoid reflection except for debugging or plugin-based extensibility, but just like with OO I'm open to revise my beliefs/impressions as well

@xperiandri
Copy link

Yes, it is safe
@marner2 maybe add that initial model property in WPF too?

@TysonMN
Copy link
Member

TysonMN commented Nov 13, 2024

I can barely think straight—I’ve hardly slept in three days trying to figure this out I'm exhausted;

I don't recommend trying to solve a problem like this without sleep. In fact, I love trying to solve a problem like this while in bed trying to fall asleep.

Have you solved this problem with a traditional WPF (i.e. MVVM C#) application? If so, can you share a minimal working example of that?

@YkTru
Copy link
Author

YkTru commented Nov 13, 2024

@TysonMN Here I made this C# implementation this morning (I use the excellent MVVMGen library for cleaner VMs):

https://github.com/YkTru/Cascading-ListBoxes---SubModelSelectedItem/tree/C%23_MVVM

It's a quick and rough draft, so my naming conventions and usage of MVVMGen might not be perfectly consistent throughout.

However, everything is functioning exactly as I intended, ie:

Goal: I’m aiming for an ItemsControl that can dynamically create ListBoxes based on selected items or added children, supporting an n-level depth.


Note: I still depends on the SelectedItem prop of the ListBox element in the template:
SelectedItem="{Binding SelectedNode, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"

I therefore still haven't figured out how to achieve the same behaviors in Elmish.WPF (particularly when using subModelItemSelected).

One thing I’m certain of now, though, is that I must use a Level_VM in the ElmishWPF version.


F# Version update: I don’t have time today to work on a corresponding F# version, but if you’d like to share any insights or code samples to guide me as for the bindings part (you can refer to my master branch's Models/VM/Xaml), it would surely be helpful.

@TysonMN
Copy link
Member

TysonMN commented Nov 14, 2024

The C# code has logic that adds to and removes from the tree. Is that behavior shown in the video in your OP?

@YkTru
Copy link
Author

YkTru commented Nov 14, 2024

That's ultimately the "complete" goal, as I mentioned in my initial post:

Goal: I’m aiming for an ItemsControl that can dynamically create ListBoxes based on selected items or added children, supporting an n-level depth.

I probably should have included an "Add Child" button in the F# example shown in the video. Or not as it is should be a MWE

In the first version my focus was on getting the selection behavior right using subModelSelectedItem (i.e., ensuring updates reflect in the property editor as I select any n-level items and avoiding selection errors); even in the hardcoded F# version without Add logic I wasnt able to select any other item than the level-0 items.

I'll work on an F# version of the C# version today without buttons.

@YkTru YkTru closed this as completed Nov 14, 2024
@TysonMN
Copy link
Member

TysonMN commented Nov 14, 2024

You closed as complete.

Did you solve your problem?

@YkTru
Copy link
Author

YkTru commented Nov 14, 2024

OMG no! I'm working on it right now

@YkTru YkTru reopened this Nov 14, 2024
@YkTru
Copy link
Author

YkTru commented Nov 18, 2024

All is working, I guess; the WPF behavior did the trick + accessing "CurrentModel" from ElmisWPF's ViewModels module (godbless chatgpt-o1 on that one) :

Video_2024-11-18_131122.mp4

I'll have to refactor everything eventually and make it safer + tests,

Q1: @TysonMN - Do you not recommend to use Hedgehog, and stick with FsCheck/XUnit exclusively?
Q2: @xperiandri and @marner2 - what do you think about the way I'm using VMs, and the overall code structure (modules, helpers, files, etc.)?

Q3 @LyndonGingerich @marner2 @xperiandri Do you think it would be valuable to turn this into a fully-featured sample by incorporating serialization, validation, asynchronous web/REST requests, make it as a treeview and other similar functionality showcasing all ElmishWPF bindings helpers + WPF interactions (behaviors/attached prop/dp) in a single solution? This is the kind of resource I wish I had when I started learning Elmish.WPF about a year ago.


Q4: @marner2 : I agree with @xperiandri on adding a CurrentModel+InitialModel props for easier/idiomatic property access from ElmishWPF's ViewModels

I had to make an helper which I'm sure should be improved:

            let getCurrentModel<'model, 'msg> (viewModel: obj) : 'model option =
                let vmType = viewModel.GetType()

                match vmType.GetInterface("IViewModel`2") with
                | null ->
                    printfn "The object does not implement IViewModel<'model, 'msg>."
                    None
                | _ ->
                    match vmType.GetProperty("CurrentModel") with
                    | null ->
                        printfn "The object does not have a CurrentModel property."
                        None
                    | prop ->
                        match prop.GetValue(viewModel) with
                        | :? 'model as model -> Some model
                        | _ ->
                            printfn
                                "The CurrentModel property could not be cast to the expected type."

                            None

@TysonMN
Copy link
Member

TysonMN commented Nov 20, 2024

Q1: Either is fine, but F# Hedgehog is not currently being actively maintained.

@YkTru YkTru closed this as completed Nov 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants