Ho già trattato di WinUI 3 in un altro articolo (C# + WinUI 3 + MVVM Toolkit (Enter like Tab Key to Move Focus on Next Element)), qui mi soffermo sull’uso di Template Studio e alla possibilità di navigare tra le pagine, un po’ come fosse un sito web.
Template Studio
Rappresenta un Wizard per imbastire un’App in modo semplice tramite Visual Studio (io uso Community, ma andrà bene anche per i fratelli maggiori).
Sia chiaro: non prendete niente per oro colato, quello che scrivo qui è un archivio di soluzioni che ho trovato a problemi che devo affrontare e che voglio custodire per poterle ritrovare, nonché tutta roba che potrebbe pure essere di pessimo esempio. Sta a chi legge valutare il contenuto, anche sulla base di quanto trova altrove (se lo trova …).
Date le doverose premesse, passiamo a vedere come ho risolto la faccenda.
Non sto ad elencare per filo e per segno; sostanzialmente ho creato la base di un’App con Template Studio, che ho chiamato “TemplateStudioForWinUISample“, dove la pagina “Main” era vuota e l’ho riempita con un piccolo form.
Quando invio il form, voglio che venga visualizzata una pagina riepilogativa, tutto qui, anzi, non ho nemmeno implementato la validazione (che comunque ho descritto nell’altro articolo linkato sopra), mi serviva giusto il minimo indispensabile per vedere se funziona e capire il meccanismo di passaggio da una pagina all’altra, decidendo io se i dati devono essere mantenuti o meno (per default vengono persi o mantenuti sempre in base all’impostazione di “NavigationCacheMode = Microsoft.UI.Xaml.Navigation.NavigationCacheMode.Enabled;“).
Sì, sto brancolando nel buio e mi arrangio alla meno peggio, per questo invito a prendere tutto con le pinze !
Iniziamo con TemplateStudioForWinUISample\Views\MainPage.xaml, che rappresenta la UI della pagina Main, inizialmente vuota, quella dove io ci ho messo il form:
<Page
x:Class="TemplateStudioForWinUISample.Views.MainPage"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid x:Name="ContentArea">
<StackPanel Spacing="20">
<TextBox x:Name="TextBoxName" Header="Nome:" Text="{x:Bind ViewModel.Name, Mode=TwoWay}" KeyDown="KeyDownEventHandler" />
<TextBox Header="Nome:" Text="{x:Bind ViewModel.Surname, Mode=TwoWay}" KeyDown="KeyDownEventHandler" />
<Border BorderThickness="1" BorderBrush="Aqua" />
<Button Content="Invia ..." HorizontalAlignment="Right" Click="SubmitButtonEventHandler" />
</StackPanel>
</Grid>
</Page>
La UI viene amministrata da questa classe TemplateStudioForWinUISample\Views\MainPage.xaml.cs (Template Studio usa il pattern MVVM e qui siamo nella View):
using System.Diagnostics;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using TemplateStudioForWinUISample.Contracts.Services;
using TemplateStudioForWinUISample.Contracts.ViewModels;
using TemplateStudioForWinUISample.ViewModels;
namespace TemplateStudioForWinUISample.Views;
public sealed partial class MainPage : Page
{
public MainViewModel ViewModel
{
get;
}
public MainPage()
{
ViewModel = App.GetService<MainViewModel>();
InitializeComponent();
NavigationCacheMode = Microsoft.UI.Xaml.Navigation.NavigationCacheMode.Enabled;
Loaded += MainPage_Loaded;
}
private void MainPage_Loaded(object sender, RoutedEventArgs e) {
TextBoxName.Focus(FocusState.Programmatic);
}
/// <summary>
/// Gestisce la pressione del "Return" sui campi del form, affinché sia come premere TAB.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <exception cref="ArgumentNullException"></exception>
private void KeyDownEventHandler(object sender, KeyRoutedEventArgs e)
{
if (sender is null)
{
throw new ArgumentNullException(nameof(sender));
}
if (e is null)
{
throw new ArgumentNullException(nameof(e));
}
if (e.Key == Windows.System.VirtualKey.Enter)
{
try
{
FindNextElementOptions options = new()
{
SearchRoot = (DependencyObject)((Control)sender).XamlRoot.Content
};
FocusManager.TryMoveFocus(FocusNavigationDirection.Next, options);
}
catch (Exception ex)
{
Debug.WriteLine($"ERRORE: {ex.Message}");
}
}
}
/// <summary>
/// Gestione dell'Invio del Form: passa alla pagina di riepilogo.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <exception cref="ArgumentNullException"></exception>
private void SubmitButtonEventHandler(object sender, RoutedEventArgs e)
{
if (sender is null)
{
throw new ArgumentNullException(nameof(sender));
}
if (e is null)
{
throw new ArgumentNullException(nameof(e));
}
Debug.WriteLine($"NOME ..: {ViewModel.Name}\nCOGNOME: {ViewModel.Surname}");
try
{
var service = App.GetService<INavigationService>();
service.NavigateTo(typeof(ReportViewModel).FullName!, ViewModel);
}
catch (Exception ex)
{
Debug.WriteLine($"ERRORE: {ex.Message}");
}
}
}
La View va a pescare i dati dalla ViewModel relativa, quindi abbiamo TemplateStudioForWinUISample\ViewModels\MainViewModel.cs:
using System.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using TemplateStudioForWinUISample.Contracts.ViewModels;
namespace TemplateStudioForWinUISample.ViewModels;
public partial class MainViewModel : ObservableRecipient, INavigationAware
{
public MainViewModel()
{
}
public void OnNavigatedTo(object parameter)
{
if (parameter is bool && (bool)parameter)
{
// È stato chiesto il reset.
Name = string.Empty;
Surname= string.Empty;
}
}
public void OnNavigatedFrom()
{
}
[ObservableProperty]
private string name = string.Empty;
[ObservableProperty]
private string surname = string.Empty;
}
Nel codice sopra, non bisogna farsi ingannare dalle poche righe, una classe che sembra insignificante: usando “ObservableRecipient” (MVVM Toolkit), avviene una magia, tale per cui tutto il codice che un tapino deve normalmente scrivere, viene creato in automatico dal tookit!
Sostanzialmente viene fatto in modo che mentre si scrive nella UI, le corrispondenti variabili del ViewModel vengono inizializzate al valore corrente. La magia avviene dichiarando le variabili “private string name ...” ed usando il tag “[ObservableProperty]“.
Ok, ora passiamo alla pagina del report: io l’ho aggiunta usando Template Studio (tasto destro sul progetto, Add ma poi si usa il menu apposito, non quello per aggiungere una classe qualsiasi) e poi l’ho rimossa da Views\ShellPage.xaml:
...cut...
</NavigationViewItem>
<!--
<NavigationViewItem x:Uid="Shell_Report" helpers:NavigationHelper.NavigateTo="TemplateStudioForWinUISample.ViewModels.ReportViewModel">
<NavigationViewItem.Icon>
<FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph=""/>
</NavigationViewItem.Icon>
</NavigationViewItem>
-->
</NavigationView.MenuItems>
...cut...
Praticamente un banale commento “<!-- ... -->“, potevo anche cancellare … ma mi è risultato più utile così: posso ripristinarla per modificare la UI agevolmente, per poi cancellarla di nuovo.
Iniziamo con la UI del report TemplateStudioForWinUISample\Views\ReportPage.xaml:
<Page
x:Class="TemplateStudioForWinUISample.Views.ReportPage"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid x:Name="ContentArea">
<StackPanel Spacing="10">
<Grid ColumnSpacing="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Spacing="10">
<TextBlock Text="Nome:" HorizontalAlignment="Right" />
<TextBlock Text="Cognome:" HorizontalAlignment="Right" />
</StackPanel>
<StackPanel Grid.Column="1" Spacing="10" >
<TextBlock Text="{x:Bind ViewModel.MainViewModel.Name,Mode=OneTime}"/>
<TextBlock Text="{x:Bind ViewModel.MainViewModel.Surname,Mode=OneTime}"/>
</StackPanel>
</Grid>
<Border BorderThickness="1" BorderBrush="Aqua" />
<StackPanel Orientation="Horizontal" Spacing="30" HorizontalAlignment="Right">
<Button x:Name="ButtonAnnulla" Content="Annulla" Click="ButtonAnnullaEventHandler" />
<Button Content="Conferma" Click="ButtonConfermaEventHandler" />
</StackPanel>
<!-- Click="" -->
</StackPanel>
</Grid>
</Page>
Quindi al relativo codice View TemplateStudioForWinUISample\Views\ReportPage.xaml.cs:
using System.Diagnostics;
using Microsoft.UI.Xaml.Controls;
using TemplateStudioForWinUISample.Contracts.Services;
using TemplateStudioForWinUISample.ViewModels;
namespace TemplateStudioForWinUISample.Views;
public sealed partial class ReportPage : Page
{
public ReportViewModel ViewModel
{
get;
}
public ReportPage()
{
ViewModel = App.GetService<ReportViewModel>();
InitializeComponent();
Loaded += ReportPage_Loaded;
}
private void ReportPage_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
ButtonAnnulla.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
}
/// <summary>
/// Gestione dell'Annullamento del Form: Ritorna indietro.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <exception cref="ArgumentNullException"></exception>
private void ButtonAnnullaEventHandler(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
if (sender is null)
{
throw new ArgumentNullException(nameof(sender));
}
if (e is null)
{
throw new ArgumentNullException(nameof(e));
}
try
{
var service = App.GetService<INavigationService>();
service.NavigateTo(typeof(MainViewModel).FullName!, false, clearNavigation: true);
}
catch (Exception ex)
{
Debug.WriteLine($"ERRORE: {ex.Message}");
}
}
private void ButtonConfermaEventHandler(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
if (sender is null)
{
throw new ArgumentNullException(nameof(sender));
}
if (e is null)
{
throw new ArgumentNullException(nameof(e));
}
try
{
Debug.WriteLine($"SUBMIT:\n\nNOME ..: {ViewModel.MainViewModel.Name}\nCOGNOME: {ViewModel.MainViewModel.Surname}");
var service = App.GetService<INavigationService>();
service.NavigateTo(typeof(MainViewModel).FullName!, true, clearNavigation: true);
}
catch (Exception ex)
{
Debug.WriteLine($"ERRORE: {ex.Message}");
}
}
}
Ed infine al corrispondente ViewModel TemplateStudioForWinUISample\ViewModels\ReportViewModel.cs:
using CommunityToolkit.Mvvm.ComponentModel;
using TemplateStudioForWinUISample.Contracts.ViewModels;
namespace TemplateStudioForWinUISample.ViewModels;
public class ReportViewModel : ObservableRecipient, INavigationAware
{
private MainViewModel mainViewModel = new();
public MainViewModel MainViewModel => mainViewModel;
public ReportViewModel()
{
}
public void OnNavigatedTo(object parameter)
{
mainViewModel = (MainViewModel)parameter ?? throw new ArgumentNullException(nameof(parameter));
}
public void OnNavigatedFrom()
{
}
}
Qui le parti salienti sono:
- Il fatto di ereditare la classe “
ReportViewModel” da “INavigationAware“. - L’implementazione dell’Interfaccia, con le funzioni “
OnNavigatedTo” e “OnNavigatedFrom” (quest’ultima peraltro vuota, ma l’Interfaccia in C# prevede che tutti i metodi siano implementati).
Spero che anche questo articolo sia di aiuto a qualche sfigato che, come me, si sta sbattendo per imparare a realizzare App per Windows 10/11 … una tribolazione inaudita (almeno, nel momento in cui scrivo e per chi è nelle mie – pessime – condizioni).