Navigace v .NET MAUI Shell: Kompletní průvodce routingem, předáváním dat a deep linking

Průvodce Shell navigací v .NET MAUI — od struktury AppShell přes registraci tras, předávání dat pomocí IQueryAttributable, deep linking až po navigační službu pro MVVM. S funkčními příklady pro .NET 10.

Úvod: Proč je navigace v mobilních aplikacích tak důležitá

Navigace je páteří každé mobilní aplikace. Může být jednoduchá — přechod z jedné obrazovky na druhou — nebo pořádně zamotaná, zahrnující podmíněné přesměrování, předávání komplexních objektů a deep linking z externích zdrojů. V .NET MAUI je Shell tím hlavním nástrojem, který celou navigaci zjednodušuje a sjednocuje napříč platformami.

Pokud jste zvyklí na starý přístup z Xamarin.Forms pomocí NavigationPage a PushAsync, Shell vám nabídne moderní alternativu založenou na URI routingu. A v .NET 10 přibyla řada vylepšení — od animací navigačního panelu po plnou podporu NativeAOT bezpečného předávání parametrů.

Tak pojďme na to. Projdeme si Shell navigaci od základů až po pokročilé scénáře a každou část doprovodíme funkčními příklady kódu, které můžete rovnou hodit do svého projektu.

Co je .NET MAUI Shell a proč ho používat

Shell je v podstatě kontejner, který definuje vizuální strukturu vaší aplikace. Poskytuje jednotné navigační rozhraní se zabudovanou podporou pro:

  • Flyout menu — postranní navigační panel
  • Spodní záložky (Tab Bar) — přepínání mezi hlavními sekcemi
  • Horní záložky — podsekce v rámci záložky
  • URI-based navigace — přechod na libovolnou stránku pomocí cesty
  • Deep linking — otevření konkrétní stránky z externího odkazu
  • Zpětná navigace — bez nutnosti procházet celý zásobník

Oproti starému NavigationPage přístupu Shell výrazně zjednodušuje definici navigační hierarchie. A co je fajn — nabízí deklarativní způsob popisu struktury aplikace přímo v XAML, takže vidíte celou strukturu na jednom místě.

Základní struktura AppShell

Celá navigační hierarchie se definuje v souboru AppShell.xaml. Tady je typická struktura:

<?xml version="1.0" encoding="UTF-8" ?>
<Shell
    x:Class="MauiApp.AppShell"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:views="clr-namespace:MauiApp.Views"
    Title="Moje Aplikace">

    <FlyoutItem Title="Domů" Icon="home.png">
        <ShellContent
            Title="Přehled"
            Route="home"
            ContentTemplate="{DataTemplate views:HomePage}" />
    </FlyoutItem>

    <FlyoutItem Title="Produkty" Icon="products.png">
        <Tab Title="Katalog">
            <ShellContent
                Title="Všechny produkty"
                Route="products"
                ContentTemplate="{DataTemplate views:ProductsPage}" />
            <ShellContent
                Title="Oblíbené"
                Route="favorites"
                ContentTemplate="{DataTemplate views:FavoritesPage}" />
        </Tab>
    </FlyoutItem>

    <TabBar>
        <Tab Title="Nastavení" Icon="settings.png">
            <ShellContent
                Route="settings"
                ContentTemplate="{DataTemplate views:SettingsPage}" />
        </Tab>
    </TabBar>

</Shell>

Klíčový koncept — ContentTemplate s DataTemplate zajišťuje, že stránky se vytvářejí líně (lazy loading). Stránka se prostě nevytvoří, dokud na ni uživatel skutečně nenaviguje. To výrazně zlepšuje dobu startu aplikace, což ocení hlavně uživatelé na starších zařízeních.

Definice a registrace tras

Každý ShellContent má vlastnost Route, která definuje cestu pro navigaci. Stránky, které nejsou součástí vizuální hierarchie Shellu (typicky detailové stránky), musíte zaregistrovat ručně v code-behind:

public partial class AppShell : Shell
{
    public AppShell()
    {
        InitializeComponent();

        // Registrace tras pro stránky mimo vizuální hierarchii
        Routing.RegisterRoute("productDetail", typeof(ProductDetailPage));
        Routing.RegisterRoute("orderSummary", typeof(OrderSummaryPage));
        Routing.RegisterRoute("userProfile", typeof(UserProfilePage));
    }
}

