How-To Tutorials
C#
MAUI Task App
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 for the complete code, see getditto > 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 for more information, see the official microsoft net maui > 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 create the maui project name the project “dittomauitasksapp” and place it in your desired location you’ll get a blank net maui application navigate to dittomauitasksapp csprojand , for now, keep only the ios and android targets ditto 4 5 0 supports only net ios and android windows and mac support is coming soon \<targetframeworks>net7 0 android;net7 0 ios;\</targetframeworks> let’s add some required dependencies from vs code’s command pallet, open the nuget gallery (you need the extension installed) add the latest available ditto version 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(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 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 \<?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 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 c# builder services addsingleton\<ipopupservice, popupservice>(); builder services addtransient\<taskspageviewmodel>(); builder services addtransient\<taskspage>(); run the application you may have to run sudo dotnet workload restore before building from the vs code command palette, select the ios / android device you want to run the project on then go to run → start debugging and select the net maui debugger when prompted we’ll end up with an app that can create, read, update and delete tasks integrating ditto create your ditto app add permissions add ditto reference ditto create a task read tasks update a task delete a task 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 https //docs ditto live/v4 4/csharp installing from the maui project update platforms/ios/info plist \<?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> update platforms/android/androidmanifest plist \<?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 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 > docid 3vo4onmgepp4s9iam0hbz c# private static ditto setupditto() { var ditto = new ditto(dittoidentity onlineplayground("your app id", "your token", true)); ditto startsync(); return ditto; } 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 c# private readonly ditto ditto; public taskspageviewmodel(ditto ditto, ipopupservice popupservice) { this ditto = ditto; } implementing crud operations 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 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 = 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 } }); } 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 = 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 update tasks to easily handle state changes for a particular task, we’ll use a feature of mvvm toolkit https //learn microsoft com/en us/dotnet/communitytoolkit/mvvm/generators/observableproperty#running code upon changes given our two way binding defined in taskspage 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 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); } delete tasks to delete a task, we have to modify the deletetaskmethod and run an update query dql 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 c# \[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! for the complete code, see getditto > https //github com/getditto/template app maui tasks app repository in github