Counter App using C# Markup and MVUX

Download the complete C# Markup + MVUX sample

Note

Estimated time to complete: 10 minutes

Introduction

This tutorial will walk you through creating a simple counter application with Uno Platform. The application will have a Button that increments the counter, a TextBox where the user can enter the step size, and a TextBlock that displays the current value of the counter.

Counter App

In this tutorial, you will learn how to:

  • Create a new Project with Uno Platform using Visual Studio Template Wizard or the dotnet new command
  • Add elements to the C# file, using C# Markup, to define the layout of the application
  • Add code to the C# file to implement the application logic using the Model-View-Update-eXtended (MVUX) pattern
  • Use data binding to connect the UI to the application logic

To complete this tutorial, you don't need any prior knowledge of the Uno Platform or C#.

If you're a more experienced developer, you may want to skip this tutorial and jump straight to either the SimpleCalculator or the TubePlayer sample. Both are more complex and will give you a better idea of what Uno Platform can do.

Create the Application

To get started, we're going to use the Uno Platform solution template with the simplest set of options selected. The solution template can be accessed using either Visual Studio's New Project wizard or the command line.

Note

If you don't have the Uno Platform Extension for Visual Studio installed, follow these instructions.

  • Launch Visual Studio and click on Create new project on the Start Window. Alternatively, if you're already in Visual Studio, click New, Project from the File menu.

  • Type Uno Platform in the search box

  • Click Uno Platform App, then Next

  • Name the project Counter and click Create

At this point you'll enter the Uno Platform Template Wizard, giving you options to customize the generated application. For this tutorial, we're only going to configure the markup language and the presentation framework.

  • Select Blank in Presets selection

  • Select the Presentation tab and choose MVUX

  • Select the Markup tab and choose C# Markup

Before completing the wizard, take a look through each of the sections and see what other options are available. You can always come back and create a new project with different options later. For more information on all the template options, see Using the Uno Platform Template.

  • Click Create to complete the wizard

The template will create a solution with a single cross-platform project, named Counter, ready to run.

At this point, the newly created application can be opened in Visual Studio or your preferred IDE. The image below displays the Counter project, which includes files for the application's layout and business logic. Additionally, the project contains a Platforms folder for platform-specific files, a Strings folder for localization, and an Assets folder for images, icons, and other assets.

Counter Solution

Before proceeding, you should select a target platform and run the application. For more information on debugging an application, see for Visual Studio, Visual Studio Code, or Rider.

MainWindow and MainPage

The majority of an Uno Platform application is defined in a project, in this case, named Counter. This project contains files that define the layout of the application and files that implement the application logic.

The startup logic for the application is contained in the App.xaml.cs file. In the OnLaunched method, the MainWindow of the application is initialized with a Frame, used for navigation between pages, and the MainPage is set as the initial page.

The layout for the MainPage is defined in the MainPage.cs file. This file contains the C# Markup that defines the layout of the application.

namespace Uno;

public sealed partial class MainPage : Page
{
    public MainPage()
    {
        this.Background(ThemeResource.Get<Brush>("ApplicationPageBackgroundThemeBrush"))
            .Content(
                new StackPanel()
                    .VerticalAlignment(VerticalAlignment.Center)
                    .HorizontalAlignment(HorizontalAlignment.Center)
                    .Children(
                        new TextBlock()
                            .Text("Hello Uno Platform!")
                    )
            );
    }
}

This defines a page with a background set to the theme resource ApplicationPageBackgroundThemeBrush, meaning it will adapt to the theme (Dark or Light) of the application.

The page contains a StackPanel, which will lay out controls in a vertical stack and is aligned in the center of the page, both horizontally and vertically. The StackPanel contains a single TextBlock control, which displays the text Hello Uno Platform and is aligned in the horizontal center of the StackPanel.

Add a Control

We're going to replace the existing TextBlock with an Image but before we can do this, we need to add the image file to the application. Download this SVG image and add it to the Assets folder. At this point, you should rebuild the application in order for the image to be included in the application package.

Note

