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.
Let’s add some required dependencies. From VS Code’s command pallet, open the NuGet Gallery (you need the extension installed).
6
Add the latest available Ditto version:
7
We’ll be using MVVM so make sure to also add CommunityToolkit.Mvvm by Microsoft.
Getting the App Ready
This section includes setting up the MAUI tasks app, without integrating Ditto.
You can check out this branch: https://github.com/getditto/template-app-maui-tasks-app/tree/tasks-app-plain to get this section completed.
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:
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:
c#
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.
In the ViewModels folder create a TasksPageviewModelwith the following content:
c#
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(newDittoTask(){
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.
There’s also an IPopupService, since we need a way to get the Task content. In an Utils folder, you can create the interface:
c#
public interface IPopupService{
Task<string>DisplayPromptAsync(string title, string message);}
And its implementation:
c#
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.
And the code-behind, simply setting the Binding Context.
c#
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:
You may have to run sudo dotnet workload restore before building.
1
From the VS Code command palette, select the iOS / Android device you want to run the project on.
2
Then go to Run → StartDebugging and select the .NET MAUI debugger when prompted.
3
We’ll end up with an app that can create, read, update and delete tasks.
Integrating Ditto
1
Create your Ditto App.
2
Add permissions.
3
Add Ditto.
4
Reference Ditto.
5
Create a task.
6
Read tasks.
7
Update a task.
8
Delete a task.
9
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.
From the MAUI project:
1
Update Platforms/iOS/Info.Plist:
XML
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPEplistPUBLIC"-//Apple//DTD PLIST 1.0//EN""http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plistversion="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>
Add the following method to create a new Ditto instance:
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 > Authentication and Initialization.
c#
private static Ditto SetupDitto(){
var ditto =newDitto(DittoIdentity.OnlinePlayground("YOUR_APP_ID","YOUR_TOKEN",true));
ditto.StartSync();return ditto;}
2
Now, having a Ditto instance created, let’s register that instance as a singleton to have it easily available across the application:
In CreateMauiApp(), before calling builder.Build() and returning, make sure to include this line:
c#
builder.Services.AddSingleton(SetupDitto());
Referencing Ditto in TasksPageViewModel
To get access to the Ditto instance, simply add it to the constructor of TasksPageviewModel and then save it in a private field.
We registered Ditto in the Service Collection, so Dependency Injection will automatically resolve the instance.
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:
c#
[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 =newDictionary<string, object>{{"body", taskData},{"isCompleted",false},{"isDeleted",false}};
await ditto.Store.ExecuteAsync($"INSERT INTO {DittoTask.CollectionName} DOCUMENTS (:doc1)",newDictionary<string, object>(){{"doc1", dict }});}
2
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.
To do that, we need to define a DQL statement:
DQL
SELECT*FROM tasks WHERE isDeleted =false
And register an observer and a subscription. The method looks like this:
c#
private void ObserveDittoTasksCollection(){
var query = $"SELECT * FROM {DittoTask.CollectionName} WHERE isDeleted = false";
ditto.Sync.RegisterSubscription(query);
ditto.Store.RegisterObserver(query, storeObservationHandler: async (queryResult)=>{
Tasks =newObservableCollection<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.
3
Update tasks:
To easily handle state changes for a particular task, we’ll use a feature of MVVM Toolkit: Running code upon changes.
Given our two-way binding defined in TasksPage.xml:
XML
<CheckBoxIsChecked="{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.
Based on the new value, we’ll run a query to update the isCompleted field for that task in the Ditto store.
c#
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);}
4
Delete tasks:
To delete a task, we have to modify the DeleteTaskmethod and run an update query:
DQL
UPDATE tasks
SET isDeleted =trueWHERE _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: