Navigating the Route Stack

In this post we will discuss route navigation in more detail.

When an AppController is created, it initializes and binds components. The main component is the route stack. Routes are pushed to this stack, and the topmost route determines the actual view of the window.

The route stack must always contain at least one route. This is why we’ve defined an InitialRoute in the previous post. That route can be replaced in runtime by clicking another route from the side menu. To demonstrate this we will add an about page in the demo project:

public class AboutRoute : Route
{
    public AboutRoute()
    {
        RouteConfig.Title = "About";
        RouteConfig.Icon = PackIconKind.Information;
    }
}

We add this route to the side menu as we did with the home route in DemoAppController:

public class DemoAppController : AppController
{
    protected override void OnInitializing()
    {
        var factory = Routes.RouteFactory;
        Routes.MenuRoutes.Add(InitialRoute = factory.Get<HomeRoute>());
        // Add about route.
        Routes.MenuRoutes.Add(factory.Get<AboutRoute>());
    }
}

We create a view for this route:

<UserControl
    x:Class="Material.Demo.Views.AboutView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes">
    <Grid>
        <materialDesign:Card
            MaxWidth="600"
            Margin="16"
            HorizontalAlignment="Stretch"
            VerticalAlignment="Stretch">
            <ScrollViewer VerticalScrollBarVisibility="Auto">
                <StackPanel Margin="24,24,16,16">
                    <TextBlock Style="{StaticResource MaterialDesignTitleTextBlock}" Text="About this app" />
                    <TextBlock
                        Margin="0,24,0,0"
                        Text="This is a description page about this application."
                        TextWrapping="Wrap" />
                    <DockPanel Margin="0,16,0,0">
                        <Button
                            HorizontalAlignment="Right"
                            VerticalAlignment="Center"
                            Command="{Binding ContactCommand}"
                            Content="CONTACT"
                            DockPanel.Dock="Right"
                            Style="{StaticResource MaterialDesignFlatButton}" />
                        <TextBlock
                            VerticalAlignment="Center"
                            Text="You can contact us via e-mail."
                            TextWrapping="Wrap" />
                    </DockPanel>
                    <DockPanel Margin="0,8,0,0">
                        <Button
                            HorizontalAlignment="Right"
                            VerticalAlignment="Center"
                            Command="{Binding HomeCommand}"
                            Content="HOME"
                            DockPanel.Dock="Right"
                            Style="{StaticResource MaterialDesignFlatButton}" />
                        <TextBlock
                            VerticalAlignment="Center"
                            Text="To navigate home, click the following button."
                            TextWrapping="Wrap" />
                    </DockPanel>
                </StackPanel>
            </ScrollViewer>
        </materialDesign:Card>
    </Grid>
</UserControl>

We link this view with AboutRoute in ViewBindings.xaml:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:routes="clr-namespace:Material.Demo.Routes"
    xmlns:views="clr-namespace:Material.Demo.Views">

    <DataTemplate DataType="{x:Type routes:HomeRoute}">
        <views:HomeView />
    </DataTemplate>

    <DataTemplate DataType="{x:Type routes:AboutRoute}">
        <views:AboutView />
    </DataTemplate>

</ResourceDictionary>

We run our application to verify that everything is working as expected:

menu

Clicking on About sends us to the route we just created:

about

Note that we’ve referenced two commands that do not yet exist, which we have to implement. To navigate to another menu route we call GoToMenuRoute. To implement the contact command we need a new route class, which we will then instantiate and push to the stack. Note that ContactCommand is async, because we need to await a result inside of it.

public class AboutRoute : Route
{
    public AboutRoute()
    {
        RouteConfig.Title = "About";
        RouteConfig.Icon = PackIconKind.Information;

        // Initialize commands.
        HomeCommand = Command(Home);
        ContactCommand = AsyncCommand(Contact);
    }

    public ICommand HomeCommand { get; }

    public ICommand ContactCommand { get; }

    private void Home()
    {
        GoToMenuRoute<HomeRoute>();
    }

    private async Task Contact()
    {
        throw new NotImplementedException();
    }
}

We create a ContactRoute using the same process (route, view, viewbinding).

ContactRoute.cs:

public class ContactRoute : Route
{
    private string name;
    private string message;

    public ContactRoute(string name)
    {
        RouteConfig.Title = "Contact us";

        // We do something with the passed argument.
        Name = name;

        // Initialize commands.
        CancelCommand = Command(Cancel);
        SendCommand = Command(Send);
    }

    public string Name
    {
        get => name;
        set
        {
            if (Equals(name, value)) return;
            name = value;
            NotifyPropertyChanged();
            if (string.IsNullOrWhiteSpace(name))
            {
                AddError("Your name is required.");
            }
            else
            {
                RemoveError();
            }
        }
    }

    public string Message
    {
        get => message;
        set
        {
            if (Equals(message, value)) return;
            message = value;
            NotifyPropertyChanged();
            if (message == null || message.Length < 10)
            {
                AddError("Message must contain at least 10 characters.");
            }
            else
            {
                RemoveError();
            }
        }
    }