Důležité pravidlo: vždy definujte trasy explicitně. Pokud vlastnost Route nenastavíte, Shell vygeneruje trasu automaticky za běhu — ale tyto generované trasy nejsou konzistentní napříč různými spuštěními aplikace. Z vlastní zkušenosti vím, že tohle vede k těžko dohledatelným chybám, které se projevují náhodně a jen na určitých zařízeních.

Správa tras v MVVM projektu

V projektech s MVVM architekturou je dobrým zvykem centralizovat názvy tras do statické třídy. Vyhnete se tak překlepům a duplicitám (a věřte mi, překlep v názvu trasy je přesně ten typ chyby, který hledáte hodinu):

public static class AppRoutes
{
    public const string Home = "home";
    public const string Products = "products";
    public const string ProductDetail = "productDetail";
    public const string Favorites = "favorites";
    public const string OrderSummary = "orderSummary";
    public const string UserProfile = "userProfile";
    public const string Settings = "settings";
}

Registrace pak vypadá takto:

Routing.RegisterRoute(
    AppRoutes.ProductDetail, typeof(ProductDetailPage));
Routing.RegisterRoute(
    AppRoutes.OrderSummary, typeof(OrderSummaryPage));

Navigace pomocí GoToAsync

Veškerá navigace v Shell probíhá přes metodu Shell.Current.GoToAsync(). Pojďme se podívat na jednotlivé způsoby.

Absolutní navigace

Prefix // resetuje celý navigační zásobník a naviguje na zadanou trasu od kořene:

// Přechod na domovskou stránku — zásobník se vyčistí
await Shell.Current.GoToAsync("//home");

Relativní navigace

Bez prefixu // se stránka přidá na aktuální navigační zásobník:

// Navigace vpřed na detail produktu
await Shell.Current.GoToAsync("productDetail");

Zpětná navigace

Pro navigaci zpět použijte dvě tečky (..). Jednoduché a intuitivní:

// Zpět o jednu stránku
await Shell.Current.GoToAsync("..");

// Zpět o dvě stránky
await Shell.Current.GoToAsync("../..");

Kombinovaná navigace

A tady to začíná být zajímavé. Můžete kombinovat zpětnou a dopřednou navigaci v jednom volání:

// Zpět o jednu stránku a pak vpřed na jinou
await Shell.Current.GoToAsync("../orderSummary");

Tohle se hodí třeba v situaci, kdy uživatel na detailové stránce dokončí akci a chcete ho přesunout rovnou na souhrn objednávky, aniž by se musel vracet ručně.

Předávání dat mezi stránkami

Předávání dat při navigaci je jeden z nejčastějších úkolů v mobilních aplikacích. Upřímně — je to taky jedna z věcí, která se dá nejsnadněji pokazit. .NET MAUI Shell nabízí několik způsobů, od jednoduchých query parametrů až po předávání komplexních objektů.

Způsob 1: Query parametry v URI

Nejjednodušší metoda pro předávání primitivních typů (string, int, bool):

// Odeslání dat
await Shell.Current.GoToAsync(
    $"{AppRoutes.ProductDetail}?productId={product.Id}&name={Uri.EscapeDataString(product.Name)}");

Na cílové stránce (nebo ViewModelu) data přijmete pomocí rozhraní IQueryAttributable:

public class ProductDetailViewModel : ObservableObject, IQueryAttributable
{
    private int _productId;
    private string _productName;

    public int ProductId
    {
        get => _productId;
        set => SetProperty(ref _productId, value);
    }

    public string ProductName
    {
        get => _productName;
        set => SetProperty(ref _productName, value);
    }

    public void ApplyQueryAttributes(
        IDictionary<string, object> query)
    {
        if (query.TryGetValue("productId", out var idValue))
            ProductId = Convert.ToInt32(idValue);

        if (query.TryGetValue("name", out var nameValue))
            ProductName = Uri.UnescapeDataString(nameValue.ToString());
    }
}

