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 MauiFitnessapp 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 Databaseand 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
Settingsclass in theMauiFitnessapp- 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
Browsetab > 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
IHasUKeyin theInterfacesfolder:public interface IHasUKey- Used to ensure that the saved model class has a property when saving to the database.
- Has public string property
Keywith a getter and setter.
-
Implement the
IHasUKeyinterface in both model classesWorkoutandMeal.- Add a private string property
key - Use it within the getter of the public property
Key - In addition to the currently implemented
FodyPropertyChanged
- Add a private string property
-
For the
Workoutitem, 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 theInterfacesfolder: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
Servicesfolder, createDatabaseService<T>class that implementsIDataStore<T>interface. -
The class is created in a generic form to allow us to operate both on
WorkoutandMealobjects. -
In the
Servicesfolder, createDatabaseService<T>class that implementsIDataStore<T>interface. -
The class is created in a generic form to allow us to operate both on
WorkoutandMealobjects. -
Class header
public class DatabaseService<T> : IDataStore<T> where T : class, IHasUKey- This will ensure that any used
Tclass has implemented theIHasUKeyinterface and hence has theKeyproperty.
- 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
readonlyto 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
Putmethod in the_realtimeDbto update.Hint: to update, you need the key; get the item key using
IHasUKeyinterface with theaskeyword. -
Do not forget
try-catchblocks.
-
-
DeleteItemAsync- Uses the
Deletemethod in the_realtimeDbto delete. - Remove from the observable collection as well.
- Do not forget
try-catchblocks.
- 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
Taskto make the asynchronous method call and use the synchronousWait()method. - But since
Wait()will simply return a void, we must use a helper methodLoadItemswhich 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 ObservableCollections of Meal and Workout items. In this step, you will utilize the DatabaseService<T> class to create two database instances: WorkoutDb and MealDb
-
The
DateRepoclass will provide properties to utilize theDatabaseServiceclass.public class FitnessRepo { private DatabaseService<Meal> mealsDb; public DatabaseService<Meal> MealsDb { get { return mealsDb ??= new DatabaseService<Meal>(...);// complete this } } }- Notes
- The generic class
DatabaseServiceis 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.csclass to instantiate the repo).- Authenticated user is passed to get the authentication token (use the
AuthService.UserCredsclass) - The path to save is using
nameof(Meal)(as discussed earlier)
- The generic class
- Notes
-
Modify the
eventhandlerβs in theMealsPageandMealsFormto reflect this change -
Run the app.
-
At this point you should be able to add
Mealand 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
Workoutclass. - 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?