    public ICommand CancelCommand { get; set; }

    public ICommand SendCommand { get; set; }

    private void Cancel()
    {
        // Return false to indicate that we canceled.
        PopRoute(false);
    }

    private void Send()
    {
        if (!Validate())
        {
            return;
        }

        // Send email here...
        var emailName = Name;
        var emailMessage = Message;

        // Pop and return true to indicate that the email is sent.
        PopRoute(true);
    }

    private bool Validate()
    {
        if (string.IsNullOrWhiteSpace(name))
        {
            AddError("Your name is required.", nameof(Name));
            return false;
        }

        if (message == null || message.Length < 10)
        {
            AddError("Message must contain at least 10 characters.", nameof(Message));
            return false;
        }

        return true;
    }
}

ContactView.xaml:

<UserControl
    x:Class="Material.Demo.Views.ContactView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes">
    <Grid>
        <materialDesign:Card
            MaxWidth="600"
            Margin="16"
            HorizontalAlignment="Stretch"
            VerticalAlignment="Center">
            <ScrollViewer VerticalScrollBarVisibility="Auto">
                <StackPanel Margin="24,24,16,16">
                    <TextBlock Style="{StaticResource MaterialDesignTitleTextBlock}" Text="Send e-mail" />
                    <TextBlock Margin="0,16,0,0" Text="Your name" />
                    <TextBox Text="{Binding Name}" />
                    <TextBlock Margin="0,16,0,0" Text="Your message" />
                    <TextBox AcceptsReturn="True" Text="{Binding Message}" />
                    <StackPanel
                        Margin="0,16,0,0"
                        HorizontalAlignment="Right"
                        Orientation="Horizontal">
                        <Button
                            Command="{Binding CancelCommand}"
                            Content="CANCEL"
                            Style="{StaticResource MaterialDesignFlatButton}" />
                        <Button
                            Command="{Binding SendCommand}"
                            Content="SEND"
                            Style="{StaticResource MaterialDesignFlatButton}" />
                    </StackPanel>
                </StackPanel>
            </ScrollViewer>
        </materialDesign:Card>
    </Grid>
</UserControl>

ViewBindings.xaml:

<DataTemplate DataType="{x:Type routes:ContactRoute}">
    <views:ContactView />
</DataTemplate>

We didn’t add this route to our side menu. Sometimes we want routes to be accessible only in a specific context.

We go back to AboutRoute and implement the Contact() method. We want to push a ContactRoute and display a snackbar notification when an email is sent. We can publish notifications by importing INotificationService in AboutRoute constructor. We don’t have to worry where this service comes from because the controller’s dependency injection container will provide it for us.

public AboutRoute(INotificationService notificationService)
{
    this.notificationService = notificationService;
    ...
}

Now we need to create a ContactRoute and push it. Routes are created using GetRoute<TRoute> method, which can optionally accept parameters. In this case, we’re passing a default name to the ContactRoute we’re creating. By convention, parameters starting with a lowercase name indicate constructor arguments, and parameters starting with an uppercase letter indicate properties that will be assigned to the newly created route.

Calling Push() puts the created route on top of the stack, which passes application control to it. When this route finishes work, it yields control back to the caller. The pushed route can optionally return a result which we can await. This is similar to pushing methods on the call stack.

In our example, we return true to indicate that the email has been sent, and false or null to indicate cancellation.

private async Task Contact()
{
    var result = await GetRoute<ContactRoute>("name", "Guest").Push();
    if (result is true)
    {
        notificationService.Notify("E-mail sent.");
    }
}

Clicking on CONTACT inside AboutRoute should now open this view:

contact

If we press CANCEL or the arrow in the top left, view returns to the about page and nothing happens. If we press SEND we see the snackbar notification:

notification

To summarize, a route can navigate to menu routes using GoToMenuRoute<TRoute>. To create new routes we use GetRoute<TRoute>, on which we can call Push() and Change(). Push() puts the created route above the caller and can return an optional result when the pushed route yields control. Change() on the other hand removes everything that is on the route stack and places the created route as the new base route. Change() cannot return results.

Because changing and popping routes are common actions, the Route class includes two commands: GoToMenuRouteCommand and PopRouteCommand. These can be used for quick navigation without any boilerplate code in routes:

<Button
    Command="{Binding GoToMenuRouteCommand}"
    CommandParameter="{x:Type routes:HomeRoute}"
    Content="HOME" />
<Button
    Command="{Binding PopRouteCommand}"
    Content="CANCEL" />

In the picture below I’ve visualized the route stack infrastructure:

route_stack

Routes receive event methods like RouteActivating and RouteDeactivating when they are placed or removed from the stack. We can use those methods to initialize routes, load data, or do cleanups. This will be the topic of the next post, where we will work with some asynchronous operations to fetch data.

Framework and demo code is available on Github.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s