Proč IQueryAttributable a ne QueryPropertyAttribute? Atribut QueryPropertyAttribute není bezpečný pro trimming a NativeAOT. V .NET 10 je doporučeným přístupem výhradně IQueryAttributable. Je plně kompatibilní s AOT kompilací a trimming optimalizacemi, takže nebudete mít žádné nepříjemné překvapení při publikování release buildu.

Způsob 2: Předávání objektů přes Dictionary

Pro předávání komplexních objektů použijte přetížení GoToAsync s parametrem typu IDictionary<string, object>:

// Odeslání komplexního objektu
var product = await _productService.GetProductAsync(productId);

var navigationParams = new Dictionary<string, object>
{
    { "product", product }
};

await Shell.Current.GoToAsync(
    AppRoutes.ProductDetail, navigationParams);

Příjem na cílové stránce:

public void ApplyQueryAttributes(
    IDictionary<string, object> query)
{
    if (query.TryGetValue("product", out var productObj)
        && productObj is Product product)
    {
        CurrentProduct = product;
        LoadRelatedProducts(product.CategoryId);
    }
}

Pozor na jednu věc: objekty předané přes Dictionary zůstávají v paměti po celou dobu životnosti stránky. Uvolní se až při odebrání stránky z navigačního zásobníku. U menších objektů to není problém, ale pokud předáváte velké kolekce dat, mějte to na paměti.

Způsob 3: Předávání dat při zpětné navigaci

Někdy potřebujete vrátit data z detailové stránky zpět na předchozí stránku. Shell to umožňuje a je to překvapivě přímočaré:

// Na detailové stránce — uživatel vybral položku
var selectedParams = new Dictionary<string, object>
{
    { "selectedProduct", _selectedProduct }
};

await Shell.Current.GoToAsync("..", selectedParams);

Navigační služba pro MVVM

V čistém MVVM projektu by ViewModel neměl přímo volat Shell.Current.GoToAsync. Vím, že to tak spousta lidí dělá (a funguje to), ale z hlediska testovatelnosti a čistoty architektury je lepší vytvořit navigační službu, která oddělí navigační logiku od ViewModelů:

public interface INavigationService
{
    Task NavigateToAsync(string route);
    Task NavigateToAsync(
        string route, IDictionary<string, object> parameters);
    Task GoBackAsync();
    Task GoBackAsync(IDictionary<string, object> parameters);
}

public class ShellNavigationService : INavigationService
{
    public async Task NavigateToAsync(string route)
    {
        await Shell.Current.GoToAsync(route);
    }

    public async Task NavigateToAsync(
        string route, IDictionary<string, object> parameters)
    {
        await Shell.Current.GoToAsync(route, parameters);
    }

    public async Task GoBackAsync()
    {
        await Shell.Current.GoToAsync("..");
    }

    public async Task GoBackAsync(
        IDictionary<string, object> parameters)
    {
        await Shell.Current.GoToAsync("..", parameters);
    }
}

Registrace v dependency injection kontejneru:

// MauiProgram.cs
builder.Services.AddSingleton<INavigationService,
    ShellNavigationService>();
builder.Services.AddTransient<ProductsViewModel>();
builder.Services.AddTransient<ProductDetailViewModel>();

A takhle pak navigační službu použijete ve ViewModelu:

public partial class ProductsViewModel : ObservableObject
{
    private readonly INavigationService _navigation;
    private readonly IProductService _productService;

    public ProductsViewModel(
        INavigationService navigation,
        IProductService productService)
    {
        _navigation = navigation;
        _productService = productService;
    }

    [RelayCommand]
    private async Task OpenProductDetail(Product product)
    {
        var parameters = new Dictionary<string, object>
        {
            { "product", product }
        };

        await _navigation.NavigateToAsync(
            AppRoutes.ProductDetail, parameters);
    }
}

Výhoda je jasná — ve unit testech stačí mocknout INavigationService a máte plnou kontrolu nad tím, kam se naviguje, aniž byste potřebovali reálný Shell.

Deep linking: Otevření aplikace z externího odkazu

Deep linking umožňuje otevřít konkrétní stránku v aplikaci z externího zdroje — například z notifikace, e-mailu nebo webového odkazu. Shell tuto funkci podporuje nativně díky svému URI-based routingu, což je jedna z jeho největších předností.

