elmish / Elmish.WPF

Static WPF views for elmish programs
Other
431 stars 71 forks source link

Accessing Xaml Objects For Newly Created Window - HELP WANTED #445

Closed minewarriorsSchool closed 2 years ago

minewarriorsSchool commented 2 years ago

So currently I am creating an application which schedules employees. For a custom feature that creates "custom made" projects and employees, I open a new window like Window2 out of the newwindow example. This all works perfectly fine. However, I need to acces a XAML object itself now and have no idea how to do that.

I make use of devexpress and need to acces the 2 gridcontrol objects that are in this window, but I have no idea how I should go by this. There is not really a way to bind to an object and get a reference of an object if I am correct.

In my main window I actually have passed the main gridcontrol of the application in the app.xaml.cs like we do in the newWindow example with the Window.

But since this newWindow "Popup" gets created at run time all the time, I see no way to do the same thing.

Hopefully you guys have an idea on how I could manage this.

Example: ` <dxg:GridControl ItemsSource="{Binding AllAvailableObjectsGrid}" AutoGenerateColumns="AddNew" EnableSmartColumnsGeneration="True" Name="AvailableObjectsGrid" SelectionMode="MultipleRow" ItemsSourceChanged="AvailableProjects_ItemsSourceChanged" MaxWidth="500" HorizontalAlignment="Left"

`

So what I want to achieve is that I can do for this newly generated window is: AvailableObjectsGrid.MethodX().

thanks in advance for your help, time and consideration!

TysonMN commented 2 years ago

If I understand correctly, I think you should be able to create a Cmd binding for MethodX. Does that make sense?

If you are still having problems, then share a link to a GitHub repository that reproduces the problem.

minewarriorsSchool commented 2 years ago

It does make sense, but it would be MethodX of the XAML object . And this tag remains in a window that is being continiously generated newly like a popUp in the NewWindow Example.

So I want to be able to control all the methods actually of that GridControl object in the code. But I can not pass the object through the App.xaml.cs since the NewWindow Example of Elmish creates a fresh window all the time if I am correct.

TysonMN commented 2 years ago

Can you share a link to a GitHub repository where you access the XAML object of the main window the way you want to access the XAML object of the a second newly created window... and also have a second newly created window in the example? Do you only want one of these second newly created windows open at a time?

minewarriorsSchool commented 2 years ago

Could take some time to set up a repository with a sample project. Will come back to this thread eventually when I have found the time for it!

Have some other priorities in the main time of solving first on the backlog.

BentTranberg commented 2 years ago

Seems to me what you want is to access and use GUI objects the way one does when using code-behind. That doesn't fit well with the Elmish way. I think perhaps this is an XY-problem.

I also use DevExpress, and after some years I tend to stay away from the more complex components they have. One specific reason is that SelectedValue and SelectedValuePath are not supported, which doesn't fit well with the Elmish.WPF way, although I've gotten dxg:GridControl to work without that, sort of. Not always well.

But I don't recall having had to get to the actual dxg:GridControl to achieve anything. Though I have made some hacky functions to do that sort of thing, as a workaround when nothing else will do.

minewarriorsSchool commented 2 years ago

Seems to me what you want is to access and use GUI objects the way one does when using code-behind. That doesn't fit well with the Elmish way. I think perhaps this is an XY-problem.

I also use DevExpress, and after some years I tend to stay away from the more complex components they have. One specific reason is that SelectedValue and SelectedValuePath are not supported, which doesn't fit well with the Elmish.WPF way, although I've gotten dxg:GridControl to work without that, sort of. Not always well.

But I don't recall having had to get to the actual dxg:GridControl to achieve anything. Though I have made some hacky functions to do that sort of thing, as a workaround when nothing else will do.

@BentTranberg may I ask how you dynamically bestfit your columns without using the actual dxg:gridcontrol or using any of the methods that is within the object? Or do you not even touch that at all?

For example I have made a custom multi cell edit for which I needed the row handles and values to change the selection. For this I would use methods in the actual gridcontrol which I passed through the constructor of the elmish window. How would you do this without touching the object it's method and only using it properties? I do not see a way how hahaha.

BentTranberg commented 2 years ago

I don't have an opinion on what's the best approach. If your idea works well, then that will perhaps be the easiest.

The MVVM Framework in the DevExpress WPF components has many interesting features. If faced with this challenge, I would perhaps explore several of these to see if they could be of any help. Just as an example; I believe one possibility is that you can indeed call functions of a control through a binding.

Another idea is to create a wrapper or descendant of the dxg:GridControl, in order to add functionality that fits better with Elmish.WPF

And then there's what I mentioned. Within the binding functions I've used reflection to get to objects. Not sure how popular I would be if I posted the function I use for that here.

TysonMN commented 2 years ago

Not sure how popular I would be if I posted the function I use for that here.

If you are on the fence, then you should definitely share it.

I am not against "breaking the rules". Consider a comparison with pure and impure code. Pure code is great, but impure code is not evil; sometimes it is necessary. However, impure code is more risky. If most of your code is pure, then it becomes possible to take risks with impure code and manage your total risk.

