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

Attempting to emulate C# CollectionViewSource among three views of the same list. #300

Closed
awaynemd opened this issue Nov 22, 2020 · 35 comments

Comments

@awaynemd
Copy link

How to emulate the C# CollectionView functionality.

I have attempted (unsuccesfully) to do the following:

  1. download a list of names (i.e., ViewName) from a WCF service. This works! (Although I do not know if my use of Async is correct not to block the UI). But for now, this does work correctly to download about 20,000 names.

  2. Create only ONE read of the database.

  3. The read database names are then to be provided to three separate user controls. Each user control is to receive user input from a textbox and will be sorted in a different order. Each user control will then provide this user input into a shared filter. Shared in the sense that all three usercontrols will use the same filter on the same list of objects. The filters are:
    1. All ViewNames with LastName starting with the text characters received from the LastName usercontrol.
    2. All ViewNames with FirstName starting with the text character received from the FirstName usercontrol.
    3. All ViewNames with BirthDate equaling the date typed into the BirthDate usercontrol.
    The final filter which ALL LIST VIEWS will use in the boolean combination of the three above conditions.

(The sorted viewables will be:

  1. For the LastName, sort is by LastName, FirstName, and Birthdate (all ascending),
  2. For the FirstName, sort is by FirstName, LastName, and Birthdate (all ascedning),
  3. For the BirthDate, sort is by BirthDate, LastName, FirstName (all ascending).

Everything initially worked individually, but I was unable to use the filter from one usercontrol in another. That is, my thinking is to have one source list/array and somehow pass the conditions of each usercontrol to the other usercontrols --or top module. Hence, althought the sort orders for the list will be different, the source list and the filter will be identical between all user controls.

I tried passing a common record, "names", with the Msg component--but failed miserably.

If anybody could give any thought to this, this newbie would sure appreciate it!

(As an aside, I never could figure out how to verify a text string as a valid date in F# to be used in a birth date filter :( )

TIA

Full (frustrating) code found at: https://github.com/awaynemd/PatientFinder

@TysonMN
Copy link
Member

TysonMN commented Nov 23, 2020

It might be two weeks before I have time to look into this.

I never could figure out how to verify a text string as a valid date in F#...

Do you know how to do this in C#?

@awaynemd
Copy link
Author

awaynemd commented Nov 23, 2020

@TysonMN Yes.

I'm updating the repo with most of it working. Each usercontrol correctly works to display its specific filter to the xaml's.

The obvious errors I see are:

  1. Each submodel has its own copy of the original Suggestion list -- no changes by filtering are mirrored to each submodel.
  2. I don't know how to make one big filter that all the submodels respect.
  3. I think it would be a better design if I used the name id's, but I again don't know how.

In any event, I'll keep trying. Happy Thanksgiving!

New Code at: https://github.com/awaynemd/PatientFinder

@TysonMN
Copy link
Member

TysonMN commented Nov 23, 2020

I never could figure out how to verify a text string as a valid date in F#...

Do you know how to do this in C#?

Yes.

Both C# and F# have access to the .NET API, so this can probably be done the same in both languages.

@awaynemd
Copy link
Author

@TysonMN I think I got the DateTime thing working, now to get messages from the children up to the parent??

@TysonMN
Copy link
Member

TysonMN commented Nov 23, 2020

now to get messages from the children up to the parent

The parent needs to match on the child's message.

Module Parent

type ParentModel =
  { Child: ChildModel }

type ParentMsg =
  | ChildMsg of ChildMsg

let update msg m =
  match msg with
  | ChildMsg MessageForParent -> // child is telling me something
  | ChildMsg msg -> { m with Child = Child.update msg m.Child }

@awaynemd
Copy link
Author

awaynemd commented Nov 23, 2020

@TysonMN Do I need to add MessageForParent to the Msg du of the parent?

(I'm getting the error: "This rule will never be matched").

Thanks.

Module Parent

type Msg =
        | FinderBirthDateMsg of FinderBirthDate.Msg
        | FinderLastNameMsg  of FinderLastName.Msg
        | FinderFirstNameMsg of FinderFirstName.Msg

    let update msg m =
        match msg with
        | FinderBirthDateMsg msg ->
            { m with FinderBirthDate = FinderBirthDate.update msg m.FinderBirthDate}
        | FinderLastNameMsg msg ->
            { m with FinderLastName  = FinderLastName.update msg m.FinderLastName}
        | FinderFirstNameMsg msg ->
            { m with FinderFirstName = FinderFirstName.update msg m.FinderFirstName} 
        | FinderLastNameMsg SetText ->
              { m with FinderLastName  = FinderLastName.update msg m.FinderLastName}
Child Module

type Msg =
             | SetOpen of bool
             | SetText of string
             | SetSelectedItem of name option
    
    let update msg m  =
        match msg with
        | SetOpen o -> { m with IsOpen = o }
        | SetText s -> { m with Text = s; IsOpen = true; Visibility = Visibility.Visible }
        | SetSelectedItem i -> { m with SelectedItem = i; IsOpen = i.IsNone; Visibility = Visibility.Collapsed; Text = if i.IsSome then i.Value.ViewName.lastname else "" }  

@TysonMN
Copy link
Member

TysonMN commented Nov 23, 2020

Order matters.

let update msg m =
  match msg with
  | ChildMsg MessageForParent -> // child is telling me something
  | ChildMsg msg -> { m with Child = Child.update msg m.Child }

is not the same as

let update msg m =
  match msg with
  | ChildMsg msg -> { m with Child = Child.update msg m.Child }
  | ChildMsg MessageForParent -> // child is telling me something // warning: This rule will never be matched

@awaynemd
Copy link
Author

@TysonMN Changing the order now results in:

| FinderLastNameMsg msg ->

"This rule will never be matched"???

@TysonMN
Copy link
Member

TysonMN commented Nov 23, 2020

You need to match in this order

| FinderLastNameMsg SetText -> ...
| FinderLastNameMsg msg -> ...

Here is what the match expression documentation says.

In the match expression, the test-expression is compared with each pattern in turn, and when a match is found, the corresponding result-expression is evaluated and the resulting value is returned as the value of the match expression.

You are misunderstanding the use of "in turn" there.

@awaynemd
Copy link
Author

@tyson :(

Parent module:

let update msg m =
        match msg with
        | FinderLastNameMsg SetText ->
            { m with LastNameText = "" }   
        | FinderLastNameMsg msg ->
            { m with FinderLastName  = FinderLastName.update msg m.FinderLastName}

The first one yields:

Uppercase variable identifiers should not generally be used in patterns,
and may indicate a missing open declaration or a misspelt pattern name.

The second yields:

This rule will never be matched

I'm hopeless...I guess I'll just eat worms.

@TysonMN
Copy link
Member

TysonMN commented Nov 24, 2020

The only branch in your repo is main and the head of that branch builds successfully (i.e. no errors) and without any warnings. Is there a commit you can share that exhibits the behavior that you described?

I'm hopeless...I guess I'll just eat worms.

You are your own worst enemy. You need to be your biggest fan and most enthusiastic cheerleader.

Whether you think you can, or you think you can't – you're right,
~Henry Ford

@awaynemd
Copy link
Author

@TysonMN Yes/No. Everything works as is--but everything is independent, i.e. Each list/submodel works to correctly filter only on what the user types into the TextBox for that list.

However, what I would like to achieve is have the three lists all share a common itemsSource with a filter that is the logical "And" of each/all the inputs from each lists textBox. Hence, I'm thinking I need someway for the topmost "App" model to send the source list (the one before filtering) along with the combined filter to each submodel which can then modify its part of the filter, while each submodel will send its filter back to the main model so the other children/lists will have it.

Anyway, long story short, I want the submodels to send the text received from their TextBox back up to the main model--and I don't know even where to start (so there is no code for it...yet).

Thanks for the encouragement. F# and Elmish/Elmish.WPF are great frameworks--but there is a very steep learning curve.

@TysonMN
Copy link
Member

TysonMN commented Nov 25, 2020

I can look at your overall design and desired behavior eventually. In the meantime, I can help you with the match expression that was either giving you an error or a warning if you share it with me.

@awaynemd
Copy link
Author

@TysonMN I find the discussion here most pertinent to what I am looking for: https://discourse.elm-lang.org/t/modifying-parent-state-from-child-page-in-an-elm-spa-example-like-architecture/2437

But how to apply this to Elmish.wpf is (temporarily) above my pay grade. How is Elmish.WPF related to Elm?

(p.s., I think I got all my match expressions working--except those as mentioned in the article).

@TysonMN
Copy link
Member

TysonMN commented Nov 25, 2020

I recently reached the point in my application at work where it is now common for me to write new code in which a child updates the state of its parent.

I don't know what is best. As mentioned in that discussion, this is what I am doing.

...expose LoginPage.Msg(..) and match on that in Main.update, but that’s a bit leaky and messy since I have to exhaustively handle it in both update functions.

@awaynemd
Copy link
Author

@TysonMN Please take a look at: https://tarmil.fr/article/2019/12/17/elmish-page-model

This is a good article that pretty well describes my problem. It will take awhile for me to understand it :)

@TysonMN
Copy link
Member

TysonMN commented Nov 25, 2020

@TysonMN Please take a look at: https://tarmil.fr/article/2019/12/17/elmish-page-model

That is a nice article. I currently use the second approach, which involves a discriminated union. As mentioned in the article, it is better than the first approach with a bunch of options since more illegal (model) state is unrepresentable. Also as mentioned in the article, both approaches suffer from "illegal ('model, 'msg) pairs" in that the type system allows certain messages to be dispatched when the model is in a certain state even though that would never happen at runtime (barring race conditions). This is what I was getting at in #295 (comment) when I said some message types are less commutative.

@TysonMN I find the discussion here most pertinent to what I am looking for: https://discourse.elm-lang.org/t/modifying-parent-state-from-child-page-in-an-elm-spa-example-like-architecture/2437

I also like this discussion. I like the idea of having an OutMsg and then streamlining its use via the translator pattern. In Elmish.WPF, the translation would happen when the parent is converting the child bindings to its own bindings. I will try this sometime soon and report back.

How is Elmish.WPF related to Elm?

Elmish implements the Elm / Model-View-Update / MVU architecture in F#. Elmish.WPF is a specialization of Elmish to WPF and with the pragmatic decision to have the user statically define their WPF UI just like in a traditional WPF application instead of dynamically generating this content like in Elm and many other UI-specific implementations of the Elm architecture.

@awaynemd
Copy link
Author

@TysonMN I'm still trying to understand this code. It works well to transmit the child text back up to the parent.

(This was taken from a really good blog: https://medium.com/@MangelMaxime/my-tips-for-working-with-elmish-ab8d193d52fd )

module Login =

    type ExternalMsg =
        | NoOp
        | SignedIn of Session

    type Msg =
        | Login
        | LoginCompleted of Session
        | LoginFailed of exn

    let update (msg : Msg) (model : Model) =
        match msg with
        // ...
        | LoginCompleted session ->
            model, Cmd.none, SignedIn session
            

module Parent =

    type Model =
        { Session : Session option
          Login : Login.Model  }        

    type Msg =
        | LoginMsg of Login.Msg

    let private update (msg : Msg) (model : Model) =
        match msg with
        | LoginMsg loginMsg ->        
            let (loginModel, loginCmd, loginExtraMsg) = Login.update loginMsg model.Login

            // Here we can match over loginExtraMsg to do something
            let newModel =
                match loginExtraMsg with
                | Login.ExternalMsg.NoOp -> model
                | Login.ExternalMsg.SignedIn session -> { model with Session = Some session }

            { newModel with Login = loginModel }, Cmd.map LoginMsg loginCmd

@TysonMN
Copy link
Member

TysonMN commented Nov 26, 2020

Ah, yes. Thanks for finding and sharing that. I remember reading this post by Mangel when I was new to Elmish.WPF, and I remember that he had a suggestion on how to do child-to-parent communication, but I didn't remember what he suggested.

I now see (in the language of the Elm discussion) that he recommends an OutMsg type but not the full translator pattern.

@awaynemd
Copy link
Author

@TysonMN I have made significant progress with this and would like to share, but I do not want to destroy the main repo here represented. How do I do that?

@TysonMN
Copy link
Member

TysonMN commented Nov 28, 2020

Make a branch and push that to your repo.

@awaynemd
Copy link
Author

@TysonMN The newest version, posted below, has most of the behavior I need. It works correctly to:

  1. The parent xaml presents three separate lists. When a letter (in lastname or firstname) is typed into the textbox, the popup will open showing a filtered list based on that letter and the other two textboxes. The birthdate textbox uses regex to match for MM/dd/yyyy input as numbers. (I felt obliged to use code-behind on the birthdate textbox to screen keyboard input with numbers only :) )

  2. All three textboxes/lists communicate back to the parent app with any text input and/or a selected item.

  3. Each textbox/list is filtered based on all three textboxes. User making a selection will show the selection made in all three textboxes.

  4. Each textbox/list has its own sort method. (Similar to a CollectionSource).

What I don't like:

  1. In reviewing the code, the submodels show alot of similarity. I bet there is a good way to simplify this code?

  2. For some reason, at startup, it takes two-clicks for a textbox to obtain focus. It would be nice if it responded immediately to one click.

  3. It feels wrong that a source list, downloaded from the database, is being copied into three different lists. (But I don't know how to do it otherwise and still provide for the correct binding--each with a different sort method).

  4. I did not use Id's for the ViewName values.
    4.1 I don't know how to use Id's
    4.2 It see Ids would add significant complexity.

  5. When watching this run, it appears the top parent app runs through its "bindings" multiple times when simply entering a character into one textbox. Although its very fast, it seems totally unnecessary. I'm thinking once should be enough?

Lastly the ONLY way I could achieve child-to-parent communication was to use Program.mkProgramWpf. I could never get
Program.mkSimpleWpf to work in this example.

I'd appreciate any suggestions to improve this code--especially a way to avoid multiple copies (although with different sorts) of the same list. I have 20,000 names.

Thanks

Please review updated version at: https://github.com/awaynemd/PatientFinder

@TysonMN
Copy link
Member

TysonMN commented Nov 30, 2020

Glad to hear that you have made progress. I should be able to review in detail within one week.

@awaynemd
Copy link
Author

awaynemd commented Nov 30, 2020

@TysonMN :-) !!

@TysonMN
Copy link
Member

TysonMN commented Dec 1, 2020

lol, what? Did you solve all your problems? 👀

@TysonMN
Copy link
Member

TysonMN commented Dec 3, 2020

I executed your program, but it crashes on load because I don't have your connected service. After I replaced

let data = context.GetAllPatientNamesAsync() 
return! Async.AwaitTask data

with

return Array.empty

the application doesn't crash, but then I don't have any data, and it doesn't seem like I can add any.

What would you like help with now?

@awaynemd
Copy link
Author

awaynemd commented Dec 3, 2020

@TysonMN Sorry...I do not know how to include database material in a github example. I wish I could say I've solved all my problems :) Now, I seemed to have done something which prevents me from exiting the program! Or its in some kind of loop??

I'll continue to work on it. Thanks.

@TysonMN
Copy link
Member

TysonMN commented Dec 3, 2020

If you seed it with some fake data, then I will take a look.

@TysonMN
Copy link
Member

TysonMN commented Dec 4, 2020

@TysonMN I find the discussion here most pertinent to what I am looking for: https://discourse.elm-lang.org/t/modifying-parent-state-from-child-page-in-an-elm-spa-example-like-architecture/2437

I also like this discussion. I like the idea of having an OutMsg and then streamlining its use via the translator pattern. In Elmish.WPF, the translation would happen when the parent is converting the child bindings to its own bindings. I will try this sometime soon and report back.

I am now reporting back.

I love the translator pattern!

The thing I liked the least about the SubModelSeq sample was the "need" for

let adjustMsgToParent msg =
match msg with
| BranchMsg (pId, LeafMsg (Remove cId)) when pId = cId -> LeafMsg (Remove cId)
| BranchMsg (pId, LeafMsg (MoveUp cId)) when pId = cId -> LeafMsg (MoveUp cId)
| BranchMsg (pId, LeafMsg (MoveDown cId)) when pId = cId -> LeafMsg (MoveDown cId)
| _ -> msg

Syntactically, it is unsatisfying to have the wildcard pattern in a match. Semantically, this function is admitting that a mistake was made because having both those IDs the same at two different levels is definitely wrong.

I "knew" why this was happening. It is because those three messages are meant for the parent while the other two are meant for the child.

type SubtreeMsg =
| CounterMsg of CounterMsg
| AddChild
| Remove of Guid
| MoveUp of Guid
| MoveDown of Guid

| CounterMsg of CounterMsg // change MY counter
| AddChild                 // add a child to ME
| Remove of Guid           // remove CHILD with that ID...don't remove ME
| MoveUp of Guid           // move up CHILD with that ID....don't move ME up
| MoveDown of Guid         // move down CHILD with that ID...don't move ME down

...but I was creating those last three messages like they should applied to "ME" and then fixing my mistake via adjustMsgToParent. I even included MsgToParent in the name of that function.

The idea that I didn't have, the idea present in both this post by Maxime Mangel
and this post by Alex Lew (which is provided by the first commenter in the Elm discussion), is to have a second message type to distinguish messages meant to update the current (child) model and message meant to update the parent model.

Now let me differentiate the approaches by Maxime and Alex as I understand them in the context of Elmish.WPF. I will call the approach by Maxime the "OutMsg pattern" and I will call the approach by Alex the "OutMsg with translator pattern". As the names suggest (and as I just said in the previous paragraph), both approaches introduce a second message type called the out message. In the OutMsg pattern, the conversation from an out message to the parent message happens in update of the parent (see the example code by Maxime). In the OutMsg with translator pattern, the conversion from an out message to the parent message happens in a Elmish.WPF binding.

For examples of the OutMsg with translator pattern applied to Elmish.WPF, see the SubModelSeq sample in most recent commits to master and v4.

The advantage of the OutMsg approach is that it is immediately applicable to all implementations of the Elm architecture. As I understand the OutMsg with translator pattern for Elmish.WPF, the place to apply that translation is in the toMsg argument of a SubModel(Seq) binding (or via a call to Bindgings.mapMsg in the v4 branch). The advantage in this case is that we already map a child message to a parent message (so the translator naturally fits there) and update is not made more complicated.

And to add another plug for v4 and the greatness that is Bindings.mapMsg, look at how the case constructor InMsg occurs once and the case destructor occurs twice (once in a recursive context and once in a base-case context). So elegant!

@cmeeren, what do you think? What do you think about achieving child-to-parent communication via the OutMsg with translator pattern? What do you think about the application of that pattern to the SubModelSeq sample(s)?

Now to go apply the OutMsg with translator pattern in my application at work.

@cmeeren
Copy link
Member

cmeeren commented Dec 4, 2020

@cmeeren, what do you think? What do you think about achieving child-to-parent communication via the OutMsg with translator pattern? What do you think about the application of that pattern to the SubModelSeq sample(s)?

I think the translator pattern is as ingenious as it is simple. 😊 It improves the SubModelSeq sample, though since it is recursive and makes use of several generic wrapper types, it's so complicated already that it's not the best way to demonstrate it (not even for me, who is at least somewhat familiar with the sample).

@TysonMN
Copy link
Member

TysonMN commented Dec 4, 2020

....though since [the SubModelSeq sample] is recursive and makes use of several generic wrapper types, it's so complicated already that it's not the best way to demonstrate it (not even for me, who is at least somewhat familiar with the sample).

Completely agree. I had partially written a new sample called TranslatorPattern that was going to be like SubModelSeq but without the recursive tree structure before (in an instant) I realized how to apply the pattern to the SubModelSeq sample.

To simplify things, I think we could rename the SubModelSeq sample to SubModelSeqTree and create a new sample SubModelSeqList that is the same as SubModelSeq is now but without the recursive structure.

@cmeeren
Copy link
Member

cmeeren commented Dec 4, 2020

To simplify things, I think we could rename the SubModelSeq sample to SubModelSeqTree and create a new sample SubModelSeqList that is the same as SubModelSeq is now but without the recursive structure.

Yes, that might be a good idea. We should remember to search the whole repo for "SubModelSeq" and update any references in documentation etc.

@awaynemd
Copy link
Author

awaynemd commented Dec 5, 2020

@TysonMN Sorry to be so dense here. I've been following this excellent conversation. When you get a chance, can you explain to this newbie the English meaning of:

| BranchMsg (pId, LeafMsg (Remove cId)) when pId = cId -> LeafMsg (Remove cId)

What does it accomplish? Thanks.

@TysonMN
Copy link
Member

TysonMN commented Dec 5, 2020

Are you asking for its semantic meaning or its syntactic meaning?

It doesn't have a good semantic meaning. It is a hack I found to get the correct behavior. Otherwise, messages were either causing changes in the wrong place or not causing any change at all.

Syntactically, it is a case in a pattern match. Not only must the target match that structure, but it must also satisfy the predicate pId = cId.

@TysonMN
Copy link
Member

TysonMN commented Aug 15, 2021

Closed due to lack of activity. Feel free to continue the conversation.

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