Konfigurace pro Android

V souboru Platforms/Android/AndroidManifest.xml přidejte intent filter:

<activity
    android:name=".MainActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data
            android:scheme="https"
            android:host="mojeappka.cz"
            android:pathPrefix="/product" />
    </intent-filter>
</activity>

Konfigurace pro iOS

V souboru Platforms/iOS/Info.plist přidejte URL scheme:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>mojeappka</string>
        </array>
    </dict>
</array>

Zpracování deep linku v aplikaci

V App.xaml.cs zpracujte příchozí URI a navigujte na odpovídající stránku:

public partial class App : Application
{
    public App()
    {
        InitializeComponent();
    }

    protected override Window CreateWindow(
        IActivationState activationState)
    {
        var window = new Window(new AppShell());

        // Zpracování deep linku při spuštění
        if (activationState?.State != null
            && activationState.State.TryGetValue(
                "url", out var url))
        {
            HandleDeepLink(url.ToString());
        }

        return window;
    }

    private async void HandleDeepLink(string url)
    {
        if (Uri.TryCreate(url, UriKind.Absolute, out var uri))
        {
            var path = uri.AbsolutePath.TrimStart('/');
            var query = uri.Query;

            // Navigace na odpovídající stránku
            await Shell.Current.GoToAsync(
                $"{path}{query}");
        }
    }
}

Jen mějte na paměti, že pro produkční aplikace budete chtít deep link handler trochu robustnější — ošetřit neplatné cesty, přidat fallback na domovskou stránku a ideálně počkat na plnou inicializaci Shellu.

Zachycení a kontrola navigačních událostí

Shell poskytuje dvě klíčové události — Navigating a Navigated — které umožňují reagovat na navigaci nebo ji dokonce zablokovat. Tohle je extrémně užitečné pro autentizační logiku:

public partial class AppShell : Shell
{
    public AppShell()
    {
        InitializeComponent();
        RegisterRoutes();

        // Přihlášení k navigačním událostem
        Navigating += OnNavigating;
        Navigated += OnNavigated;
    }

    private async void OnNavigating(
        object sender, ShellNavigatingEventArgs e)
    {
        // Podmíněné zablokování navigace
        if (e.Target.Location.OriginalString
            .Contains("orderSummary"))
        {
            var authService = Handler.MauiContext.Services
                .GetRequiredService<IAuthService>();

            if (!authService.IsAuthenticated)
            {
                // Zrušení navigace
                e.Cancel();

                // Přesměrování na přihlášení
                await GoToAsync("login");
            }
        }
    }

    private void OnNavigated(
        object sender, ShellNavigatedEventArgs e)
    {
        // Logování nebo analytika
        var currentRoute = Current.CurrentState.Location
            .ToString();
        System.Diagnostics.Debug.WriteLine(
            $"Navigováno na: {currentRoute}");
    }

    private void RegisterRoutes()
    {
        Routing.RegisterRoute(
            AppRoutes.ProductDetail,
            typeof(ProductDetailPage));
        Routing.RegisterRoute(
            AppRoutes.OrderSummary,
            typeof(OrderSummaryPage));
        Routing.RegisterRoute(
            "login", typeof(LoginPage));
    }
}

Tato technika je obzvlášť užitečná pro implementaci navigačních strážců (route guards). Osobně ji používám ve většině projektů pro kontrolu autentizace před přístupem k chráněným stránkám — je to čistší řešení než kontrolovat stav přihlášení v každém ViewModelu zvlášť.

Novinky Shell navigace v .NET 10

.NET 10, vydaný v listopadu 2025 jako LTS verze, přinesl do Shell navigace několik vylepšení, na která se čekalo.

Animace navigačního panelu

Nová vlastnost Shell.NavBarVisibilityAnimationEnabled umožňuje řídit animaci při zobrazení nebo skrytí navigačního panelu. Zní to jako drobnost, ale v praxi to řeší nepříjemné problikávání při přechodu na full-screen stránky:

<!-- Vypnutí animace pro okamžité zobrazení/skrytí -->
<ContentPage
    Shell.NavBarIsVisible="True"
    Shell.NavBarVisibilityAnimationEnabled="False">
    <!-- obsah stránky -->
