5

I have a View with an entry control and two buttons in a .NET MAUI MVVM project. When the View loads the entry control is not focused. How can I set the focus to the entry control without adding functions to the code behind? I.e. the code behind for the ContentPage just links the ViewModel to the BindingContext.

As far as I am aware the form controls should not be accessed directly from the ViewModel, so is it possible to set the focus to the entry control by some other means when the form loads?

1
  • You won't get around adding some code in the code behind file. There's no way around that if you don't want to break MVVM. Therefore, I suggest to either call Focus() on the Entry in the OnAppearing() override or add a delegate to your ViewModel and set that from the code behind. The delegate could then be used to invoke Focus() without the ViewModel ever knowing what it called. Commented Jan 9, 2023 at 17:19

3 Answers 3

7

The only way that I could get this to work was to put in that xaml for my intended control for focus:

Loaded="OnEntryLoaded"

And then in the code behind:

private void OnEntryLoaded(object sender, EventArgs e)
{
    Name.Focus();
}
Sign up to request clarification or add additional context in comments.

Comments

1

We cannot set the Entry focus in ViewModel without any code in .cs as you mentioned in the question.

The easiest way is set the focus in OnAppearing method (allows users to customize behavior immediately prior to the Page becoming visible):

protected override void OnAppearing()
{
    base.OnAppearing();
    Task.Run(() =>
    {
        while (!myentry.IsVisible)
        {              
            Task.Delay(10).Wait();
        }
        Application.Current.Dispatcher.Dispatch(() =>
        {
            myentry.Focus();
        });
    });
}  

Hope it works for you.

5 Comments

I tried your suggestion but it seems that the focus is being called before the page has been loaded, hence having no effect. To get around this I have had to add a delay to give the view more time to load before setting the focus. I.e. await Task.Delay(500)
That seems because the main ui thread instead of the time delay. I have updated my answer and you could have a try.
Thanks for the update, unfortunately this didn't work for me so still having to use the await Task.Delay(500) method.
It work for me using the default template. Maybe your UI is much more complicated. Then I think that may because the entry was not visible when set it focus. However, delay 500 is too time-consuming. So i update my answer, have another try in order to reduce the waiting time.
I suspect that my UI is more complicated and probably takes a bit more time to load. I tried your revised solution but it didn't seem to work as the control was visible immediately and never hit the Task.Delay section. I have however made slight change that checks for !myEntry.IsLoaded instead of !myentry.IsVisible which seems to work!! Thanks.
0

Intro/Context:

I try to restrict references to Views to only within the View (xaml) itself. Ideally this means I don't have any x:Name="Whatever" in the xaml unless a View within that same xaml references another View within the xaml, which will be what is seen below. One benefit of this is I can, for example, reduce a parent View with 20 child Views to a single child View for the sake of troubleshooting and focusing my attention on that single View, and then any property changes are because of that View and not any of the other 19 Views. Additionally, if I need to make a View have different behavior in Windows versus iOS, then not having these references makes that easier if I decide to make large changes.

Solution:

The problem you have posed is solvable without any backend code. Here is an outline of the setup steps:

  1. Create a new MAUI project in Visual Studio 2022.
  2. Add the following nuget package to the project: "CommunityToolkit.Maui" version 7.0.0.
  3. Modify MauiProgram.cs.
  4. Add a XAML-only View to the project, i.e., a XAML file without a backend CS file.
  5. Add a View Model to the project.
  6. Modify the AppShell.xaml file.

I'll assume steps 1 and 2 are straightforward. (I have been using "CommunityToolkit.Maui" version 7.0.0 without any issues in my .NET 8 project, but when I tried adding the most recent version Visual Studio complained about the Android context, so that's why I went back to version 7 of the toolkit. It's trivial to this discussion.)

After step 3, this is what MauiProgram.cs should look like (the main point of interest is ".UseMauiCommunityToolkit()" and its associated using directive):

using CommunityToolkit.Maui;
using Microsoft.Extensions.Logging;

