Search code examples
mvvmmauimaui-community-toolkitmaui-androidcontenttemplate

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.


Solution

  • 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;
        }
    }