Okay so we have our separation of concerns, but this app isn’t really an app yet. It doesn’t really do anything. We want to be able to write our own action items and save the state for the next time we launch the app. Well how we do that is with Services. Services allow us to add storage and external APIs to our app. In this blog post we will wire up a database to store the action items and their state.
Database Choice
This was a tough one for me. I have experience with many database systems, but to pick one for a learning application is what’s important here. We could spin up Microsoft SQL Server, or PostgreSQL and use those behind an API. Maybe that’d be an interesting story in the future, but for this article we are going to stick with reliable old SQLite.
Now that we have a database let’s add the NuGet packages that we need to accomplish this. Run the following commands in your project folder:
dotnet add package sqlite-net-pcl
dotnet add package SQLitePCLRaw.provider.dynamic_cdecl
Or you could use the NuGet Package Manager and search for these packages and add them that way. The choice is yours.
Next go to our model ActionItem.cs. Add a using statement for SQLite and then decorate the class with [Table("actionItems")]
, the ID with [PrimaryKey, AutoIncrement]
, and the Title with [MaxLength(250)]
. Your model should look like this when this is complete:
|
|
This will create a table in our SQLite database called actionItems
which has three columns one for ID
which is the primary key and will auto increment, one for Title
which has a max length of 250 characters, and one for IsCompleted
.
What about a service?
Create a folder called Services
and add a class called ActionItemRepository.cs
. That’s right, I’m going to tell you all about the Repository pattern. Basically it just hides all of the database specific “stuff” away from the main code. You can call things like AddActionItem and MarkComplete without your ViewModel to know how to deal with the database. It makes it so that in the future we could replace SQLite with an API and we would only have to rewrite the repository. Simple right?
Now for the repository add this code and I’ll try to explain it afterwards:
|
|
Let’s start by looking at the constructor. It expects a string for the database path to be passed in. This is done using dependency injection. I’ll show you that later. For now we pass it in and save it to a private field.
Next up is the Init
function. This gets called before every database action. It first checks to see if there is already a database connection and if so returns early. If there isn’t a database connection it creates one and then tries to create the ActionItem
table. This will first check if the table exists and create the table if it does not exist.
Next we create two async functions. One for AddNewActionItemAsync
and one for GetAllActionItemsAsync
. There’s no point right now for these to be async and you could make the synchronous. I’m used to writing async code so that’s why I chose it. The first function takes a string for the title
and a Boolean for isComplete
. The ID
will be generated by the database when we insert this new action item. The interesting part of this function is on line 37 where we do the actual insert. It reminds me of dealing with List
. Only instead of calling Add
we are calling InsertAsync
with an ActionItem object. The result is the number of records changed. Hopefully this will always be 1.
There is a StatusMessage
that we use to store this information. In the ViewModel we can interrogate this parameter for a status bar message in the future.
GetAllActionItemsAsync
on line 56 gets a list of all Action Items from the table and returns a List of them to the calling function.
Great, but how do we use it?
Well before we can do that let’s add it to the Dependency Injection services list. This gets a little complicated because we are passing a variable in to the constructor. First, let’s create that variable in the MauiProgram.cs
file. It’s the database path, so do something like this:
var dbPath = Path.Combine(FileSystem.AppDataDirectory, "ActionItems.db3");
This uses a device agnostic path called FileSystem.AppDataDirectory
and combines that with the database name. Which I called ActionItems.db3
. What do I mean by device agnostic? Well the path to the app data directory is different on Windows, iOS, Android, and MacOS. This is one of the really helpful functions provided by .NET MAUI to make writing cross platform applications easy. You don’t have to think about it and it reduces the number of ugly and confusing #ifdef
’s in the code.
Now that we have the path we want to include the ActionItemRepository. Use this line to do that:
builder.Services.AddSingleton(s => ActivatorUtilities.CreateInstance<ActionItemRepository>(s, dbPath));
This is obviously a bit more complicated than we did for MainPage and MainViewModel. Basically what we are doing is creating an ActionItemRepository
factory and pass in an IServiceProvider and that parameter that we need to have passed into the constructor.
Once finished your MauiProgram.cs
should look like this:
|
|
Now what?
Feeling the complexity growing now? There are changes to two more files to go. First we want to update our view model. Then we’ll update the view. First the ViewModel. Open MainViewModel.cs
and first thing we need to do is get our repository into the view model. Create a private variable to hold the repository:
private ActionItemRepository Repo;
Then create a constructor for the view model. It will take in the repository. Save the repository that’s passed in to the private variable created previously.
public MainViewModel(ActionItemRepository air)
{
Repo = air;
}
Next, delete the GetActionItems()
function and replace it with this:
[RelayCommand]
async void GetActionItems()
{
Items.Clear();
var ai = await Repo.GetAllActionItemsAsync();
foreach (var item in ai)
{
Items.Add(item);
}
}
Again if you prefer synchronous calls you don’t need to make this async. The first thing we clear the Items
. Next we call GetAllActionItemsAsync
on the repository and store the action items in a temporary variable. Unfortunately, ObservableObject
doesn’t include an AddRange
method, only an Add
method. So we have to loop over all of the items in the ai
variable and add them to the Items
variable. Not terrible, but it adds a little code smell.
The other thing we want to do in this blog post is add an item. An item consists of a title and an isComplete bool. Add a couple of fields at the top to hold these values and then we’ll create the AddActionItem method.
[ObservableProperty]
private string title;
[ObservableProperty]
private bool isComplete;
By marking these private fields as ObservableProperty
it allows us to data-bind to them. This is more magic that the MVVM Community Toolkit gives us. It saves us from writing a bunch of lines of boilerplate code for INotifyPropertyChanged
and related events. Next, add the AddActionItem
method:
[RelayCommand]
async void AddActionItem()
{
await Repo.AddNewActionItemAsync(Title, IsComplete);
Title = string.Empty;
IsComplete = false;
}
This method is fairly straight forward. We create the new database entry by calling AddNewActionItemAsync
with the Title and IsComplete properties. Then we reset those properties back to their defaults.
After all of this your ViewModel should look like this:
|
|
Are we done yet?
Almost there. We just have to update our View with the new fields for adding an action item to our to-do list. We already have a grid with two rows. Let’s add another row at the top and make it a grid with three columns like the highlighted code below.
|
|
Don’t forget to update the Grid.Row
definitions as well as adding the row to the main grid’s RowDefinitions
. Now, with that all in place we can launch the app. Add an item to the ActionItems (remember your DB is empty after first launch). Finally click the Load Data button to load your new rows. It should look something like this:
Wow! We’ve come a long way to making a functional application. In the next post I’ll tackle launching a new page where we can update the text and completion check box. If you are enjoying this series let me know . Also, if you’d like a companion video of me going through this let me know that as well. I could put one together on my YouTube channel.