C# + WinUI 3 + MVVM Toolkit (Enter like Tab Key to Move Focus on Next Element)

Quelli che seguono sono degli esempi su come implementare quanto descritto nel titolo in pratica.

In altri termini: un’App Windows che usa WinUI 3 come interfaccia, scritta in C#, che sfrutta il MVVM Toolkit .

WinUI 3

Si tratta sostanzialmente della Libreria Interfaccia per Windows 10/11

C#

È uno dei linguaggi di programmazione (oltre a C++ e Visual Basic) che si possono usare in .NET .

MVVM Toolkit

È una libreria che consente di usare il “pattern MVVM” usando dei “tag” che “scrivono il codice al posto tuo”, una roba “magica”, a capo della quale c’è pure un italiano molto bravo.

Ebbene, mettendo tutto insieme, volevo fare in modo che si potesse passare da un controllo all’altro premendo il tasto INVIO e non solo con il TAB. Inoltre volevo che il form venisse validato a seguito dell’invio, nonché l’interfaccia si aggiornasse durante la digitazione (e tutto questo lo fa MVVM Toolkit, che scrive una marea di codice sotto le quinte, in tempo reale, mentre uno scrive il proprio codice !).

Dato che prevedo mi servirà questa cosa in futuro, me la scrivo qui sinteticamente, ma che possa aiutare anche altri disgraziati come me.

La soluzione si chiama “WinUITest” e l’ho realizzata partendo da “Blank App Packaged (WinUI 3 in Desktop)” (io ho Visual Studio Community, configurato per usare la lingua inglese … mi agevola a trovare soluzione ad eventuali messaggi di errore).

Partiamo da MainWindow.xaml, che rappresenta l’interfaccia dell’App, molto semplice, due campi di immissione testo ed un tasto per inviare il form:

<Window
    x:Class="WinUITest.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="using:WinUITest"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <StackPanel
            Grid.Column="0"
            Margin="20"
            Spacing="20">
            <TextBox
                x:Name="NomeTextBox"
                KeyDown="EnterKeyEventManager"
                PlaceholderText="Nome"
                Text="{x:Bind ViewModel.FirstName, Mode=TwoWay}" />
            <TextBox
                x:Name="CognomeTextBox"
                KeyDown="EnterKeyEventManager"
                PlaceholderText="Cognome"
                Text="{x:Bind ViewModel.LastName, Mode=TwoWay}" />
            <TextBlock Text="{x:Bind ViewModel.FullName, Mode=OneWay}" />

            <Button Click="{x:Bind Submit}" Content="Invia" />
        </StackPanel>
    </Grid>

</Window>

Quindi il relativo MainWindow.xaml.cs, che rappresenta il codice di controllo:

using System.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using WinUITest.Services;
using WinUITest.ViewModel;

// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.

namespace WinUITest;

/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
[ObservableObject]
public sealed partial class MainWindow : Window
{

    public ValidationWithServiceModel ViewModel
    {
        get; 
    }

    public MainWindow()
    {
        InitializeComponent();

        FancyService service = new();
        ViewModel = new(service);
    }

    [RelayCommand]
    public void Submit()
    {
        ViewModel.Submit();

        NomeTextBox.Focus(FocusState.Programmatic);
    }

    private void EnterKeyEventManager(object sender, KeyRoutedEventArgs e)
    {
        if (e.Key == Windows.System.VirtualKey.Enter)
        {
            try
            {
                FindNextElementOptions options = new()
                {
                    SearchRoot = (DependencyObject)((Control)sender).XamlRoot.Content
                };
                
                Debug.WriteLine($"{sender} = {FocusManager.TryMoveFocus(FocusNavigationDirection.Next, options)}");
            }
            catch (System.Exception ex)
            {
                Debug.WriteLine($"ERROR: {ex.Message}");
            }
        }
    }
}

Infine abbiamo il “modello”, rappresentato da ValidationWithServiceModel.cs:

using System;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using WinUITest.Service;

namespace WinUITest.ViewModel;

public partial class ValidationWithServiceModel : ObservableValidator
{
    private readonly IFancyService service;

    public ValidationWithServiceModel(IFancyService service)
    {
        this.service = service;
    }

    #region "Validation"
    public static ValidationResult ValidateName(string name, ValidationContext context)
    {
        var isValid = ((ValidationWithServiceModel)context.ObjectInstance).service.Validate(name);

        if (isValid)
        {
            return ValidationResult.Success;
        }

        return new ValidationResult("The name contains invalid characters");
    }
    #endregion

    #region "Properties"
    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(FullName))]
    [CustomValidation(typeof(ValidationWithServiceModel), nameof(ValidateName))]
    private string firstName = string.Empty;

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(FullName))]
    private string lastName = string.Empty;

    public string FullName => $"{FirstName} {LastName}";
    #endregion

    public void Submit()
    {
        ValidateAllProperties();

        if (HasErrors)
        {
            Debug.WriteLine($"[E] ERROR: {string.Join(Environment.NewLine, GetErrors().Select(e => e.ErrorMessage))}");
        }
        else
        {
            Debug.WriteLine($"[I] SUBMITTED: {FullName}");

            FirstName = string.Empty;
            LastName = string.Empty;
        }
    }
}

Per eseguire la validazione del form viene utilizzato un servizio creato ad-hoc e rappresentato dall’Interfaccia IFancyService:

namespace WinUITest.Service;

public interface IFancyService
{
    bool Validate(string name);
}

Nel mio caso custodita nel file Contracts\Services\IFancyService.cs e dalla sua implementazione Services\FancyService.cs:

using System.Text.RegularExpressions;
using WinUITest.Service;

namespace WinUITest.Services;

public class FancyService : IFancyService
{
    public bool Validate(string name)
    {
        return Regex.IsMatch(name, @"^[A-Za-z ]+$");
    }
}

Sia chiaro: il codice di cui sopra è frutto di estenuanti ricerche, video vari ed è copiato a destra e manca da tutte queste fonti.

Purtroppo non ho – ancora, nel momento in cui scrivo – un tutoria decente da seguire su tutta la faccenda, lo sbattimento è ancora grande, soprattutto perché non c’è un’interfaccia per creare rapidamente la UI, bisogna scriversi tutto a mano e vedere i cambiamenti run-time, non il massimo.

Autore: BuDuS

Disegnatore Meccanico, con la passione per l'Informatica. Come hobby, produco piccoli software di (in)utilità.