ledragon
ledragon

Reputation: 301

Maui (Android): Bind Editor/Entry in ContentTemplate to Relay Command on ViewModel using Behaviours

HI I have a Maui app using ContentTemplates where I'm trying to bind the Unfocused event of an Editor to a RelayCommand on the Viewmodel.

I've managed to bind a button click to a RelayCommand but the Editor Unfocused command won't trigger.

FYI I've implemented the following solution to solve the issue with Editors not unfocusing on clicking outside of the control:

https://github.com/dotnet/maui/issues/21053

Sample repo here where the functionality is in MainPage:

Repo

Any help much appreciated.

Upvotes: 0

Views: 217

Answers (2)

Liqun Shen-MSFT
Liqun Shen-MSFT

Reputation: 8260

You want to use TemplateBinding for the ControlTemplate. But the tricky thing is that we cannot use RelativeBinding or TemplateBinding in Behavoirs. You may use x:reference Binding expression instead, see this similar thread, How to use UserStoppedTypingBehavior from ControlTemplate?.

As a workaround, I made some changes to your code, in ControlTemplates,

        <Editor x:Name="myeditor"  BindingContext="{Binding Source={RelativeSource TemplatedParent}}"
            Grid.Row="1"
            AutoSize="TextChanges"
            Text="{Binding  BindingContext.Response,Source={RelativeSource TemplatedParent}}">
            <Editor.Behaviors>
                <!--<toolkit:EventToCommandBehavior Command="{Binding Source={RelativeSource TemplatedParent}, Path=EditorUnfocusedRelayCommand}" EventName="Unfocused" />-->
                <!--<toolkit:EventToCommandBehavior
                    Command="{Binding Source={RelativeSource AncestorType={x:Type viewModel:MainPageViewModel}}, Path=EditorUnfocusedRelayCommand}"
                    CommandParameter="{Binding .}"
                    EventName="Unfocused" />-->
                <toolkit:EventToCommandBehavior
                    Command="{Binding Source={x:Reference myeditor}, Path=BindingContext.EditorUnfocusedCommand}"
                    CommandParameter="{Binding BindingContext.BindingContext,Source={x:Reference myeditor}}"
                    EventName="Unfocused" />
            </Editor.Behaviors>
        </Editor>

Please note that you have both EditorUnfocusedCommand and EditorUnfocusedRelayCommand in viewmodel. Here I use EditorUnfocusedCommand for debugging.

It seems complex though works on my side. Also, the tricky thing is that you may use different Binding Source in an Editor control.

First is the Text Property, we want to bind it to the ContentTemplateDto Item which is set as the BindingContext for the EditorContentView.

Second is the Command property, we want to set it to the value of EditorUnfocusedCommand in EditorContentView.

To achieve this, we set the BinidngContext of the Editor control to the TemplatedParent which is EditorContentView. For the Command Property, we bind it to the EditorUnfocusedCommand in EditorContentView while for the CommandParameter Property, we should bind it to the the EditorContentView's BindingContext, which is set as the ContentTemplateDto Item in MainPage.cs.

Upvotes: 0

Stephen Quan
Stephen Quan

Reputation: 26274

When working with ContentView, it's best to focus on Commands or Properties. If you want to map an Event to a Command, there's extra work involved, but it is doable. However, since the Editor has an IsFocused property, we can declare a BindableProperty called IsEditorFocused and use a OneWayBindingToSource to propagate this event from the Editor through the ContentView to the Page.

<!-- CustomEditor.xaml -->
<ContentView
    x:Class="Maui.StackOverflow.CustomEditor"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
    <ContentView.ControlTemplate>
        <ControlTemplate>
            <Border>
                <Editor IsFocused="{TemplateBinding IsEditorFocused, Mode=OneWayToSource}" Placeholder="CustomEntry" />
            </Border>
        </ControlTemplate>
    </ContentView.ControlTemplate>
</ContentView>
// CustomEditor.xaml.cs
namespace Maui.StackOverflow;

public partial class CustomEditor : ContentView
{
    public static readonly BindableProperty IsEditorFocusedProperty = BindableProperty.Create(nameof(IsEditorFocused), typeof(bool), typeof(CustomEditor), false);
    public bool IsEditorFocused
    {
        get => (bool)GetValue(IsFocusedProperty);
        set => SetValue(IsFocusedProperty, value);
    }

    public CustomEditor()
    {
        InitializeComponent();
    }
}

And we can consume it on our page as follows:

<!-- EditorPage.xaml -->
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
    x:Class="Maui.StackOverflow.EditorPage"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:Maui.StackOverflow"
    x:Name="thisPage"
    Title="EditorPage"
    x:DataType="local:EditorPage">
    <VerticalStackLayout>
        <Entry Placeholder="Entry1" />
        <local:CustomEditor IsEditorFocused="{Binding IsEditorFocused, Mode=OneWayToSource, Source={Reference thisPage}}" />
        <Entry Placeholder="Entry2" />
    </VerticalStackLayout>
</ContentPage>
// EditorPage.xaml.cs
using System.Diagnostics;

namespace Maui.StackOverflow;

public partial class EditorPage : ContentPage
{
    public static readonly BindableProperty IsEditorFocusedProperty = BindableProperty.Create(nameof(IsEditorFocused), typeof(bool), typeof(EditorPage), false);
    public bool IsEditorFocused
    {
        get => (bool)GetValue(IsEditorFocusedProperty);
        set => SetValue(IsEditorFocusedProperty, value);
    }