</ContentPage>

CollectionView jako výchozí

CollectionView je v .NET 10 výchozím handlerem na všech platformách. Pokud ve svých navigačních scénářích používáte seznamy (třeba pro výběr položky a navigaci na detail), měli byste migrovat z ListView na CollectionView. Výkon je znatelně lepší, zvlášť u delších seznamů.

XAML Source Generation

Nový generátor zdrojového kódu pro XAML vytváří silně typovaný kód při kompilaci. V praxi to znamená rychlejší načítání stránek při navigaci a lepší podporu IntelliSense. Aktivujete ho jedním řádkem v souboru projektu:

<PropertyGroup>
    <MauiXamlInflator>SourceGen</MauiXamlInflator>
</PropertyGroup>

Časté problémy a jak je řešit

Za dobu práce s .NET MAUI Shell jsem narazil na pár problémů, které trápí snad každého. Tady jsou ty nejčastější.

Problém: Query parametry přetrvávají při zpětné navigaci

Tohle je klasika. Při zpětné navigaci se query parametry znovu doručí do ApplyQueryAttributes, což může vést k nechtěnému přepsání dat. Řešení — vyčistěte slovník po prvním zpracování:

public void ApplyQueryAttributes(
    IDictionary<string, object> query)
{
    if (query.TryGetValue("product", out var productObj)
        && productObj is Product product)
    {
        CurrentProduct = product;
        query.Remove("product"); // Zabránit opakovanému zpracování
    }
}

Problém: Jedna instance stránky pro stejný typ

Shell standardně spravuje jednu instanci každého typu stránky. Pokud potřebujete navigovat na dvě různé instance stejného typu (například detail produktu A a detail produktu B v zásobníku), Shell to přímo nepodporuje. Řešením je zajistit, aby ApplyQueryAttributes správně aktualizoval data při opětovné navigaci.

Problém: NavigationPage uvnitř Shell

Shell a NavigationPage nejsou kompatibilní. Bod. Pokud se pokusíte vložit NavigationPage do Shell aplikace, dostanete výjimku. Při migraci z Xamarin.Forms musíte všechna volání Navigation.PushAsync nahradit za Shell.Current.GoToAsync.

Problém: Dynamicky registrované trasy a absolutní cesty

