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

Binding.TwoWay setter with parameters given from xaml #557

Closed
minewarriorsSchool opened this issue Mar 9, 2023 · 6 comments
Closed

Binding.TwoWay setter with parameters given from xaml #557

minewarriorsSchool opened this issue Mar 9, 2023 · 6 comments

Comments

@minewarriorsSchool
Copy link

minewarriorsSchool commented Mar 9, 2023

So currently I have an interesting usecase. Now that I have separated all my ui and removed it from my model, I need to pass ui objects to the messaged to interact with them. #555

Now I have the following use case for a textbox:

  1. This textBox is a twoWay binding that is used as a setting in my model to show / hide x amount of columns visible in a GridControl UI object from DevExpress.
  2. previously we had the gridControl in our model to easily access it, but this acceptable according to coding conventions
  3. after removing it, we had to change are setter into something like a Binding.cmdParam where we would receive the value and a grid control. This is the part that we have no Idea how to do.

Current Xaml

<TextBox
    Width="auto"
    MinWidth="25"
    HorizontalAlignment="Left"
    Text="{Binding AmountOfTimeColumnsToDisplay, Mode=TwoWay, UpdateSourceTrigger=LostFocus}">
</TextBox>

But we would like to also bind

"{Binding` Path=GridControl, RelativeSource={RelativeSource AncestorType={x:Type `local:Settings}}}"

to it as an extra parameter.

Current binding:

"AmountOfTimeColumnsToDisplay"
|> Binding.twoWay ((fun m -> m.AmountOfColumnsToDisplay.ToString()), convertSetAmountOfColumnsToDisplay)

convertSetAmountOfColumnsToDisplay method:

let convertSetAmountOfColumnsToDisplay (p: obj) =
    try
        //splits p object into two objects of string and gridcontrol
        let args = p :?> string * GridControl
        SetAmountOfColumnsToDisplay args
    with
    | e ->
        Serilog.Log.Logger.Information(e.StackTrace)
        UpdateUi

SetAmountOfColumnsToDisplay args command:

| SetAmountOfColumnsToDisplay (x, gridControl) ->
    let result, value = System.Int32.TryParse(x.ToString())
  
    match result with
    | false -> { m with AmountOfColumnsToDisplay = 12 }, Cmd.ofMsg CmdNone
    | true ->
        let newModel =
            match value with
            | value when value > ColumnInformation.getAmountOfTimeColumsMax () ->
                { m with AmountOfColumnsToDisplay = ColumnInformation.getAmountOfTimeColumsMax () }
  
            | value when value < 6 -> { m with AmountOfColumnsToDisplay = 6 }
            | _ -> { m with AmountOfColumnsToDisplay = value }
  
        { newModel with ColumnVisibilityAmountChanged = true },
        Cmd.ofMsg (PreviewAmountOfColumnsToDisplay(false, m.Model.IsResourceViewActive, gridControl))

Hopefully you guys would have a suggestion on how we would go about and solve this.
This is one of the last steps into making my application pure without ui in the models.

I have tried multibindings, but I got stuck on that part, since a multibinding for the "textbox.text" would not trigger the setter of the elmish loop and would just stay within the converter itself.

@LyndonGingerich
Copy link
Contributor

LyndonGingerich commented Mar 9, 2023

let convertSetAmountOfColumnsToDisplay (p: obj) = try //splits p object into two objects of string and gridcontrol let args = p :?> string * GridControl SetAmountOfColumnsToDisplay args with | e -> Serilog.Log.Logger.Information(e.StackTrace) UpdateUi

Could you format this with line breaks for readability?

If I understand correctly, you pass the GridControl as a parameter to use in your update function because you don't want it on the model. What about the GridControl do you use? Could you store that property on the model with a separate binding (OneWayToSource, even)?

@minewarriorsSchool
Copy link
Author

