Single-page web applications (SPA) provide faster transitions and interactivity that make a web application feel like a native app. It has become a popular style of developing web applications with JavaScript frameworks like React, Vue, or Angular. In a SPA, all the resources needed for the web app to be functional are loaded the first time the app is loaded, or some of it dynamically loaded. Unlike a server-rendered web application where some user interactions go to the server to retrieve the required markup and data, there’s no page reload in a SPA. Although the browser location history can be updated dynamically to provide the perception that the user is taken to a new page.

In this post, you’ll learn how to build a SPA using Uno Platform, C#, and XAML. As a pre-requisite, you should have some knowledge of C#, XAML, Uno Platform, and WebAssembly. You should also have set up your development environment for building with Uno Platform. If you haven’t, take some time to read the first two sections of a previous post on how to do this.

Project Set-Up

The sample app we’re going to build will be a basic app with a header, and a sidebar that will show the list of employees for a fictitious company. It’ll have buttons to add and delete employee data, with a details view to show more info about a selected employee.

If you’re using Visual Studio or Jetbrains Rider, you can create a new solution for a WebAssembly project using the installed Uno Platform solutions template. If you’re working with VS Code, you can use the command-line to create it and then open the solution folder in VS Code. In the command-line, run the command dotnet new unoapp -o MyWasmApp -ios=false -android=false -macos=false -uwp=false –vscodeWasm to create the .NET solution and then code ./MyWasmApp to open it in VS Code

The instructions in this post will be demonstrated using VS Code.

The View Model

We’re going to start by creating an Employee class and a view model which will be bound to some controls on the page. Create a folder named Model and in it create a file named Employee.cs with the following code:

using Windows.Foundation.Metadata;

namespace MyWasmApp.Model
{
  [CreateFromString(
   MethodName = "MyWasmApp.Model.ModelConverter.CreateEmployeeFromString")]
  public class Employee : Observable
  {
    private string _firstName;
    private string _lastName;
    private string _nationality;

    public string FirstName
    {
      get => _firstName;
      set
      {
       _firstName = value;
       NotifyPropertyChanged();
      }
    }

    public string LastName
    {
     get => _lastName;
      set
      {
       _lastName = value;
       NotifyPropertyChanged();
      }
    }
    public string Nationality
    {
      get => _nationality;
      set
      {
       _nationality = value;
       NotifyPropertyChanged();
      }
    }
  }
}

This is a plain class that inherits from an Observable class and is used to notify clients that a property value has changed. We also specified a CreateFromString attribute for the type conversion in XAML, with a fully qualified name to the method that will do the conversion.

Add a new file named ModelConverter in the same directory. This will contain the type conversion method:

namespace MyWasmApp.Model
{
  public static class ModelConverter
  {
    public static Employee CreateEmployeeFromString(string inputString)
    {
      var values = inputString.Split(',');
      return new Employee
      {
        FirstName = values[0],
        LastName = values[1],
        Nationality = values[2]
      };
    }
  }
}

Add another file ViewModel.cs with the following content:

using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;

namespace MyWasmApp.Model
{
  public class ViewModel : Observable
  {
    private Employee _selectedEmployee;

    public ViewModel()
    {
      Employees = new ObservableCollection{
          new Employee{FirstName="Grillo",LastName="Huber",Nationality="German"},
          new Employee{FirstName="Anna",LastName="Rockstar",Nationality="Nigerian"},
          new Employee{FirstName="Julia",LastName="Mojave", Nationality="Canadian"},
        };
    }

    public Employee SelectedEmployee
    {
      get { return _selectedEmployee; }
      set
      {
        if (_selectedEmployee != value)
        {
          _selectedEmployee = value;
          NotifyPropertyChanged();
          NotifyPropertyChanged(nameof(IsEmployeeSelected));
        }
      }
    }

    public bool IsEmployeeSelected => SelectedEmployee != null;

    public ObservableCollection Employees { get; }

    public void AddEmployee()
    {
      var employee = new Employee { FirstName = "New" };
      Employees.Add(employee);
      SelectedEmployee = employee;
    }