    public EditorPage()
    {
        InitializeComponent();

        BindingContext = this;

        PropertyChanged += (s, e) =>
        {
            switch (e.PropertyName)
            {
                case nameof(IsEditorFocused):
                    Debug.WriteLine($"IsEditorFocused changed to {IsEditorFocused}");
                    break;
            }
        };
    }
}

CustomEntry.gif

Now, I mentioned it was possible to convert Events to Commands. The pattern I follow is I declare a thin wrapper to the thing that I need commands on. In this case, I will declare CustomEditorInner which is basically a wrapper on Editor where Focused and Unfocused events are converted to ICommands. Then, I can rewrite CustomEditor and my ContentPage to use it. On the ContentPage is where I declare the RelayCommand.

// CustomEditorInner.cs
using System.Windows.Input;

namespace Maui.StackOverflow;

class CustomEditorInner : Editor
{
    public static readonly BindableProperty FocusedCommandProperty = BindableProperty.Create(nameof(FocusedCommand), typeof(ICommand), typeof(CustomEditorInner), null);
    public ICommand FocusedCommand
    {
        get => (ICommand)GetValue(FocusedCommandProperty);
        set => SetValue(FocusedCommandProperty, value);
    }

    public static readonly BindableProperty UnfocusedCommandProperty = BindableProperty.Create(nameof(UnfocusedCommand), typeof(ICommand), typeof(CustomEditorInner), null);
    public ICommand UnfocusedCommand
    {
        get => (ICommand)GetValue(UnfocusedCommandProperty);
        set => SetValue(UnfocusedCommandProperty, value);
    }

    public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create(nameof(CommandParameter), typeof(object), typeof(CustomEditorInner), null);
    public object CommandParameter
    {
        get => GetValue(CommandParameterProperty);
        set => SetValue(CommandParameterProperty, value);
    }

    public CustomEditorInner()
    {
        this.Focused += (s,e) => FocusedCommand?.Execute(CommandParameter);
        this.Unfocused += (s,e) => UnfocusedCommand?.Execute(CommandParameter);
    }
}
<!-- CustomEditor.xaml -->
<?xml version="1.0" encoding="utf-8" ?>
<ContentView
    x:Class="Maui.StackOverflow.CustomEditor"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:Maui.StackOverflow">
    <ContentView.ControlTemplate>
        <ControlTemplate>
            <Border>
                <local:CustomEditorInner
        CommandParameter="{TemplateBinding CommandParameter}"
        FocusedCommand="{TemplateBinding FocusedCommand}"
        Placeholder="CustomEditor"
        UnfocusedCommand="{TemplateBinding UnfocusedCommand}" />
            </Border>
        </ControlTemplate>
    </ContentView.ControlTemplate>
</ContentView>
// CustomEditor.xaml.cs
using System.Windows.Input;

namespace Maui.StackOverflow;

public partial class CustomEditor : ContentView
{
    public static readonly BindableProperty FocusedCommandProperty = BindableProperty.Create(nameof(FocusedCommand), typeof(ICommand), typeof(CustomEditor), null);
    public ICommand FocusedCommand
    {
        get => (ICommand)GetValue(FocusedCommandProperty);
        set => SetValue(FocusedCommandProperty, value);
    }

    public static readonly BindableProperty UnfocusedCommandProperty = BindableProperty.Create(nameof(UnfocusedCommand), typeof(ICommand), typeof(CustomEditor), null);
    public ICommand UnfocusedCommand
    {
        get => (ICommand)GetValue(UnfocusedCommandProperty);
        set => SetValue(UnfocusedCommandProperty, value);
    }

    public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create(nameof(CommandParameter), typeof(object), typeof(CustomEditor), null);
    public object CommandParameter
    {
        get => GetValue(CommandParameterProperty);
        set => SetValue(CommandParameterProperty, value);
    }

    public CustomEditor()
    {
        InitializeComponent();
    }
}
<!-- EditorPage.xaml -->
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
    x:Class="Maui.StackOverflow.EditorPage"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:Maui.StackOverflow"
    x:Name="thisPage"
    Title="EditorPage"
    x:DataType="local:EditorPage">

    <VerticalStackLayout Spacing="20">
        <Entry Placeholder="Entry1" />
        <local:CustomEditor
            CommandParameter="{Binding Magic}"
            FocusedCommand="{Binding MyFocusedCommand}"
            UnfocusedCommand="{Binding MyUnfocusedCommand}" />
        <Entry Placeholder="Entry2" />
    </VerticalStackLayout>

</ContentPage>
// EditorPage.xaml.cs
using System.Diagnostics;
using CommunityToolkit.Mvvm.Input;

namespace Maui.StackOverflow;

public partial class EditorPage : ContentPage
{
    [RelayCommand]
    void MyFocused(int magic)
    {
        Debug.WriteLine($"OnFocused magic:{magic}");
    }

    [RelayCommand]
    void MyUnfocused(int magic)
    {
        Debug.WriteLine($"OnUnfocused magic:{magic}");
    }

    public int Magic { get; set; } = 42;

    public EditorPage()
    {
        InitializeComponent();

        BindingContext = this;
    }
}

Upvotes: 1

Related Questions