let convertSetAmountOfColumnsToDisplay (p: obj) = try //splits p object into two objects of string and gridcontrol let args = p :?> string * GridControl SetAmountOfColumnsToDisplay args with | e -> Serilog.Log.Logger.Information(e.StackTrace) UpdateUi

Could you format this with line breaks for readability?

If I understand correctly, you pass the GridControl as a parameter to use in your update function because you don't want it on the model. What about the GridControl do you use? Could you store that property on the model with a separate binding (OneWayToSource, even)?

Hey @LyndonGingerich , sorry for the formatation. I did not see I forgot to add the language etc which caused weird formatting.

Currently I do not pass my xaml GridControl YET, but I want to pass it. with that twoway TextBox.Text setter.
I am not able to store it for the way I am currently having my MainView:

<dx:ThemedWindow
    x:Class="ResourcePlanner.View.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:converters="clr-namespace:Resource.GUI.Converters;assembly=ResourcePlanner.GUI"
    xmlns:dx="http://schemas.devexpress.com/winfx/2008/xaml/core"
    xmlns:dxmvvm="http://schemas.devexpress.com/winfx/2008/xaml/mvvm"
    xmlns:dxwui="http://schemas.devexpress.com/winfx/2008/xaml/windowsui"
    xmlns:dxwuin="http://schemas.devexpress.com/winfx/2008/xaml/windowsui/navigation"
    xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
    xmlns:templates="clr-namespace:ResourcePlanner.View.Resources"
    Title="{Binding mainWindowTitle}"
    Width="auto"
    Height="auto">
    <dx:ThemedWindow.Resources>
        <converters:NavigationServiceAndDataContext x:Key="NavigationServiceAndDataContext" />
    </dx:ThemedWindow.Resources>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Closing">
            <i:InvokeCommandAction Command="{Binding CloseMainApplication}" PassEventArgsToCommand="True" />
        </i:EventTrigger>
        <i:EventTrigger EventName="ScrollChanged">
            <i:InvokeCommandAction Command="{Binding UpdateUi}" PassEventArgsToCommand="True" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <DockPanel>
        <Grid>
            <dxmvvm:Interaction.Behaviors>
                <dxmvvm:KeyToCommand
                    Command="{Binding ToggleDeveloperMode}"
                    EventName="PreviewKeyUp"
                    KeyGesture="Control + Alt + D" />
            </dxmvvm:Interaction.Behaviors>
            <Grid.RowDefinitions>
                <RowDefinition Height="auto" />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <templates:Ribbon
                Grid.Row="0"
                FilterProjectsCommand="{Binding ElementName=ViewNavigationFrame, Path=Content.FilterProjectsCommand, UpdateSourceTrigger=PropertyChanged}"
                GridControl="{Binding ElementName=ViewNavigationFrame, Path=Content.GridControlProperty, UpdateSourceTrigger=PropertyChanged}"
                INavigationFrameService="{Binding ElementName=FrameNavigationService, Path=., UpdateSourceTrigger=PropertyChanged}"
                NavigationFrame="{Binding ElementName=ViewNavigationFrame, Path=., UpdateSourceTrigger=PropertyChanged}" />
            <templates:ResourcePlannerProjectView
                x:Name="ResourcePlannerResourceView"
                Grid.Row="1"
                Visibility="Collapsed" />
            <dxwui:NavigationFrame
                x:Name="ViewNavigationFrame"
                Grid.Row="1"
                AnimationType="None"
                NavigationCacheMode="Required">
                <dxmvvm:Interaction.Behaviors>
                    <dxwuin:FrameNavigationService x:Name="FrameNavigationService" />
                </dxmvvm:Interaction.Behaviors>
            </dxwui:NavigationFrame>

        </Grid>
    </DockPanel>
</dx:ThemedWindow>

In the navigationFrame from DevExpress we can load in usercontrols on runtime and cache them to switch instantly between different usercontrols.
The gridControl on these userControls that I pass to my ribbon I do with:

GridControl="{Binding ElementName=ViewNavigationFrame, Path=Content.GridControlProperty, UpdateSourceTrigger=PropertyChanged}"