If you're working in Visual Studio, select the newly added logo.svg file in the Solution Explorer, open the Properties tool window, and make sure the Build Action property is set to UnoImage. For other IDEs, no further action is required as the template automatically sets the Build Action to UnoImage for all files in the Assets folder.

Including SVG files with the UnoImage build action will use Uno.Resizetizer to convert the SVG file to a PNG file for each platform. The generated PNG files will be included in the application package and used at runtime. For more information on using Uno.Resizetizer in Uno Platform, see Get Started with Uno.Resizetizer.

Now that we have the image file, we can replace the TextBlock with an Image.

  • Open the MainPage.cs file.

  • Replace the TextBlock with the following Image element.

    new Image()
        .Width(150)
        .Height(150)
        .Source("ms-appx:///Assets/logo.png")
    
  • The Width and Height have been set on the Image to ensure the image is displayed at the correct size. The Source property has been set to the path of the image file.

Run the application to see the updated MainPage. You should see the image displayed in the center of the page. Keep the application running whilst completing the rest of this tutorial. Hot Reload is used to automatically update the running application as you make changes to the application. For more information on Hot Reload, see Hot Reload.

Change the Layout

The layout of the application uses a StackPanel which allows multiple controls to be added as children and will layout them in a vertical stack. An alternative to the StackPanel that is often used to control layout within an Uno Platform application is the Grid. The Grid allows controls to be laid out in rows and columns, and is often used to create more complex layouts.

A StackPanel is a good choice for this application as we want the controls to be laid out vertically, one above the other. Let's go ahead and add the remaining controls for the counter.

  • Update the StackPanel to remove the HorizontalAlignment property, as we'll be centering each of the nested elements individually.

    new StackPanel()
        .VerticalAlignment(VerticalAlignment.Center)
    
  • Update the Image element to center it horizontally and add a margin.

    new Image()
        .Margin(12)
        .HorizontalAlignment(HorizontalAlignment.Center)
        .Width(150)
        .Height(150)
        .Source("ms-appx:///Assets/logo.png"),
    
  • Add a TextBox to allow the user to enter the step size.

    new TextBox()
        .Margin(12)
        .HorizontalAlignment(HorizontalAlignment.Center)
        .TextAlignment(Microsoft.UI.Xaml.TextAlignment.Center)
        .PlaceholderText("Step Size")
        .Text("1"),
    
  • Add a TextBlock to display the current counter value.

    new TextBlock()
        .Margin(12)
        .HorizontalAlignment(HorizontalAlignment.Center)
        .TextAlignment(Microsoft.UI.Xaml.TextAlignment.Center)
        .Text("Counter: 1"),
    
  • Add a Button to increment the counter.

    new Button()
        .Margin(12)
        .HorizontalAlignment(HorizontalAlignment.Center)
        .Content("Increment Counter by Step Size")
    

MainModel

So far, all the elements we've added to the MainPage have had their content set directly. This is fine for static content, but for dynamic content, we need to use data binding. Data binding allows us to connect the UI to the application logic, so that when the application logic changes, the UI is automatically updated.

As part of creating the application, we selected MVUX as the presentation framework. This added a reference to MVUX which is responsible for managing our Models and generating the necessary bindings.

  • Add a new class named MainModel.

  • Update the MainModel class to be a partial record.

    internal partial record MainModel
    {
    }
    
  • Add a new partial record above named Countable. This record will be responsible for updating our counter's properties all while ensuring immutability. Learn more about immutable records here.

    internal partial record Countable
    {
    }
    
  • Add the Count and Step properties to the Countable's primary constructor.

    internal partial record Countable(int Count, int Step)
    {
    }
    
  • Add an Increment method to the Countable record. The with operator allows us to create a new instance of the object.

    public Countable Increment() => this with
    {
        Count = Count + Step
    };
    
  • Add the newly created Countable as a property in the MainModel class. The type must be IState<Countable> and we use => State.Value(...) to initialize it.

    public IState<Countable> Countable => State.Value(this, () => new Countable(0, 1));
    
  • Add a method named IncrementCounter to the MainModel that will in turn call the Countable's Increment method and therefore update the counter. You can find more information on commands in MVUX here.

    public ValueTask IncrementCounter()
        => Countable.UpdateAsync(c => c?.Increment());
    

