Lab 5 - Realtime database
- π Worth: 3%
- π Due: Friday April 19, 2024 @End of class
- π Late submissions: 3 days maximum
- π₯ Submission: In class
Objectives
The next requirement in the app is to save the data.
To achieve this task, you need to implement all the CRUD operations.
The CRUD operations stand for: Create, Read, Update and Delete.
The MauiFitness
app should add (create), view (read), update and delete workout and meal items.
- We will setup a cloud base database using Firebase to save the dat and perform the CRUD operations.
- The create operation has been developed in milestone 1, but it will be updated here.
Firebase Realtime Database
The Firebase Realtime Database is a NoSQL cloud base database. It saves data in tree structure using Json
format. Each object is assigned a unique identifier, a key, which is used for the update and delete operations.
Example:
Meal
-NvDPv2vDVpsM8ioM33p
Calories:386
Description:"cereal"
Name : "Breakfast"
Time : "2024-04-11T09:00:00"
-NvDPyz-cl8ezW8qjfZk
Calories:262.9
Description:"Pizza"
Name:"Lunch"
Time:"2024-04-11T13:00:00"
The installed FirebaseDatabase.net
NuGet package has a Xamarin.Forms
sample project code. The proposed implementation below is based on the sample code for the offline database example. This sample code will you in building a database for your project.
Setup - Firebase Realtime database
-
Go to https://console.firebase.google.com/
-
In the welcome page, click on the MauiFitness project. (Created in the previous Lab
Authentication
) -
From the menu (left), click on
All Products
>Realtime Database
> ClickCreate Database
and choose the following settings:- Region:
United States (us-central1)
- Security rules:
test mode
- Once done click
Enable
- Region:
-
From the Realtime Database page, get the database link.
-
Copy the database URL link, should look like:
-
Add the value to the
appsettings.json
-
Add an attribute to
Settings
class in theMauiFitness
app- Do not forget to have it as:
public
- Do not forget to have it as:
-
-
Right-click on the solution and choose
Manage NuGet Packages
-
Go to
Browse
tab > Search forFirebaseDatabase.net
.- Install latest version
4.2.0
. (Tested to be working withMAUI
&.NET 7.0
)
- Install latest version
-
IHasUKey
and the IDataStore<T>
interfaces
In efforts of avoiding code repetition, we will introduce some interfaces that will allow us to treat objects as the interface to help is the CRUD operations.
In the MauiFitness
app, create a folder called Interfaces
.
-
Create an interface called
IHasUKey
in theInterfaces
folder:public interface IHasUKey
- Used to ensure that the saved model class has a property when saving to the database.
- Has public string property
Key
with a getter and setter.
-
Implement the
IHasUKey
interface in both model classesWorkout
andMeal
.- Add a private string property
key
- Use it within the getter of the public property
Key
- In addition to the currently implemented
Fody
PropertyChanged
- Add a private string property
-
For the
Workout
item, update theCopy()
method to include theKey
. This method is important to ensure that we can create deep copies of required in the edit form of theWorkoutPage
. -
Create an interface called
IDataStore<T>
in theInterfaces
folder:public interface IDataStore<T>
-
Will be used by the database service that we will create to provide CRUD methods:
-
Create operation:
Task<bool> AddItemAsync(T item); //Create operation
-
Read operation:
Task<IEnumerable<T>> GetItemsAsync(bool forceRefresh = false); //Read operation
-
To bind the items to a collection of items:
public ObservableCollection<T> Items { get; } //local list of items
-
Update operation:
Task<bool> UpdateItemAsync(T item); //Update operation
-
Delete operation:
Task<bool> DeleteItemAsync(T item); //Delete operation
-
β¨ Test your understanding What is the interest of having the Items
property?
DatabaseService<T>
Similarly to the AuthService
, we will create a class that contains all the functionality provided by the Firebase database client. This class should not be static and is created in a generic form so that we can create databases for both Meal
and Workout
and possibly other objects.
-
In the
Services
folder, createDatabaseService<T>
class that implementsIDataStore<T>
interface. -
The class is created in a generic form to allow us to operate both on
Workout
andMeal
objects. -
In the
Services
folder, createDatabaseService<T>
class that implementsIDataStore<T>
interface. -
The class is created in a generic form to allow us to operate both on
Workout
andMeal
objects. -
Class header
public class DatabaseService<T> : IDataStore<T> where T : class, IHasUKey
- This will ensure that any used
T
class has implemented theIHasUKey
interface and hence has theKey
property.
- This will ensure that any used
-
Private fields
-
private readonly RealtimeDatabase<T> _realtimeDb;
- Firebase database client which will be use to synchronize the data online.
- Set as
readonly
to avoid reinitializing it at any time in the app life cycle.
-
-
Class Constructor
-
public DatabaseService(Firebase.Auth.User user, string path, string BaseUrl, string customKey = "") { FirebaseOptions options = new FirebaseOptions() { OfflineDatabaseFactory = (t, s) => new OfflineDatabase(t, s), AuthTokenAsyncFactory = async () => await user.GetIdTokenAsync() }; var client = new FirebaseClient(BaseUrl, options); _realtimeDb = client.Child(path) .AsRealtimeDatabase<T>(customKey, "", StreamingOptions.LatestOnly, InitialPullStrategy.MissingOnly, true); }
-
β
β
β - Requires an authentication token to access the database, which can be acquired from AuthService
after the user logs in.
β - Path: a location where to store the data object on the cloud. The easiest implementation is to pass the class name.
β - Example: a Workout
object will be saved under the path of the same name using nameof(Workout)
β - Will be discussed later in the Repo section.
β - BaseUrl
acquired from the ResourceStrings
β - customKey
a custom string which will get appended to the file name. (not needed in this app)
β - Note some of the offline database _realtimeDb
initialization options. These options are enums and can be changed based on the app needs.
β - StreamingOptions.LatestOnly
β - InitialPullStrategy.MissingOnly
Interface members IDataStore<T>
Note: add try-catch
blocks for all IO operations.
-
AddItemAsync
- Uses
_realtimeDb.Post(item)
method to add data to database. - This method returns a key which must be used to assign a key to the added item.
- Code is provided below because of its unconventional approach.
- When adding a new item to the database, the assigned unique identifier will be returned.
- We want to save that key in the same object to use it later for updating and deleting.
public async Task<bool> AddItemAsync(T item) { try { item.Key = _realtimeDb.Post(item); //returns the unique key _realtimeDb.Put(key, item); //Update the entry in the database to maintain the key Items.Add(item); //place new item in the observable collection for UI display } catch (Exception) { return await Task.FromResult(false); } return await Task.FromResult(true); }
- Uses
-
β¨ Test your understanding Why are the CRUD methods asynchronous?
-
UpdateItemAsync
-
Uses the
Put
method in the_realtimeDb
to update.Hint: to update, you need the key; get the item key using
IHasUKey
interface with theas
keyword. -
Do not forget
try-catch
blocks.
-
-
DeleteItemAsync
- Uses the
Delete
method in the_realtimeDb
to delete. - Remove from the observable collection as well.
- Do not forget
try-catch
blocks.
- Uses the
-
GetItemsAsync
- The following implementation is taken from the sample code.
- If the offline database is empty, get data from the cloud.
- Empty because this is the first run of the app.
- Empty because the app is installed on a new device.
public async Task<IEnumerable<T>> GetItemsAsync(bool forceRefresh = false) { if (_realtimeDb.Database?.Count == 0) { try { await _realtimeDb.PullAsync(); } catch (Exception) { return null; } } IEnumerable<T> result = _realtimeDb.Once().Select(x => x.Object); return await Task.FromResult(result); }
-
Items
- This is a tricky task. You need to call a asynchronous method in the getter of the property to fill the observable collection, but the property cannot be marked as
async
. (C# restriction) - A work around is to create a
Task
to make the asynchronous method call and use the synchronousWait()
method. - But since
Wait()
will simply return a void, we must use a helper methodLoadItems
which will assign the_items
. And in the property getter, we call the helper method usingTask.Run
:
private ObservableCollection<T> _items; public ObservableCollection<T> Items { get { if (_items == null) Task.Run(() => LoadItems()).Wait(); return _items; } } private async Task LoadItems() { _items = new ObservableCollection<T>(await GetItemsAsync()); }
- This is a tricky task. You need to call a asynchronous method in the getter of the property to fill the observable collection, but the property cannot be marked as
β¨ Test your understanding: Are Wait()
and await
the same thing?
Data Repo
Currently the data repo contains two simple ObservableCollection
s of Meal
and Workout
items. In this step, you will utilize the DatabaseService<T>
class to create two database instances: WorkoutDb
and MealDb
-
The
DateRepo
class will provide properties to utilize theDatabaseService
class.public class FitnessRepo { private DatabaseService<Meal> mealsDb; public DatabaseService<Meal> MealsDb { get { return mealsDb ??= new DatabaseService<Meal>(...);// complete this } } }
- Notes
- The generic class
DatabaseService
is created to save workout objects, hence<Meal>
class type passed to the declaration. ??=
in the getter will check if the backing field is null to initialize the database object. (This same logic is used in theApp.xaml.cs
class to instantiate the repo).- Authenticated user is passed to get the authentication token (use the
AuthService.UserCreds
class) - The path to save is using
nameof(Meal)
(as discussed earlier)
- The generic class
- Notes
-
Modify the
event
handlerβs in theMealsPage
andMealsForm
to reflect this change -
Run the app.
-
At this point you should be able to add
Meal
and they should appear within the firebase console browser:- Click: Firebase > Projects> MauiFitness > More Products > Realtime Database
β¨ Test question: The data repo currently depends on the AuthService
class. What would be a better design to increase the testability of this class? What concept have we seen in the past that could help with this?
- Repeat the process for the
Workout
class. - Test the Add, Delete and update operations to make sure your Realtime database is working properly.
β¨ Test your understanding: What happens if you modify an item from the firebase console page?