    public void DeleteEmployee()
    {
      var employee = SelectedEmployee;
      if (employee != null)
      {
        Employees.Remove(employee);
        SelectedEmployee = null;
      }
    }
  }
}

This class has properties that contain a list of employees, methods to add and delete an employee, and it also inherits from the Observable class.

Now add the Observable class. Create a new file Observable.cs, then copy and paste the code below to it.

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace MyWasmApp.Model
{
  public class Observable : INotifyPropertyChanged
  {
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
    {
      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
  }
}

The Observable class implements the INotifyPropertyChanged interface, which is used to notify clients that a property value has changed. With this, we can bind controls to properties and get changes from the controls to the model, and vice versa.

Creating Reusable Components

You can create custom controls that can be reused in different pages, or as a way to break up your UI and make the code easier to read and manage. We will create a custom control that will be used to display the detail of a selected employee

Create a new directory named Controls and a file named EmployeeDetailControl.xaml. Copy and paste the XAML below to the file.

<UserControl x:Class="MyWasmApp.Controls.EmployeeDetailControl"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="using:MyWasmApp.Controls"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400">
  <StackPanel>
    <TextBox Header="Firstname" Margin="10" Text="{x:Bind Employee.FirstName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
    <TextBox Header="Lastname" Margin="10" Text="{x:Bind Employee.LastName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
    <TextBox Header="Nationality" Margin="10" Text="{x:Bind Employee.Nationality,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
  </StackPanel>
</UserControl>

This control will render 3 textboxes that is bound to specific properties of the Employee object passed to it, with two way data binding.

Add a code-behind file for this XAML control named EmployeeDetailControl.xaml.cs with the following content:

using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Markup;
using System;
using MyWasmApp.Model;

namespace MyWasmApp.Controls
{
  [ContentProperty(Name = nameof(Employee))]
  public sealed partial class EmployeeDetailControl : UserControl
  {
    public static readonly DependencyProperty EmployeeProperty = DependencyProperty.Register("Employee", typeof(Employee),
      typeof(EmployeeDetailControl), new PropertyMetadata(null));

    public EmployeeDetailControl()
    {
      this.InitializeComponent();
    }

    public Employee Employee
    {
      get { return (Employee)GetValue(EmployeeProperty); }
      set { SetValue(EmployeeProperty, value); }
    }
  }
}

This control will render 3 textboxes that is bound to specific properties of the Employee object passed to it, with two way data binding.

Add a code-behind file for this XAML control named EmployeeDetailControl.xaml.cs with the following content:

using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Markup;
using System;
using MyWasmApp.Model;

namespace MyWasmApp.Controls
{
  [ContentProperty(Name = nameof(Employee))]
  public sealed partial class EmployeeDetailControl : UserControl
  {
    public static readonly DependencyProperty EmployeeProperty = DependencyProperty.Register("Employee", typeof(Employee),
      typeof(EmployeeDetailControl), new PropertyMetadata(null));

    public EmployeeDetailControl()
    {
      this.InitializeComponent();
    }

    public Employee Employee
    {
      get { return (Employee)GetValue(EmployeeProperty); }
      set { SetValue(EmployeeProperty, value); }
    }
  }
}

Defining The Main Page and Using The EmployeeDetailControl

We’ve created the view model and also a custom control to display an employee’s detail. Now we’ll put it all together and to display and employees from a page. Open MainPage.xaml and update the markup with the XAML below.

<Page x:Class="MyWasmApp.MainPage"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:model="using:MyWasmApp.Model"
  xmlns:controls="using:MyWasmApp.Controls"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" d:DesignWidth="600" d:DesignHeight="400">

  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="Auto"/>
      <ColumnDefinition/>
      <ColumnDefinition Width="Auto"/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition/>
    </Grid.RowDefinitions>


    <!--Header-->
    <StackPanel Grid.ColumnSpan="2" Background="#F05A28" Orientation="Horizontal">
      <TextBlock Text="Employee Facebook" FontSize="30" FontWeight="ExtraBold" Foreground="White" VerticalAlignment="Bottom" Margin="20"/>
    </StackPanel>

    <!-- Employee list (sidebar) -->
    <Grid Grid.Row="1" x:Name="employeeListGrid" Background="#9A9492">
      <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition/>
      </Grid.RowDefinitions>

      <StackPanel Orientation="Horizontal">
        <Button Margin="10" Click="{x:Bind ViewModel.AddEmployee}">
          <StackPanel Orientation="Horizontal">
            <SymbolIcon Symbol="AddFriend"/>
            <TextBlock Text="Add" Margin="5 0 0 0"/>
          </StackPanel>
        </Button>
        <Button Margin="10" Click="{x:Bind ViewModel.DeleteEmployee}" IsEnabled="{x:Bind ViewModel.IsEmployeeSelected,Mode=OneWay}">
          <StackPanel Orientation="Horizontal">
            <SymbolIcon Symbol="Delete"/>
            <TextBlock Text="Delete" Margin="5 0 0 0"/>
          </StackPanel>
        </Button>
      </StackPanel>

      <ListView Grid.Row="1" ItemsSource="{x:Bind ViewModel.Employees,Mode=OneWay}" SelectedItem="{x:Bind ViewModel.SelectedEmployee,Mode=TwoWay}">
        <ListView.ItemTemplate>
          <DataTemplate x:DataType="model:Employee">
            <StackPanel Orientation="Horizontal">
              <TextBlock Text="{x:Bind FirstName,Mode=OneWay}" FontWeight="Bold"/>
              <TextBlock Text="{x:Bind LastName,Mode=OneWay}" Margin="5 0 0 0"/>
            </StackPanel>
          </DataTemplate>
        </ListView.ItemTemplate>
      </ListView>
    </Grid>

    <!-- Employee detail -->
    <controls:EmployeeDetailControl Grid.Row="1" Grid.Column="1" Employee="{x:Bind ViewModel.SelectedEmployee,Mode=OneWay}" Visibility="{x:Bind ViewModel.IsEmployeeSelected,Mode=OneWay}"/>
  </Grid>
</Page>

This control will render 3 textboxes that is bound to specific properties of the Employee object passed to it, with two way data binding.

Add a code-behind file for this XAML control named EmployeeDetailControl.xaml.cs with the following content:

using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Markup;
using System;
using MyWasmApp.Model;

namespace MyWasmApp.Controls
{
  [ContentProperty(Name = nameof(Employee))]
  public sealed partial class EmployeeDetailControl : UserControl
  {
    public static readonly DependencyProperty EmployeeProperty = DependencyProperty.Register("Employee", typeof(Employee),
        typeof(EmployeeDetailControl), new PropertyMetadata(null));

    public EmployeeDetailControl()
    {
        this.InitializeComponent();
    }

    public Employee Employee
    {
      get { return (Employee)GetValue(EmployeeProperty); }
      set { SetValue(EmployeeProperty, value); }
    }
  }
}

Defining The Main Page and Using The EmployeeDetailControl

We’ve created the view model and also a custom control to display an employee’s detail. Now we’ll put it all together and to display and employees from a page. Open MainPage.xaml and update the markup with the XAML below.

<Page x:Class="MyWasmApp.MainPage"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:model="using:MyWasmApp.Model"
  xmlns:controls="using:MyWasmApp.Controls"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" d:DesignWidth="600" d:DesignHeight="400">

  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="Auto"/>
      <ColumnDefinition/>
      <ColumnDefinition Width="Auto"/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition/>
    </Grid.RowDefinitions>

    <!--Header-->
    <StackPanel Grid.ColumnSpan="2" Background="#F05A28" Orientation="Horizontal">
      <TextBlock Text="Employee Facebook" FontSize="30" FontWeight="ExtraBold" Foreground="White" VerticalAlignment="Bottom" Margin="20"/>
    </StackPanel>

    <!-- Employee list (sidebar) -->
    <Grid Grid.Row="1" x:Name="employeeListGrid" Background="#9A9492">
      <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition/>
      </Grid.RowDefinitions>

      <StackPanel Orientation="Horizontal">
        <Button Margin="10" Click="{x:Bind ViewModel.AddEmployee}">
          <StackPanel Orientation="Horizontal">
            <SymbolIcon Symbol="AddFriend"/>
            <TextBlock Text="Add" Margin="5 0 0 0"/>
          </StackPanel>
        </Button>
        <Button Margin="10" Click="{x:Bind ViewModel.DeleteEmployee}" IsEnabled="{x:Bind ViewModel.IsEmployeeSelected,Mode=OneWay}">
          <StackPanel Orientation="Horizontal">
            <SymbolIcon Symbol="Delete"/>
            <TextBlock Text="Delete" Margin="5 0 0 0"/>
          </StackPanel>
        </Button>
      </StackPanel>

      <ListView Grid.Row="1" ItemsSource="{x:Bind ViewModel.Employees,Mode=OneWay}" SelectedItem="{x:Bind ViewModel.SelectedEmployee,Mode=TwoWay}">
        <ListView.ItemTemplate>
          <DataTemplate x:DataType="model:Employee">
            <StackPanel Orientation="Horizontal">
              <TextBlock Text="{x:Bind FirstName,Mode=OneWay}" FontWeight="Bold"/>
              <TextBlock Text="{x:Bind LastName,Mode=OneWay}" Margin="5 0 0 0"/>
            </StackPanel>
          </DataTemplate>
        </ListView.ItemTemplate>
      </ListView>
    </Grid>

    <!-- Employee detail -->
    <controls:EmployeeDetailControl Grid.Row="1" Grid.Column="1" Employee="{x:Bind ViewModel.SelectedEmployee,Mode=OneWay}" Visibility="{x:Bind ViewModel.IsEmployeeSelected,Mode=OneWay}"/>
  </Grid>
</Page>

This page uses a combination of the Grid and StackPanel controls to layout the page. It has two buttons on the sidebar. One to add a new employee, with its click event bound to the AddEmployee method in the ViewModel class. Another to delete an employee record, with its click event bound to the DeleteEmployee method. You should notice the “ also on the sidebar. It is bound to the Employees property which is an observable collection of Employee. It will display an employee’s first and last name, and when it’s selected, sets the value for ViewModel.SelectedEmployee.

Then you have the “ which is displayed only if an employee is selected in the ListView, and its Employee property is bound to ViewModel.SelectedEmployee.

That’s all we need for this app. But, because I’m working with VS Code, I’m going to update MyWasmApp.Shared.projitems so that the files I added will get compiled alongside the default files that were added when the project was created. Open the file and add the follow statement after line 20:

xml
<Compile Include="$(MSBuildThisFileDirectory)MainPage.xaml.cs">
  <DependentUpon>MainPage.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Controls\EmployeeDetailControl.xaml.cs">
  <DependentUpon>EmployeeDetailControl.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Model\Employee.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Model\ModelConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Model\Observable.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Model\ViewModel.cs" />

Then add the below after line 36:

xml
<Page Include="$(MSBuildThisFileDirectory)Controls\EmployeeDetailControl.xaml">
  <SubType>Designer</SubType>
  <Generator>MSBuild:Compile</Generator>
</Page>

That’s A Wrap!

Now you have a SPA that runs on WASM! You can build a native-like app experience with this pattern and be able to apply patterns like MVVM and break your UI into XAML components. In a future post, I will talk about routing/navigation.

To run the app, start the app using the “.NET Core Launch (Uno Platform App)” launch configuration. When this is done, it starts the server and you can navigate to localhost:5000 to see your running app.

You can get the source code on GitHub

 

Guest post by Peter Mbanugo.

Peter is a software developer, tech writer, and maker of Hamoni Sync. When he’s not building tech products, he spends his time learning and sharing his knowledge on topics related to GraphQL, Offline-First, and recently WebAssembly. He’s also a contributor to Hoodie and a member of the Offline-First community. You can follow him on Twitter.

Tune in Today at 12 PM EST for our free Uno Platform 5.0 Live Webinar
Watch Here