Absolutní navigace (//route) nefunguje pro dynamicky registrované trasy. Na takové trasy musíte vždy navigovat relativně:

// Toto nebude fungovat pro dynamicky registrovanou trasu:
// await Shell.Current.GoToAsync("//productDetail");

// Správný způsob:
await Shell.Current.GoToAsync("productDetail");

Tohle je něco, co dokumentace nezmiňuje úplně jasně, a přitom to stojí za spoustou frustrujícího debuggování.

Kompletní příklad: E-shop s Shell navigací

Na závěr si ukažme, jak všechny koncepty propojit do fungujícího příkladu. Představte si jednoduchou e-shop aplikaci s produkty, košíkem a objednávkou:

// AppRoutes.cs
public static class AppRoutes
{
    public const string Products = "products";
    public const string ProductDetail = "productDetail";
    public const string Cart = "cart";
    public const string Checkout = "checkout";
    public const string OrderConfirmation = "orderConfirmation";
}

// AppShell.xaml.cs
public partial class AppShell : Shell
{
    public AppShell()
    {
        InitializeComponent();

        Routing.RegisterRoute(
            AppRoutes.ProductDetail,
            typeof(ProductDetailPage));
        Routing.RegisterRoute(
            AppRoutes.Checkout,
            typeof(CheckoutPage));
        Routing.RegisterRoute(
            AppRoutes.OrderConfirmation,
            typeof(OrderConfirmationPage));
    }
}

// ProductsViewModel.cs
public partial class ProductsViewModel : ObservableObject
{
    private readonly INavigationService _nav;
    private readonly IProductService _productService;

    [ObservableProperty]
    private ObservableCollection<Product> _products;

    public ProductsViewModel(
        INavigationService nav,
        IProductService productService)
    {
        _nav = nav;
        _productService = productService;
    }

    [RelayCommand]
    private async Task LoadProducts()
    {
        var items = await _productService.GetAllAsync();
        Products = new ObservableCollection<Product>(items);
    }

    [RelayCommand]
    private async Task SelectProduct(Product product)
    {
        await _nav.NavigateToAsync(
            AppRoutes.ProductDetail,
            new Dictionary<string, object>
            {
                { "product", product }
            });
    }
}

// ProductDetailViewModel.cs
public partial class ProductDetailViewModel :
    ObservableObject, IQueryAttributable
{
    private readonly INavigationService _nav;
    private readonly ICartService _cartService;

    [ObservableProperty]
    private Product _product;

    public ProductDetailViewModel(
        INavigationService nav,
        ICartService cartService)
    {
        _nav = nav;
        _cartService = cartService;
    }

    public void ApplyQueryAttributes(
        IDictionary<string, object> query)
    {
        if (query.TryGetValue("product", out var obj)
            && obj is Product product)
        {
            Product = product;
            query.Remove("product");
        }
    }

    [RelayCommand]
    private async Task AddToCart()
    {
        await _cartService.AddItemAsync(Product);
        await _nav.NavigateToAsync(
            $"//{AppRoutes.Cart}");
    }
}

// MauiProgram.cs — registrace služeb
public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder.UseMauiApp<App>();

        // Služby
        builder.Services.AddSingleton<INavigationService,
            ShellNavigationService>();
        builder.Services.AddSingleton<IProductService,
            ProductService>();
        builder.Services.AddSingleton<ICartService,
            CartService>();

        // ViewModely
        builder.Services.AddTransient<ProductsViewModel>();
        builder.Services.AddTransient
            <ProductDetailViewModel>();

        // Stránky
        builder.Services.AddTransient<ProductsPage>();
        builder.Services.AddTransient<ProductDetailPage>();
        builder.Services.AddTransient<CartPage>();
        builder.Services.AddTransient<CheckoutPage>();

        return builder.Build();
    }
}

Často kladené otázky (FAQ)

Jaký je rozdíl mezi GoToAsync a PushAsync v .NET MAUI?

GoToAsync je metoda Shell navigace založená na URI routingu. PushAsync pochází ze starého NavigationPage přístupu a pracuje se zásobníkem stránek. V Shell aplikaci byste měli vždy používat GoToAsync. Pokud v Shell aplikaci použijete PushAsync, vytvoří se separátní navigační zásobník a tlačítko zpět nemusí fungovat správně.

Jak předat komplexní objekt mezi stránkami v .NET MAUI?

Nejlepší způsob je použít přetížení GoToAsync s parametrem Dictionary<string, object>. Objekt vložíte do slovníku pod klíčem a na cílové stránce ho přijmete přes rozhraní IQueryAttributable v metodě ApplyQueryAttributes. Žádná serializace, žádné komplikace.

Je QueryPropertyAttribute stále bezpečný v .NET 10?

Ne. QueryPropertyAttribute není bezpečný pro trimming ani NativeAOT kompilaci. V .NET 10 je doporučeným a oficiálně podporovaným přístupem výhradně rozhraní IQueryAttributable. Pokud ve svém projektu stále používáte QueryPropertyAttribute, je nejvyšší čas ho nahradit.

Jak implementovat navigační strážce (route guards) v Shell?

Shell poskytuje událost Navigating, kterou můžete odebírat v AppShell. V handleru události máte přístup k cílové lokaci a můžete navigaci zrušit voláním e.Cancel(). Následně přesměrujete uživatele třeba na přihlašovací obrazovku. Je to nejčistší způsob, jak implementovat autentizační middleware v .NET MAUI aplikaci.

Podporuje .NET MAUI Shell deep linking?

Ano. Díky URI-based routingu Shell přirozeně podporuje deep linking. Na Androidu konfigurujete intent filtry v AndroidManifest.xml, na iOS přidáte URL schemes do Info.plist. Příchozí URI pak zpracujete v App.xaml.cs a navigujete na odpovídající Shell trasu. Shell sám o sobě neposkytuje automatické mapování externích URL na interní trasy — to musíte implementovat ručně, ale díky flexibilitě URI routingu to není nic složitého.

O Autorovi Editorial Team

Our team of expert writers and editors.