In which I have that textbox of which I want to access that gridControl.

In short, the gridControl changes dynamically depending on the current usercontrol loaded in by the navigationFrame and I am not able to bind it.

Unless you have another idea?

Hopefully this all makes sense. Otherwise I will try to make a test project to make it more clear <3

Kind regards,

Jelle

@LyndonGingerich
Copy link
Contributor

LyndonGingerich commented Mar 10, 2023

I think I'm starting to understand, but I think my question was unclear. Here's what I meant: Why did you have your GridControl on the model? What properties of GridControl was update using? Storing those properties (but not the whole GridControl) on the model would be an optimal solution, if it would in fact solve your problem.

@marner2
Copy link
Collaborator

marner2 commented Mar 10, 2023

I think you're going to have to have some code-behind in order to solve this properly. In our project, we have the constraint that our messages have to be fully serializable, which forces all of the logic that you are pushing into the elmish Cmd handler for PreviewAmountOfColumnsToDisplay into the actual code behind. We've taken that tradeoff under the concept that such logic is fundamentally "view logic" or has to do with details of the control construction and not the "app logic" that is specific to your app.

In some cases, that means we have a WPF control that doesn't expose the right bindings to interop well with Elmish.WPF, so we have to reframe them into a different binding structure using Code-Behind.

@minewarriorsSchool
Copy link
Author

@LyndonGingerich and @marner2 , your two answers made me remember a potential solution for this issue.
I am going to try something and if it was successful, I will post it here so it could be a potential answer for others also.

@minewarriorsSchool
Copy link
Author

So After the comments you guys left @LyndonGingerich and @marner2 , I managed to actually build an efficient working solution making use of NO code behind actually, but I did make use of DevExpress behaviors. So this solution is only possible in THIS way if you have DevExpress.

So as @marner2 suggested there probably would be needed some supporting code because of the way Elmish is setup.
The way we did this is by making 2 separate behaviors and attaching these to the textbox for the getter and the setter.

Previously our textbox xaml would look like:

<TextBox
    Width="auto"
    MinWidth="25"
    HorizontalAlignment="Left"
    Text="{Binding AmountOfTimeColumnsToDisplay, Mode=TwoWay, UpdateSourceTrigger=LostFocus}">
</TextBox>

And now it looks like the following

<TextBox
            Width="auto"
            MinWidth="25"
            HorizontalAlignment="Left">
            <dxmvvm:Interaction.Behaviors>
                <customClasses:AmountOfColumnsToDisplayGetterHelper AmountOfColumnsToDisplay="{Binding AmountOfTimeColumnsToDisplayGetter, UpdateSourceTrigger=PropertyChanged}" />
                <customClasses:AmountOfColumnsToDisplaySetterHelper Command="{Binding AmountOfTimeColumnsToDisplaySetter}" GridControlContext="{Binding Path=GridControl, RelativeSource={RelativeSource AncestorType={x:Type local:Settings}}, UpdateSourceTrigger=PropertyChanged}" />
            </dxmvvm:Interaction.Behaviors>
        </TextBox>

As you can see we removed the binding command AmountOfTimeColumnsToDisplay as substituded it for 2 new bindings
AmountOfTimeColumnsToDisplayGetter
AmountOfTimeColumnsToDisplaySetter

so the changed in the init loop:

          //"AmountOfTimeColumnsToDisplay"
          //|> Binding.twoWay ((fun m -> m.AmountOfColumnsToDisplay.ToString()), convertSetAmountOfColumnsToDisplay)
          "AmountOfTimeColumnsToDisplayGetter"
          |> Binding.oneWay (fun m -> m.AmountOfColumnsToDisplay.ToString())
          "AmountOfTimeColumnsToDisplaySetter"
          |> Binding.cmdParam convertSetAmountOfColumnsToDisplay