Furthermore, it is very informative to see what functionality users want especially when this involves breaking the rules. This shows me where the existing API is lacking or completely missing.

BentTranberg commented 2 years ago

This is it. I haven't used it for a very long time, so don't remember exactly what it's about. Perhaps there's another proper way now. I have improved much of my old code, and Elmish.WPF is far better now, so most of these hacks have gone away.

(Thanks cmeeren and tysonmn for all you did, not just coding Elmish.WPF but also to teach us how to use it. And there are some more heroes out there that also contributed and helped people like me from time to time. Thanks to you too.)

[<RequireQualifiedAccess>]
module ElmishWpfHack =

    open System

    let peekViewModel<'M,'R> (vm: obj) =
        if not (isNull vm) then
            let t = vm.GetType()
            let p = t.GetProperty("CurrentModel", Reflection.BindingFlags.NonPublic ||| Reflection.BindingFlags.Public ||| Reflection.BindingFlags.Instance)
            if not (isNull p) then
                if p.CanRead then
                    let value = p.GetValue(vm, null)
                    match value with
                    | :? Tuple<'M,'R> as tu -> Some tu
                    | _ -> None
                else
                    None
            else
                None
        else
            None

These are some snippets from elsewhere in my huge project that explains a bit more, hopefully, about my use case.

Two of these one-liners are dead code.

    let peek (o: obj) = ElmishWpfHack.peekViewModel<Model,RegisterModel> o
    let peekCmdParam (f: RegisterModel -> Msg) (models: obj) = match peek models with Some (_, rm) -> f rm | None -> NoOp
    let peekIf (f: RegisterModel -> bool) (models: obj) = match peek models with Some (_, rm) -> f rm | None -> false

but one of them is used like this

            "AddModuleRegister" |> Binding.cmdParam (peekCmdParam (fun rm -> AddModuleRegister rm.Id))
            "MoveRegisterUp" |> Binding.cmdParam (peekCmdParam (fun rm -> MoveRegisterUp rm.Id))
            "MoveRegisterDown" |> Binding.cmdParam (peekCmdParam (fun rm -> MoveRegisterDown rm.Id))
            "DeleteRegister" |> Binding.cmdParam (peekCmdParam (fun rm -> DeleteRegister rm.Id))

Somewhere else again

            "Rows" |> Binding.subModelSeq ((fun (m: Model) -> m.Rows), (fun r -> r.Guid), fun () ->
                [
                    "Guid" |> Binding.oneWay (fun (_, r) -> r.Guid)
                    "Knapp" |> Binding.twoWay ((fun (m, r: SorterRow) -> r.Knapp), (fun v (m, r: SorterRow) -> SetRowKnapp (r.Guid, v)))
                    "Register" |> Binding.twoWay ((fun (m, r: SorterRow) -> r.Register), (fun v (m, r: SorterRow) -> SetRowRegister (r.Guid, v)))
                    "Lengde1" |> Binding.twoWay ((fun (m, r: SorterRow) -> decimal r.Lengde1), (fun v (m, r: SorterRow) -> SetRowLengde1 (r.Guid, float v)))
                    "Lengde2" |> Binding.twoWay ((fun (m, r: SorterRow) -> decimal r.Lengde2), (fun v (m, r: SorterRow) -> SetRowLengde2 (r.Guid, float v)))
                    "Lengde3" |> Binding.twoWay ((fun (m, r: SorterRow) -> decimal r.Lengde3), (fun v (m, r: SorterRow) -> SetRowLengde3 (r.Guid, float v)))
                    "FuktMin" |> Binding.twoWay (
                        (fun (m, r: SorterRow) -> match r.FuktMin with None -> "" | Some v -> string v),
                        (fun (v: string) (m, r: SorterRow) -> SetRowFuktMin (r.Guid, Misc.stringToIntOpt v)))
                    "FuktMaks" |> Binding.twoWay (
                        (fun (m, r: SorterRow) -> match r.FuktMaks with None -> "" | Some v -> string v),
                        (fun (v: string) (m, r: SorterRow) -> SetRowFuktMaks (r.Guid, Misc.stringToIntOpt v)))
                ])
            "SelectedItem" |> Binding.subModelSelectedItem ("Rows", (fun m -> m.SelectedItem), SetSelectedItem)
            "AddRowBefore" |> Binding.cmd (fun _ -> AddRowBefore)
            "AddRowAfter" |> Binding.cmd (fun _ -> AddRowAfter)
            "DeleteSelectedRow" |> Binding.cmdIf ((fun _ -> DeleteSelectedRow), (fun m -> m.SelectedItem.IsSome))
            "WhenDropRecord" |> Binding.cmdParam (fun (o: obj) ->
                match o with
                | :? DevExpress.Xpf.Core.DropRecordEventArgs as a ->
                    a.Handled <- true
                    let targetRow = a.TargetRecord |> ElmishWpfHack.peekViewModel<Model, SorterRow> |> Option.map snd |> Option.map (fun tr -> tr.Guid)
                    match targetRow with
                    | Some targetRow ->
                        match a.DropPosition with
                        | DevExpress.Xpf.Core.DropPosition.Before -> DropBefore targetRow
                        | DevExpress.Xpf.Core.DropPosition.After -> DropAfter targetRow
                        | DevExpress.Xpf.Core.DropPosition.Append -> NoDropTarget
                        | DevExpress.Xpf.Core.DropPosition.Inside -> NoDropTarget
                        | _ -> NoDropTarget
                    | None -> NoDropTarget
                | _ -> NoDropTarget
                |> DropRecord)
            "WhenCompleteRecordDragDrop" |> Binding.cmdParam (fun (o: obj) ->
                match o with
                | :? DevExpress.Xpf.Core.CompleteRecordDragDropEventArgs as a ->
                    a.Handled <- true // Stop component from handling the reorganization of the items itself. We must do it.
                    if a.Canceled then
                        CompleteRecordDragDrop []
                    elif a.Effects = Windows.DragDropEffects.None then // Workaround for apparent bug.
                        CompleteRecordDragDrop []
                    else
                        let records = a.Records |> Seq.map ElmishWpfHack.peekViewModel<Model, SorterRow> |> Seq.choose id |> Seq.map snd |> Seq.map (fun x -> x.Guid) |> Seq.toList
                        CompleteRecordDragDrop records
                | _ -> CompleteRecordDragDrop [])

