---
title: MAUI Task App
slug: best-practices/how-to/c-sharp/maui-task-app
docTags: 
createdAt: 2024-01-09T21:58:56.003Z
---

This guide shows you how to build a task list app using Ditto and .NET Maui, including how to prepare a MAUI project, set up your app, integrate Ditto, and implement basic `create`, `read`, `update`, `delete` (CRUD) operations.

:::hint{type="info"}
For the complete code, see getditto > [template-app-maui-tasks-app](https://github.com/getditto/template-app-maui-tasks-app) repository in GitHub.
:::

# Prerequisites

Before you begin, make sure you meet the following preconditions:

- Visual Studio Code with the following extensions:
  - .NET MAUI
  - C# Dev Kit
  - NuGet Gallery
- .NET version 7.0 or later.
- If developing for iOS, Xcode
- If developing for Android, Android SDK

:::hint{type="info"}
For more information, see the official Microsoft .NET MAUI > [Installation](https://learn.microsoft.com/en-us/dotnet/maui/get-started/installation?view=net-maui-8.0\&tabs=visual-studio-code) documentation.
:::

# Preparing the MAUI Project

:::::WorkflowBlock
:::WorkflowBlockItem
Create the MAUI project.

![](https://api.archbee.com/api/optimize/qoRkNxW5fJ81r_NqVpc8C/9JdJVwRs1LDTAzMknLB4F_screenshot-2023-12-14-at-125845.png)
:::

:::WorkflowBlockItem
Name the project “DittoMauiTasksApp” and place it in your desired location.&#x20;
:::

:::WorkflowBlockItem
You’ll get a blank .NET MAUI application.&#x20;
:::

::::WorkflowBlockItem
Navigate to `DittoMauiTasksApp.csprojand`, for now, keep only the iOS and Android targets:&#x20;

:::hint{type="info"}
Ditto 4.5.0 supports only .NET iOS and Android. Windows and Mac support is coming soon.&#x20;
:::

```javascript
<TargetFrameworks>net7.0-android;net7.0-ios;</TargetFrameworks>
```
::::

:::WorkflowBlockItem
Let’s add some required dependencies. From VS Code’s command pallet, open the NuGet Gallery (you need the extension installed).

![](https://api.archbee.com/api/optimize/qoRkNxW5fJ81r_NqVpc8C/iXYUPRQcfJSQWqDGfBo1O_screenshot-2023-12-14-at-131526.png)
:::

:::WorkflowBlockItem
Add the latest available Ditto version:&#x20;

![](https://api.archbee.com/api/optimize/qoRkNxW5fJ81r_NqVpc8C/uT9NTGRU8jeuGokhqCYaW_setup-nuget.png)
:::

:::WorkflowBlockItem
We’ll be using MVVM so make sure to also add **CommunityToolkit.Mvvm** by Microsoft.&#x20;
:::
:::::



# Getting the App Ready&#x20;

This section includes setting up the MAUI tasks app, without integrating Ditto.&#x20;

:::hint{type="info"}
You can check out this branch: https\://github.com/getditto/template-app-maui-tasks-app/tree/tasks-app-plain to get this section completed.&#x20;
:::

## Understanding the Structure

Before integrating Ditto, let’s create the app skeleton. We’ll be using the Shell-based template and the MVVM architectural pattern. We’ll have a single page application, so this means we’ll have:&#x20;

**Model** - Task

**ViewModel** - TaskPageViewModel

**View** - TasksPage

## Add the Model

For this application, the model will be a simple `Task`, with an Id, a body and a `completed` flag. In the `Model` folder let’s add the `DittoTask` class:&#x20;

:::CodeblockTabs
c#

```csharp
using CommunityToolkit.Mvvm.ComponentModel;

namespace DittoMauiTasksApp
{
    public partial class DittoTask : ObservableObject
    {
        [ObservableProperty]
        string id;

        [ObservableProperty]
        string body;

        [ObservableProperty]
        bool isCompleted;
    }
}
```
:::

## Add the ViewModel

The ViewModel will handle the logic behind the view. This means storing and working with the list of tasks that is going to be displayed on the UI.&#x20;

In the `ViewModels` folder create a `TasksPageviewModelwith` the following content:&#x20;

:::CodeblockTabs
c#

```csharp
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DittoMauiTasksApp.Utils;

namespace DittoMauiTasksApp.ViewModels
{
    public partial class TasksPageviewModel : ObservableObject
    {
        private readonly IPopupService popupService;

        [ObservableProperty]
        ObservableCollection<DittoTask> tasks = new();

        public TasksPageviewModel(IPopupService popupService)
        {
            this.popupService = popupService;
        }

        [RelayCommand]
        private async Task AddTaskAsync()
        {
            string taskData = await popupService.DisplayPromptAsync("Add Task", "Add a new task:");

            if (taskData == null)
            {
                //nothing was entered. 
                return; 
            }

            Tasks.Add(new DittoTask() {
                IsCompleted = false,
                Body = taskData
            });
        }

        [RelayCommand]
        private void DeleteTask(DittoTask task)
        {
            Tasks.Remove(task);
        }
    }
}
```
:::

The ViewModel simply has an `ObservableCollection` of `DittoTask` and two commands: to remove and to delete a task.&#x20;

There’s also an `IPopupService`, since we need a way to get the Task content. In an `Utils` folder, you can create the interface:&#x20;

:::CodeblockTabs
c#

```csharp
public interface IPopupService
{
    Task<string> DisplayPromptAsync(string title, string message);
}
```
:::

And its implementation:&#x20;

:::CodeblockTabs
c#

```csharp
public class PopupService : IPopupService
{
    public Task<string> DisplayPromptAsync(string title, string message)
    {
        Page page = Application.Current?.MainPage;
        return page.DisplayPromptAsync(title, message);
    }
}
```
:::

## Add the View

Create a new folder `Views` and add move the existing `MainPage.xaml` (and `MainPage.xaml.cs`) there. For additional clarity, these can be renamed to TasksPage(don’t forget to change the name in `AppShell.xaml`). We’ll replace the content with ours - simply a list that will be displaying the Task content and a checkbox - to track it’s completeness.&#x20;

```xml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             Title="Ditto Tasks"
             x:Class="DittoMauiTasksApp.TasksPage">

    <ContentPage.ToolbarItems>
        <ToolbarItem Text="Add Task"
                     Command="{Binding AddTaskCommand}"/>

    </ContentPage.ToolbarItems>

    <ListView
        x:Name="listView"
        ItemsSource="{Binding Tasks}"
        SelectionMode="None">
        <ListView.ItemTemplate>
            <DataTemplate>
                <ViewCell>
                    <ViewCell.ContextActions>
                        <MenuItem
                            Text="Delete"
                            Command="{Binding Source={x:Reference listView}, Path=BindingContext.DeleteTaskCommand}"
                            CommandParameter="{Binding}"/>
                    </ViewCell.ContextActions>
                    <Grid
                        Margin="16, 5"
                        ColumnDefinitions="*, Auto">
                        <Label
                            VerticalOptions="Center"
                            Text="{Binding Body}"/>
                        <CheckBox
                            HorizontalOptions="End"
                            IsChecked="{Binding IsCompleted}"/>
                    </Grid>
                </ViewCell>
            </DataTemplate>
        </ListView.ItemTemplate>

    </ListView>

</ContentPage>
```

And the code-behind, simply setting the Binding Context.&#x20;

:::CodeblockTabs
c#

```csharp
using DittoMauiTasksApp.ViewModels;

namespace DittoMauiTasksApp;

public partial class TasksPage : ContentPage
{
    public TasksPage(TasksPageviewModel viewModel)
    {
        InitializeComponent();

        BindingContext = viewModel; 
    }
}
```
:::

## Register Services

Before running the application, you must handle the service registration. In `MauiProgram.cs`, under `CreateMauiApp()`, before returning make sure to include:&#x20;

:::CodeblockTabs
c#

```csharp
builder.Services.AddSingleton<IPopupService, PopupService>();
builder.Services.AddTransient<TasksPageviewModel>();
builder.Services.AddTransient<TasksPage>();
```
:::

## Run the Application

:::hint{type="info"}
You may have to run `sudo dotnet workload restore` before building.&#x20;
:::

::::WorkflowBlock
:::WorkflowBlockItem
From the VS Code command palette, select the iOS / Android device you want to run the project on.&#x20;

![](https://api.archbee.com/api/optimize/qoRkNxW5fJ81r_NqVpc8C/Y9vmP8qoENcUyrD78khNZ_screenshot-2023-12-14-at-161332.png)
:::

:::WorkflowBlockItem
Then go to **Run** → **Start** **Debugging** and select the **.NET MAUI** debugger when prompted.&#x20;

![](https://api.archbee.com/api/optimize/qoRkNxW5fJ81r_NqVpc8C/5H9vn7CRLa-Kr0YDi52gR_screenshot-2023-12-14-at-161418.png)
:::

:::WorkflowBlockItem
We’ll end up with an app that can create, read, update and delete tasks.&#x20;

![](https://api.archbee.com/api/optimize/qoRkNxW5fJ81r_NqVpc8C/bcPe1jZV4FknvP83NDNqK_recordingplain.gif)
:::
::::

# Integrating Ditto

::::WorkflowBlock
:::WorkflowBlockItem
Create your Ditto App.
:::

:::WorkflowBlockItem
Add permissions.
:::

:::WorkflowBlockItem
Add Ditto.
:::

:::WorkflowBlockItem
Reference Ditto.
:::

:::WorkflowBlockItem
Create a task.
:::

:::WorkflowBlockItem
Read tasks.
:::

:::WorkflowBlockItem
Update a task.
:::

:::WorkflowBlockItem
Delete a task.
:::

:::WorkflowBlockItem
Run the app.
:::
::::

## Creating your Ditto App

We first need to create a new app in the \[Ditto Portal]\(https\://portal.ditto.live/). Apps created on the portal will automatically sync data between them and also to the Ditto Big Peer.

Each app created on the portal has a unique appID which can be seen on your app's settings page once the app has been created. This ID is used in subsequent sections to configure your Ditto instance.

## Adding Permissions

For Ditto to fully use all the network transports like Bluetooth Low Energy, Local Area Network, Apple Wireless Direct, the app will need to ask the user for permission. For that, platform-specific instructions should be followed, so on iOS these must be specified in `Info.Plist` while on Android, there is `AndroidManifest.xml`. For more detailed instructions, see [Installing C# SDK](https://docs.ditto.live/v4-4/csharp-installing).

From the MAUI project:

::::WorkflowBlock
:::WorkflowBlockItem
Update `Platforms/iOS/Info.Plist`:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>LSRequiresIPhoneOS</key>
	<true/>
	<key>UIDeviceFamily</key>
	<array>
		<integer>1</integer>
		<integer>2</integer>
	</array>
	<key>UIRequiredDeviceCapabilities</key>
	<array>
		<string>arm64</string>
	</array>
	<key>UISupportedInterfaceOrientations</key>
	<array>
		<string>UIInterfaceOrientationPortrait</string>
		<string>UIInterfaceOrientationLandscapeLeft</string>
		<string>UIInterfaceOrientationLandscapeRight</string>
	</array>
	<key>UISupportedInterfaceOrientations~ipad</key>
	<array>
		<string>UIInterfaceOrientationPortrait</string>
		<string>UIInterfaceOrientationPortraitUpsideDown</string>
		<string>UIInterfaceOrientationLandscapeLeft</string>
		<string>UIInterfaceOrientationLandscapeRight</string>
	</array>
	<key>XSAppIconAssets</key>
	<string>Assets.xcassets/appicon.appiconset</string>
	<key>NSBluetoothAlwaysUsageDescription</key>
	<string>Uses Bluetooth to connect and sync with nearby devices</string>
	<key>NSLocalNetworkUsageDescription</key>
	<string>Uses WiFi to connect and sync with nearby devices</string>
	<key>NSBonjourServices</key>
	<array>
		<string>_http-alt._tcp.</string>
	</array>
</dict>
</plist>
```
:::

:::WorkflowBlockItem
Update `Platforms/Android/AndroidManifest.Plist`:

```xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
	<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true"></application>
	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
	<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
	<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
	<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" tools:targetApi="s" />
	<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" tools:targetApi="s" />
	<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" tools:targetApi="s" />
	<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="32" />
	<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="30" />
	<uses-permission android:name="android.permission.INTERNET" />
	<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
	<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
	<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
	<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
	<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" android:usesPermissionFlags="neverForLocation" tools:targetApi="32" />
</manifest>
```
:::
::::

## Adding Ditto to MauiProgram.cs

In `MauiProgram.cs`:

:::::WorkflowBlock
::::WorkflowBlockItem
Add the following method to create a new Ditto instance:&#x20;

:::hint{type="warning"}
This tutorial uses a playground identity for authentication to demonstrate functionality and is not intended for realworld use in product-level apps.

Deploying playground certificates to a live environment could lead to vulnerabilities and security risks. For more information, see *Ditto Basics&#x20;*> [Authentication and Initialization](docId:3Vo4OnmGEpp4S9IAm0HBZ).
:::

:::CodeblockTabs
c#

```csharp
private static Ditto SetupDitto()
{
    var ditto = new Ditto(DittoIdentity.OnlinePlayground("YOUR_APP_ID", "YOUR_TOKEN", true));
    ditto.StartSync();

    return ditto;
}
```
:::
::::

::::WorkflowBlockItem
Now, having a Ditto instance created, let’s register that instance as a singleton to have it easily available across the application:&#x20;

In `CreateMauiApp()`, before calling `builder.Build()` and returning, make sure to include this line:&#x20;

:::CodeblockTabs
c#

```csharp
builder.Services.AddSingleton(SetupDitto());
```
:::
::::
:::::

## Referencing Ditto in TasksPageViewModel&#x20;

To get access to the Ditto instance, simply add it to the constructor of `TasksPageviewModel` and then save it in a private field.&#x20;

We registered Ditto in the Service Collection, so Dependency Injection will automatically resolve the instance.&#x20;

:::CodeblockTabs
c#

```csharp
private readonly Ditto ditto;

public TasksPageviewModel(Ditto ditto, IPopupService popupService)
{
    this.ditto = ditto;
    ...
}
```
:::

## Implementing CRUD Operations

:::::WorkflowBlock
::::WorkflowBlockItem
Create tasks:

To create a Task, we have to modify the AddTaskAsyncmethod. We’ll be getting the task data in the same way, but this time, instead of adding it to the tasks `ObservableCollection`, we'll execute a DQL statement to add the task to the Ditto Store:

:::CodeblockTabs
c#

```csharp
[RelayCommand]
private async Task AddTaskAsync()
{
    string taskData = await popupService.DisplayPromptAsync("Add Task", "Add a new task:");

    if (taskData == null)
    {
        //nothing was entered. 
        return; 
    }

    var dict = new Dictionary<string, object>
    {
        {"body", taskData},
        {"isCompleted", false},
        { "isDeleted", false }
    };

    await ditto.Store.ExecuteAsync($"INSERT INTO {DittoTask.CollectionName} DOCUMENTS (:doc1)", new Dictionary<string, object>()
    {
        { "doc1", dict }
    });
}
```
:::
::::

::::WorkflowBlockItem
Read tasks:

The read part is actually an observe task where we want to monitor changes to the tasks collection and get updates from other peers as well.&#x20;

To do that, we need to define a DQL statement:

:::CodeblockTabs
DQL

```mysql
SELECT * FROM tasks WHERE isDeleted = false
```
:::

And register an observer and a subscription. The method looks like this:  &#x20;

:::CodeblockTabs
c#

```csharp
private void ObserveDittoTasksCollection()
{
    var query = $"SELECT * FROM {DittoTask.CollectionName} WHERE isDeleted = false";

    ditto.Sync.RegisterSubscription(query);
    ditto.Store.RegisterObserver(query, storeObservationHandler: async (queryResult) =>
    {
        Tasks = new ObservableCollection<DittoTask>(queryResult.Items.ConvertAll(d =>
        {
            return JsonSerializer.Deserialize<DittoTask>(d.JsonString());
        }));
    });

    ditto.Store.ExecuteAsync($"EVICT FROM {DittoTask.CollectionName} WHERE isDeleted = false");
}
```
:::

And this needs to be called from the `TasksPageviewModel`'s constructor.&#x20;
::::

::::WorkflowBlockItem
Update tasks:

To easily handle state changes for a particular task, we’ll use a feature of MVVM Toolkit:  [Running code upon changes.](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/generators/observableproperty#running-code-upon-changes)

Given our two-way binding defined in `TasksPage.``xml`:

```xml
<CheckBox IsChecked="{Binding IsCompleted}"/>
```

When a user clicks on the checkbox, the value of `IsCompleted` is going to change. We’ll monitor this directly in the `DittoTask` class.&#x20;

Based on the new value, we’ll run a query to update the **isCompleted** field for that task in the Ditto store.&#x20;

:::CodeblockTabs
c#

```csharp
partial void OnIsCompletedChanged(bool value)
{
    var ditto = Utils.ServiceProvider.GetService<Ditto>();

    var updateQuery = $"UPDATE {CollectionName} " +
        $"SET isCompleted = {value} " +
        $"WHERE _id = '{Id}' AND isCompleted != {value}";
    ditto.Store.ExecuteAsync(updateQuery);
}
```
:::
::::

::::WorkflowBlockItem
Delete tasks:

To delete a task, we have to modify the DeleteTaskmethod and run an update query:&#x20;

:::CodeblockTabs
DQL

```mysql
UPDATE tasks
SET isDeleted = true 
WHERE _id = {task.Id}
```
:::

Running this query will also notify the registered observer, so there’s no need to update the Tasks collection manually.

This is how the `DeleteTask` method should look like:&#x20;

:::CodeblockTabs
c#

```csharp
[RelayCommand]
private void DeleteTask(DittoTask task)
{
    var updateQuery = $"UPDATE {DittoTask.CollectionName} " +
        "SET isDeleted = true " +
        $"WHERE _id = '{task.Id}'";
    ditto.Store.ExecuteAsync(updateQuery);
}
```
:::
::::
:::::

## Running the App

Congratulations you are now complete with the Ditto .NET MAUI task app!

:::hint{type="info"}
For the complete code, see getditto > [template-app-maui-tasks-app](https://github.com/getditto/template-app-maui-tasks-app) repository in GitHub.
:::

![](https://api.archbee.com/api/optimize/qoRkNxW5fJ81r_NqVpc8C/TxBpQ9PGI7uCbvbeYAPm-_output.gif)