The final code for the MainModel class should look like this:

namespace Counter;

internal partial record Countable(int Count, int Step)
{
    public Countable Increment() => this with
    {
        Count = Count + Step
    };
}

internal partial record MainModel
{
    public IState<Countable> Countable => State.Value(this, () => new Countable(0, 1));

    public ValueTask IncrementCounter()
        => Countable.UpdateAsync(c => c?.Increment());
}

Data Binding

Now that we have the MainViewModel class, we can update the MainPage to use data binding to connect the UI to the application logic.

  • Let's add the DataContext to our page. To do so, add .DataContext(new MainViewModel(), (page, vm) => page before .Background(...). Remember to close the DataContext expression with a ) at the end of the code. It should look similar to the code below:

    this.DataContext(new MainViewModel(), (page, vm) => page
        .Background(ThemeResource.Get<Brush>("ApplicationPageBackgroundThemeBrush"))
        .Content(
            ...
        )
    );
    
  • Update the TextBlock by removing its current text content and replacing it with a binding expression for the Countable.Count property of the MainViewModel. Modify the existing Text property with () => vm.Countable.Count, txt => $"Counter: {txt}". The adjusted code is as follows:

    new TextBlock()
        .Margin(12)
        .HorizontalAlignment(HorizontalAlignment.Center)
        .TextAlignment(Microsoft.UI.Xaml.TextAlignment.Center)
        .Text(() => vm.Countable.Count, txt => $"Counter: {txt}")
    
  • Update the TextBox by binding the Text property to the Countable.Step property of the MainViewModel. The Mode of the binding is set to TwoWay so that the Countable.Step property is updated when the user changes the value in the TextBox.

    new TextBox()
        .Margin(12)
        .HorizontalAlignment(HorizontalAlignment.Center)
        .TextAlignment(Microsoft.UI.Xaml.TextAlignment.Center)
        .PlaceholderText("Step Size")
        .Text(x => x.Binding(() => vm.Countable.Step).TwoWay())
    
  • Update the Button to add a Command property that is bound to the IncrementCounter task of the MainViewModel.

    new Button()
        .Margin(12)
        .HorizontalAlignment(HorizontalAlignment.Center)
        .Command(() => vm.IncrementCounter)
        .Content("Increment Counter by Step Size")
    
  • The final code for MainPage.cs should look like this:

    namespace Counter;
    
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.DataContext(new MainViewModel(), (page, vm) => page
                .Background(ThemeResource.Get<Brush>("ApplicationPageBackgroundThemeBrush"))
                .Content(
                    new StackPanel()
                        .VerticalAlignment(VerticalAlignment.Center)
                        .Children(
                            new Image()
                                .Margin(12)
                                .HorizontalAlignment(HorizontalAlignment.Center)
                                .Width(150)
                                .Height(150)
                                .Source("ms-appx:///Assets/logo.png"),
                            new TextBox()
                                .Margin(12)
                                .HorizontalAlignment(HorizontalAlignment.Center)
                                .TextAlignment(Microsoft.UI.Xaml.TextAlignment.Center)
                                .PlaceholderText("Step Size")
                                .Text(x => x.Binding(() => vm.Countable.Step).TwoWay()),
                            new TextBlock()
                                .Margin(12)
                                .HorizontalAlignment(HorizontalAlignment.Center)
                                .TextAlignment(Microsoft.UI.Xaml.TextAlignment.Center)
                                .Text(() => vm.Countable.Count, txt => $"Counter: {txt}"),
                            new Button()
                                .Margin(12)
                                .HorizontalAlignment(HorizontalAlignment.Center)
                                .Command(() => vm.IncrementCounter)
                                .Content("Increment Counter by Step Size")
                        )
                )
            );
        }
    }
    

Wrap Up

At this point, you should have a working counter application. Try changing the step size and clicking the button to increment the counter.

If you want to see the completed application, you can download the source code from GitHub.