Building Material Design WPF Applications – an introduction to ViewModel-first routing

This is the first post in a series about app development in WPF.

WPF is a great platform for building desktop applications. Although it is deprecated by Microsoft, the community is keeping it fresh and alive with a lot of great projects and toolkits such as Mahapps Metro and Material Design In XAML.

While developing apps using these toolkits I have come up with some tools and patterns which I would like to share in this blog.

The first of these is Material.Application. It’s an MVVM framework suited for creating single window material design applications. It is opinionated to a philosophy known as ViewModel-first, where ViewModels determine what should be displayed. This is in contrast to View-first, where views determine what they should bind to.

To get started, we create a new WPF project. We add a reference to Material.Application and install the following NuGet packages:

Install-Package MaterialDesignThemes
Install-Package MahApps.Metro

We need to include the standard boilerplate in App.xaml:

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
            <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
            <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Colors.xaml" />

            <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Light.xaml" />
            <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Defaults.xaml" />
            <ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/Recommended/Primary/MaterialDesignColor.Red.xaml" />
            <ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/Recommended/Accent/MaterialDesignColor.Teal.xaml" />

            <!--  Route View Bindings  -->
            <ResourceDictionary Source="ViewBindings.xaml" />
        </ResourceDictionary.MergedDictionaries>
        <!--  MahApps Brushes  -->
        <SolidColorBrush x:Key="HighlightBrush" Color="{DynamicResource Primary700}" />
        <SolidColorBrush x:Key="AccentColorBrush" Color="{DynamicResource Primary500}" />
        <SolidColorBrush x:Key="AccentColorBrush2" Color="{DynamicResource Primary400}" />
        <SolidColorBrush x:Key="AccentColorBrush3" Color="{DynamicResource Primary300}" />
        <SolidColorBrush x:Key="AccentColorBrush4" Color="{DynamicResource Primary200}" />
        <SolidColorBrush x:Key="WindowTitleColorBrush" Color="{DynamicResource Primary700}" />
        <SolidColorBrush x:Key="AccentSelectedColorBrush" Color="{DynamicResource Primary500Foreground}" />
        <LinearGradientBrush x:Key="ProgressBrush" StartPoint="1.002,0.5" EndPoint="0.001,0.5">
            <GradientStop Offset="0" Color="{DynamicResource Primary700}" />
            <GradientStop Offset="1" Color="{DynamicResource Primary300}" />
        </LinearGradientBrush>
        <SolidColorBrush x:Key="CheckmarkFill" Color="{DynamicResource Primary500}" />
        <SolidColorBrush x:Key="RightArrowFill" Color="{DynamicResource Primary500}" />
        <SolidColorBrush x:Key="IdealForegroundColorBrush" Color="{DynamicResource Primary500Foreground}" />
        <SolidColorBrush x:Key="IdealForegroundDisabledBrush"
                            Opacity="0.4"
                            Color="{DynamicResource Primary500}" />
    </ResourceDictionary>
</Application.Resources>

We don’t need StartupUri in App.xaml and neither MainWindow.xaml so we delete both. This leaves our application without an entry point, but we will fix that shortly.

Next, we need to create our custom application controller, it’s a class that derives from AppController. The app controller is responsible for bootstrapping our application with all the necessary configuration. We override OnInitializing method, where we will start declaring what our app should contain. We grab a reference to RouteFactory because we will need it to create our initial routes.

public class DemoAppController : AppController
{
    protected override void OnInitializing()
    {
        var factory = Routes.RouteFactory;
    }
}

Now we need to add some routes. A Route is a supercharged ViewModel which is capable of communicating with the route system. We add a Routes folder to our project and add a new class named HomeRoute that derives from Material.Application.Routing.Route. We give our route a title and a nice icon:

public class HomeRoute : Route
{
    public HomeRoute()
    {
        RouteConfig.Title = "Home";
        RouteConfig.Icon = PackIconKind.Home;
    }
}

In the app controller, we create an instance of this route and add it to the side menu. We also assign it as the InitialRoute.

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

Now we need a view to visualize this route. We add a Views folder and create a new UserControl named HomeView.

<UserControl
    x:Class="Material.Demo.Views.HomeView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid>
        <TextBlock Margin="8" Text="Hello world!" />
    </Grid>
</UserControl>

Views and routes are linked in ViewBindings.xaml. You may have noticed that this file is included in the app.xaml resources above, so it’s time to create it. Add a new ResourceDictionary to your project root with the name ViewBindings.xaml. We tell it that our HomeRoute should be represented by a HomeView:

<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>

</ResourceDictionary>

The last thing left is to write our startup code. Change App.xaml’s Build Action to “Page”. In App.xaml add an event handler for the Startup event:

Startup="OnAppStartup"

Now we go to App.xaml.cs, where we will write this:

public partial class App
{
    [STAThread]
    public static void Main(string[] args)
    {
        var app = new App();
        app.InitializeComponent();
        app.Run();
    }

    private void OnAppStartup(object sender, StartupEventArgs e)
    {
        var controller = new DemoAppController();
        controller.ShowApplicationWindow();
    }
}

We create our controller and ask it to show the window for us. After executing we should see this:

home

menu

Nothing interesting yet, but it’s a great start for a clean and maintainable project. In a future post, we will explore route lifecycle methods, how to navigate the route stack, how to pass parameters to other routes, and how to receive results from routes we’ve pushed.

Source code for the framework and demo 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