My Xamarin (C#/.NET) Full-Sync ToDo App

Prerequisites

Before we get started we need a few things setup; the prerequisites for this project are:

  • Visual Studio (this guide was written with 7.5.3 - Visual Studio for Mac)

  • You will need your Realm Cloud instance URL that was generated when you created your instance (it can be found by logging in to the cloud portal, and clicking the Copy Instance URL link)

This tutorial should take around 20-30 minutes

Want to get started right away with the complete source code? Check out our Github with ready-to-compile source code then follow the instructions in README.md to get started. Don't forget to update the Constants.cs file with your Realm Cloud or self-hosted instance URL before running the app.

Step 1: Create a New Xamarin Project

Open Visual Studio for Mac and select "New Project..." Then go to Multiplatform>App>Blank Forms App and click Next. Name it "XamarinToDoApp" and make sure both platforms, iOS and Android, are selected and that you are using a .NET Standard shared project. The Organization Identifier can be whatever you want. On the next screen make sure the Project Name and Solution Name are something easy for you to remember and save it somewhere convenient for you and click "Create."

Step 2: Initial Run Test

Before we dive into adding code, it's a good idea to make sure that Visual Studio is configured correctly and that we can successfully compile the app template we just created. In order to do this, select a simulator (for example the iPhoneX) from the build menu, then press the build/run icon.

Step 3: Installing the Realm Frameworks

With the foundation in place, we need to add Realm to it. Our solution has three projects, the Android app, the iOS app, and the .NET Standard library for shared code. Do this by expanding the projects in the solution view, right-clicking on the Packages node, and clicking “Add Packages…” You will need to add the dependencies for all three projects.

Search for “Realm” in the search box, select it, and add it. Only the single Realm package is needed - adding Realm.Database is not needed and may causes issues on some Windows platforms.

In the same nuget pop-up search for the package Acr.UserDialogs and add it to the project as well.

Step 4: Add the FodyWeavers

You may have noticed that your project does not compile and run anymore, we need to add the weavers for this to run. Go to the shared code XamarinToDoApp project, right-click and select Add>New File... then select Empty XML File and name it FodyWeavers

For each Project(shared, Android, iOS) add a blank XML file calledFodyWeavers.xml

FodyWeavers.xml
<?xml version="1.0" encoding="UTF-8" ?>
<Weavers>
<Realm />
</Weavers>

You should now be able to build and run the app.

Step 5: Adding the Realm Model for ToDo

Go to the shared code XamarinToDoApp project, right-click and select Add>New File... then select Empty Class and name it Item.

Item.cs
using Realms;
using System;
namespace XamarinToDoApp
{
public class Item : RealmObject
{
[PrimaryKey]
[MapTo("itemId")]
public string ItemId { get; set; } = Guid.NewGuid().ToString();
[MapTo("body")]
public string Body { get; set; }
[MapTo("isDone")]
public bool IsDone { get; set; }
[MapTo("timestamp")]
public DateTimeOffset Timestamp { get; set; }
}
}

You should now be able to build and run the app.

Step 6: Setup the User Interface

Now that we have our model in place, we’ll create its corresponding UI. Let’s create a new Page in the portable project, where all of our code will go. Make sure to select Forms > Forms ContentPage XAML in the New File dialog. Name it ItemEntriesPage. This will contain the ListView that displays the items and will be the main screen of the app. Open the ItemEntriesPage.xaml and add the following XML:

ItemEntriesPage.xaml
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="XamarinToDoApp.ItemEntriesPage"
Title="ToDo">
<ContentPage.ToolbarItems>
<ToolbarItem Text="Log Out" Command="{Binding LogOutCommand}" />
<ToolbarItem Text="Add" Command="{Binding AddEntryCommand}" />
</ContentPage.ToolbarItems>
<ContentPage.Content>
<ListView ItemsSource="{Binding Entries}" x:Name="EntriesListView" ItemSelected="OnItemSelected">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Body}">
<TextCell.ContextActions>
<MenuItem Text="Delete" IsDestructive="true"
Command="{Binding BindingContext.DeleteEntryCommand, Source={x:Reference EntriesListView}"
CommandParameter="{Binding .}" />
</TextCell.ContextActions>
</TextCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage.Content>
</ContentPage>