namespace FocusExperiment
{
    public static class MauiProgram
    {
        public static MauiApp CreateMauiApp()
        {
            var builder = MauiApp.CreateBuilder();
            builder
                .UseMauiApp<App>()
                // Initialize the .NET MAUI Community Toolkit by adding the below line of code
                .UseMauiCommunityToolkit()
                .ConfigureFonts(fonts =>
                {
                    fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                    fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
                });

#if DEBUG
            builder.Logging.AddDebug();
#endif

            return builder.Build();
        }
    }
}

Regarding step 4, I chose to delete the MainPage and add only a new XAML file for the sake of emphasizing that there is no backend code of a View involved. I simply add a new XAML ContentPage to the project and accept its default name "NewPage1.xaml". It looks like this:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
    xmlns:l="clr-namespace:FocusExperiment"
    x:Class="FocusExperiment.NewPage1"
    Title="NewPage1">

    <ContentPage.BindingContext>
        <l:MyViewModel />
    </ContentPage.BindingContext>

    <ContentPage.Behaviors>
        <toolkit:EventToCommandBehavior
            EventName="Loaded"
            Command="{Binding LoadedCommand}"
            CommandParameter="{x:Reference EntryControl}"
            />
    </ContentPage.Behaviors>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
            <RowDefinition />
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
        <Button
            Grid.Row="0"
            Text="Hello"
            />
        <Button
            Grid.Row="1"
            Text="World"
            />
        <Entry
            Grid.Row="2"
            />
        <!--This one gets the focus:-->
        <Entry
            Grid.Row="3"
            x:Name="EntryControl"
            />
        <Entry
            Grid.Row="4"
            />
    </Grid>

</ContentPage>

I put 3 Entry Views in there, and will put the focus on the middle one. That way if there is any default behavior that puts focus on the 1st or last input control I am not fooled by that.

Take note of the "toolkit:EventToCommandBehavior" node. It is attaching a behavior to the ContentPage. The EventName tells it to listen for the ContentPage's Loaded event. When that event is broadcasted the attached behavior will call the ICommand::Execute method (assuming the ICommand::CanExecute returns true). I am passing the View I want to receive the focus; I do that using the CommandParameter and giving it "{x:Reference EntryControl}". This is where it is okay to have "x:Name=..." as it is not coupled to the backend nor to the View Model.

Also note, the View Model is implicitly created via "<l:MyViewModel />".

Here is the View Model of step 5:

using System.Windows.Input;

namespace FocusExperiment;

public class MyViewModel : BindableObject
{
    private class Loaded : ICommand
    {
        public event EventHandler? CanExecuteChanged;

        private bool _canExecute = true;

        public bool CanExecute(object? parameter)
        {
            if (parameter is bool canExecute && _canExecute != canExecute)
            {
                _canExecute = canExecute;
                CanExecuteChanged?.Invoke(this, EventArgs.Empty);
            }

            return _canExecute;
        }

        public void Execute(object? parameter)
        {
            if (parameter is View view)
            {
                view.Focus();
            }
        }
    }

    public ICommand LoadedCommand
    {
        get { return (ICommand)GetValue(LoadedCommandProperty); }
        set { SetValue(LoadedCommandProperty, value); }
    }

    public static readonly BindableProperty LoadedCommandProperty = BindableProperty.Create(nameof(LoadedCommand)
        , typeof(ICommand), typeof(MyViewModel), new Loaded());
}

Note: the View that is passed via CommandParameter, its Focus method is invoked in the ICommand::Execute method.

Last (step 6), modify the AppShell.xaml to look like:

<?xml version="1.0" encoding="UTF-8" ?>
<Shell
    x:Class="FocusExperiment.AppShell"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:FocusExperiment"
    Shell.FlyoutBehavior="Disabled"
    Title="FocusExperiment">

    <ShellContent
        Title="Home"
        ContentTemplate="{DataTemplate local:NewPage1}"
        Route="NewPage1"
        />

</Shell>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.