the actual code created for these behaviors is the following:

    /// <summary>
    /// This class is used to set the textValue of a textBox when the textBox is fully rendered and visible to the user
    /// It will retrieve the value from the DataContext of the TextBox binded to elmish.wpf model
    /// "AmountOfTimeColumnsToDisplayGetter" |> Binding.oneWay(fun m -> m.AmountOfColumnsToDisplay.ToString())
    /// </summary>
    public class AmountOfColumnsToDisplayGetterHelper : Behavior<TextBox>
    {
        public static readonly DependencyProperty AmountOfColumnsToDisplayProperty =
            DependencyProperty.Register("AmountOfColumnsToDisplay", typeof(int), typeof(AmountOfColumnsToDisplayGetterHelper), new PropertyMetadata());

        public int AmountOfColumnsToDisplay
        {
            get { return (int)GetValue(AmountOfColumnsToDisplayProperty); }
            set { SetValue(AmountOfColumnsToDisplayProperty, value); }
        }

        protected override void OnAttached()
        {
            base.OnAttached();
            this.AssociatedObject.Loaded += AssociatedObject_Loaded;
        }

        private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
        {
            this.AssociatedObject.Text = AmountOfColumnsToDisplay.ToString();
        }

        protected override void OnDetaching()
        {
            this.AssociatedObject.Loaded -= AssociatedObject_Loaded;
            base.OnDetaching();
        }
    }

    /// <summary>
    /// This class is used to execute a command when the textBox lost focus event is triggered
    /// "AmountOfTimeColumnsToDisplaySetter" |> Binding.cmdParam convertSetAmountOfColumnsToDisplay
    /// As command parameters it will pass the textValue of the textBox and custom dependency property DevExpress Xpf GridControl
    /// These parameters should be given with a multibinding in the xaml file
    /// </summary>
    /// 
    public class AmountOfColumnsToDisplaySetterHelper : Behavior<TextBox>
    {
        public static readonly DependencyProperty CommandProperty =
            DependencyProperty.Register("Command", typeof(ICommand), typeof(AmountOfColumnsToDisplaySetterHelper), new PropertyMetadata());

        public ICommand Command
        {
            get { return (ICommand)GetValue(CommandProperty); }
            set { SetValue(CommandProperty, value); }
        }

        public static readonly DependencyProperty GridControlContextProperty =
            DependencyProperty.Register("GridControlContext", typeof(GridControl), typeof(AmountOfColumnsToDisplaySetterHelper), new PropertyMetadata());

        public GridControl GridControlContext
        {
            get { return (GridControl)GetValue(GridControlContextProperty); }
            set { SetValue(GridControlContextProperty, value); }
        }

        protected override void OnAttached()
        {
            base.OnAttached();
            this.AssociatedObject.LostFocus += AssociatedObject_LostFocus;
        }

        private void AssociatedObject_LostFocus(object sender, RoutedEventArgs e)
        {
            if (Command != null)
            {
                //create object array with the textValue of the textBox and the gridControl context
                object[] commandParameter = new object[2];
                commandParameter[0] = this.AssociatedObject.Text;
                commandParameter[1] = GridControlContext;
                Command.Execute(commandParameter);
            }
        }

        protected override void OnDetaching()
        {
            this.AssociatedObject.LostFocus -= AssociatedObject_LostFocus;
            base.OnDetaching();
        }
    }

And to wrap this all up our cmdParam converter method now works also in the following way:

let convertSetAmountOfColumnsToDisplay (p: obj) =
    try
        //splits p array object into two objects of string and gridcontrol
        let args = 
            let objectArray = p :?> obj array
            let stringObject = objectArray.[0] :?> string
            let gridControlObject = objectArray.[1] :?> GridControl
            (stringObject, gridControlObject)
        SetAmountOfColumnsToDisplay args
    with
    | e ->
        Serilog.Log.Logger.Information(e.StackTrace)
        UpdateUi

Thank you all a lot for the brainstorm and if you guys have any questions regarding me solutions, I gladly explain.

Kind regards,

Jelle

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