We’ll set this as the MainPage of the App class that Xamarin.Forms generated for you in App.xaml.cs

App.xaml.cs
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
namespace XamarinToDoApp
{
public partial class App : Application
{
public App()
{
InitializeComponent();
MainPage = new NavigationPage(new ItemEntriesPage());
}
}
}

Step 7: Add the ViewModel for the ToDo Page

We’ll also need a ViewModel for our Page, so let’s right-click on the shared project and Add a New Class file just like before called ItemEntriesViewModel.cs

And add the following libraries with the using statement, we will use them later:

ItemEntriesViewModel.cs
using Acr.UserDialogs;
using Realms;
using Realms.Sync;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Xamarin.Forms;
using Credentials = Realms.Sync.Credentials;
namespace XamarinToDoApp
{
public class ItemEntriesViewModel
{
public ItemEntriesViewModel()
{
}
}
}

And wire it up in the page’s code-behind in ItemEntriesPage.xaml.cs

ItemEntriesPage.xaml.cs
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace XamarinToDoApp
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class ItemEntriesPage : ContentPage
{
public ItemEntriesPage()
{
InitializeComponent();
BindingContext = new ItemEntriesViewModel();
}
private void OnItemSelected(object sender, SelectedItemChangedEventArgs e)
{
(sender as ListView).SelectedItem = null;
}
}
}

You can now run the app, but you’ll end up with an empty list and buttons that don't do anything, let’s do something about that!

Step 8: Create a Constants File

Right Click on our Project and Add a new Class called Constants.cs

Constants.cs
namespace XamarinToDoApp
{
public static class Constants
{
// **** Realm Cloud Users:
// **** Replace MY_INSTANCE_ADDRESS with the hostname of your cloud instance
// **** e.g., "mycoolapp.us1.cloud.realm.io"
// ****
// ****
// **** ROS On-Premises Users
// **** Replace the AUTH_URL and REALM_URL strings with the fully qualified versions of
// **** address of your ROS server, e.g.: "http://127.0.0.1:9080" and "realm://127.0.0.1:9080"
public static string AuthUrl = "https://awesome-metal-cheese.us1.cloud.realm.io"; // <- update this
public static string RealmPath = "ToDo";
}
}

Step 9: Bind Realm to the ViewModel

A Realm is an object that encapsulates a database. We’ll let our ViewModel manage the realm. Define Realm at the top below the ItemEntriesViewModel class:

ItemEntriesViewModel.cs
using Acr.UserDialogs;
using Realms;
using Realms.Sync;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Xamarin.Forms;
namespace XamarinToDoApp
{
public class ItemEntriesViewModel : INotifyPropertyChanged
{
private Realm _realm;
private IEnumerable<Item> _entries;
public IEnumerable<Item> Entries
{
get { return _entries; }
private set
{
// Do nothing if there is no actual change.
if (_entries == value)
{
return;
}
_entries = value;
// Let the UI know a change has occurred.
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(nameof(Entries)));
}
}
public ICommand LogOutCommand { get; private set; }
public ICommand DeleteEntryCommand { get; private set; }
public ICommand AddEntryCommand { get; private set; }
public event PropertyChangedEventHandler PropertyChanged;
public ItemEntriesViewModel()
{
}
}
}

We're using the command pattern to wire up the buttons defined in the XAML to this view model. The commands are declared as properties above: LogOutCommand, DeleteEntryCommand, and AddEntryCommand.

Now configure them in the class constructor:

ItemEntriesViewModel.cs
public ItemEntriesViewModel()
{
LogOutCommand = new Command(() =>
{
if (User.Current == null)
{
return;
}
User.Current.LogOutAsync().IgnoreResult();
_realm = null;
Entries = null;
StartLoginCycle().IgnoreResult();
});
DeleteEntryCommand = new Command<Item>(DeleteEntry);
AddEntryCommand = new Command(() => AddEntry().IgnoreResult());
StartLoginCycle().IgnoreResult();
}