and in the corresponding XAML

            <dxg:GridControl x:Name="gridControl" MaxHeight="1000" ItemsSource="{Binding Rows}" SelectedItem="{Binding SelectedItem}" SelectionMode="None" ShowBorder="False">
                <dxg:GridControl.View>
                    <dxg:TableView AllowPerPixelScrolling="True" ShowGroupPanel="False" ShowIndicator="True" AllowDragDrop="True" ShowDragDropHint="False" AllowColumnFiltering="False" AllowSorting="False">
                        <dxmvvm:Interaction.Behaviors>
                            <dxmvvm:EventToCommand EventName="DropRecord" Command="{Binding WhenDropRecord}" PassEventArgsToCommand="True" UseDispatcher="True"/>
                            <dxmvvm:EventToCommand EventName="CompleteRecordDragDrop" Command="{Binding WhenCompleteRecordDragDrop}" PassEventArgsToCommand="True" UseDispatcher="True"/>
                        </dxmvvm:Interaction.Behaviors>
                    </dxg:TableView>
                </dxg:GridControl.View>
                <dxg:GridControl.SortInfo>
                    <dxg:GridSortInfo FieldName="Nr" SortOrder="Ascending" />
                </dxg:GridControl.SortInfo>
                <dxg:GridControl.Columns>
                    <dxg:GridColumn AllowEditing="true" Binding="{Binding Register, Mode=TwoWay}" Header="Register" Width="70"/>
                    <dxg:GridColumn AllowEditing="true" Binding="{Binding Knapp, Mode=TwoWay}" Header="Knapp" Width="70"/>
                    <dxg:GridColumn AllowEditing="true" Binding="{Binding Lengde1, Mode=TwoWay}" Header="Lengde 1" Width="70"/>
                    <dxg:GridColumn AllowEditing="true" Binding="{Binding Lengde2, Mode=TwoWay}" Header="Lengde 2" Width="70"/>
                    <dxg:GridColumn AllowEditing="true" Binding="{Binding Lengde3, Mode=TwoWay}" Header="Lengde 3" Width="70"/>
                    <dxg:GridColumn AllowEditing="true" Binding="{Binding FuktMin, Mode=TwoWay}" Header="Fukt min." Width="70"/>
                    <dxg:GridColumn AllowEditing="true" Binding="{Binding FuktMaks, Mode=TwoWay}" Header="Fukt maks." Width="70"/>
                </dxg:GridControl.Columns>
            </dxg:GridControl>

And that was all that was left of these hacks in my code. I had at least ten times more of them long ago.

It's not like I believe this is going to solve this issue. I just post it for what it's worth.

I do have some more code that uses dxg:GridControl, but nothing that I believe can enlighten us with regards to this issue. As already hinted at, I try not to use these complex controls. Instead I've become more of an expert on building stuff from the ground up with the basic WPF controls, and that tends to be a lot more pleasant. It's also surprising how far it gets me. And how much more performant it is, probably only because then I know exactly what I'm doing, so I do it right.

TysonMN commented 2 years ago

Thanks for sharing all this code @BentTranberg.

Do you think you could also share in a small GitHub repo so I could see it in action?

TysonMN commented 2 years ago

Or don't bother if yo don't really use these hacks anymore.

minewarriorsSchool commented 2 years ago

OK so I see that my issue was still open. It can be closed. The way a person could pass xaml objects to the elmish loop would be to use the "passEventArgsToCommand" on an event or use the "CommandParameter" to customize it.