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:
- Create a new MAUI project in Visual Studio 2022.
- Add the following nuget package to the project: "CommunityToolkit.Maui" version 7.0.0.
- Modify MauiProgram.cs.
- Add a XAML-only View to the project, i.e., a XAML file without a backend CS file.
- Add a View Model to the project.
- 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>
Focus()on theEntryin theOnAppearing()override or add a delegate to your ViewModel and set that from the code behind. The delegate could then be used to invokeFocus()without the ViewModel ever knowing what it called.