Here, DeleteEntry is a simple method to do the actual deletion and should be added to the ItemEntriesViewModel:

ItemEntriesViewModel.cs
private void DeleteEntry(Item entry) => _realm.Write(() => _realm.Remove(entry));

Now we have an app that displays item entries straight from a Realm database using concise XAML and data binding.

Note that we will be exposing the available entries by making a simple query to the Realm. Realm.All<>()will give us the entire set of a given type that exists in the Realm. As it is a live query which auto-refreshes with the latest state, we can simply bind our list view to it and it will be continuously up to date.

Step 10: Add the StartLoginCycle and LogIn methods

Below the ItemEntriesViewModel constructor add a new asynchronous function called StartLoginCycle() This function which is called in the ViewModel constructor will continuously attempt to call the next function, LogIn(), until it succeeds. Don't worry: since these functions use async/await, we are not locking up the UI!

ItemEntriesViewModel.cs
private async Task StartLoginCycle()
{
// Yield first in case this is called from the constructor, when not
// everything will be loaded and the dialog may not show.
do
{
await Task.Yield();
} while (!await LogIn());
}

Next we need the LogIn() method, which will ask the user to log in if not already logged in, then open the realm and fetch item entries. This method returns true when it has successfully logged in and loaded the realm.

Errors are displayed to the user in an alert dialog.

Since we are looping in the StartLoginCycle()method until this method returns true, the app effectively tries to log in until it succeeds.

ItemEntriesViewModel.cs
private async Task<bool> LogIn()
{
try
{
var user = User.Current;
if (user == null)
{
// Not already logged in.
var loginResult = await UserDialogs.Instance.LoginAsync("Log in", "Enter a username and password");
if (!loginResult.Ok)
{
return false;
}
// Create credentials with the given username and password.
// Leaving the third parameter null allows a user to be registered
// if one does not already exist for this username.
var credentials = Realms.Sync.Credentials.UsernamePassword(loginResult.LoginText, loginResult.Password);
// Log in as the user.
user = await User.LoginAsync(credentials, new Uri(Constants.AuthUrl));
}
Debug.Assert(user != null);
var configuration = new FullSyncConfiguration(new Uri(Constants.RealmPath, UriKind.Relative), user);
_realm = await Realm.GetInstanceAsync(configuration);
// Get the list of items.
Entries = _realm.All<Item>().OrderBy(i => i.Timestamp);
Console.WriteLine("Login successful.");
return true;
}
catch (Exception ex)
{
// Display the error message.
await Application.Current.MainPage.DisplayAlert("Error", ex.Message, "OK");
return false;
}
}

Step 11: Define the AddEntry Function

Below the LogIn() method, let's define the AddEntry function. This function uses the Acr.UserDialogs library to show a pop-up to the user which prompts them to type a new item into the textbox. If there is a result it writes this text in a new Item in a realm write transaction.

ItemEntriesViewModel.cs
private async Task AddEntry()
{
var result = await UserDialogs.Instance.PromptAsync(new PromptConfig
{
Title = "New entry",
Message = "Specify the item text",
});
if (result.Ok)
{
_realm.Write(() =>
{
_realm.Add(new Item
{
Timestamp = DateTimeOffset.Now,
Body = result.Value,
});
});
}
}

Step 12: Add an Extension

We will get a warning when we attempt to use Tasks that are not called with the await keyword - to get around this lets add a new C# class called Extensions.cs

Extensions.cs
using System;
using System.Threading.Tasks;
namespace XamarinToDoApp
{
public static class Extensions
{
public static void IgnoreResult(this Task task)
{
// This just silences the warnings when tasks are not awaited.
}
}
}

Now build and run your app and start syncing data! Be sure to use Studio to see data synced bidirectionally.

Further reading