Search code examples
c#sqlitexamarinmauisqlite-net

.NET MAUI - SQLite-net-PCL CreateTableAsync breaks


I previously had this issue where my whole application froze after calling db.CreateTableAsync in a Service.cs file after following this tutorial. After fixing this issue with this solution, the application stopped freezing but CreateTableAsync seems to do nothing and just breaks out of the Init Task and the AddPin Task. In the same runtime, if I were to try to run the AddPin task multiple times, the first time CreateTableAsync would break and consecutive times after the initial, the rest of AddPin will be called (because db != null), and it's worth noting that no matter how many times I run AddPin the id does not increment.

Restroom.cs

using SQLite;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Dook.Model
{
    public class Restroom
    {
        [SQLite.PrimaryKey, SQLite.AutoIncrement]
        [Column("Id")]
        public int Id { get; set; }
        [Column("Name")]
        public string Name { get; set; }
        [Column("Address")]
        public string Address { get; set; }
        [Column("Username")]
        public string Username { get; set; }
        [Column("Location")]
        public Location PinLocation { get; set; }
    }
}

RestroomService.cs

using Dook.Model;
using SQLite;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Dook.Services
{
    public static class RestroomService
    {
        static SQLiteAsyncConnection db;

        static async Task InitAsync()
        {
            if(db != null) 
                return;

            var databasePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "MyData.db");

            db = new SQLiteAsyncConnection(databasePath);

            await db.CreateTableAsync<Restroom>();

            Debug.WriteLine("Bruh");
        }

        public static async Task AddPinAsync(string name, string address, string username, Location location)
        {
            await InitAsync();
            var restroom = new Restroom
            {
                Name = name,
                Address = address,
                Username = username,
                PinLocation = location
            };

            var id = await db.InsertAsync(restroom);
        }

        public static async Task RemovePinAsync(int id)
        {
            await InitAsync();

            await db.DeleteAsync<Restroom>(id);
        }

        public static async Task<IEnumerable<Restroom>> GetPinAsync()
        {
            await InitAsync();

            var restroom = await db.Table<Restroom>().ToListAsync();
            return restroom;
        }
    }
}

MainPage.xaml.cs

namespace Dook;
using Microsoft.Maui.Controls.Maps;
using Microsoft.Maui.Maps;
using System.Diagnostics;
using Map = Microsoft.Maui.Controls.Maps.Map;
using Dook.ViewModel;
using Dook.Model;

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
        MoveMapLocation();
    }

    private void GoToLocation_Button(object sender, EventArgs e)
    {
        MoveMapLocation();
    }

    private void OnMapClicked(object sender, MapClickedEventArgs e)
    {
        var vm = (MainViewModel)this.BindingContext;
        if (vm.AddCommand.CanExecute(e.Location))
            vm.AddCommand.ExecuteAsync(e.Location);
    }

    private void RefreshButton_Clicked(object sender, EventArgs e)
    {
        var vm = (MainViewModel)this.BindingContext;
        vm.RefreshCommand.ExecuteAsync();
    }

    private void MoveMapLocation()
    {
        //Function to avoid boilerplate code
        MapSpan mapSpan = new MapSpan(MainViewModel.GetLocation(), 0.01, 0.01);
        mainmap.MoveToRegion(mapSpan);
    }
}

MainPage.xaml

<?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"
             xmlns:viewmodel="clr-namespace:Dook.ViewModel"
             x:DataType="viewmodel:MainViewModel"
             xmlns:maps="clr-namespace:Microsoft.Maui.Controls.Maps;assembly=Microsoft.Maui.Controls.Maps"
             xmlns:sensors="clr-namespace:Microsoft.Maui.Devices.Sensors;assembly=Microsoft.Maui.Essentials"
             xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
             xmlns:model="clr-namespace:Dook.Model"
             x:Class="Dook.MainPage"
             Title="{Binding Title}"
             Shell.NavBarIsVisible="False">

    <ContentPage.BindingContext>
        <viewmodel:MainViewModel />
    </ContentPage.BindingContext>

    <Grid
        Margin="0"
        RowDefinitions="*, Auto"
        ColumnDefinitions="*">

        <maps:Map
            x:Name="mainmap"
            MapType="Street"
            IsShowingUser="True"
            x:FieldModifier="public"
            HorizontalOptions="FillAndExpand"
            VerticalOptions="FillAndExpand"
            IsVisible="True"
            IsEnabled="True"
            Grid.Row="0"
            MapClicked="OnMapClicked"
            ItemsSource="{Binding Restroom}">

            <maps:Map.ItemTemplate>
                <DataTemplate x:DataType="model:Restroom">
                    <maps:Pin
                        Label="{Binding Name}"
                        Address="{Binding Address}"
                        Type="Generic"
                        Location="{Binding PinLocation}"/>
                </DataTemplate>
            </maps:Map.ItemTemplate>
        </maps:Map>

        <Button
            Text="Refresh"
            IsEnabled="{Binding IsNotBusy}"
            Clicked="RefreshButton_Clicked"
            Margin="5"
            VerticalOptions="Center"
            HorizontalOptions="Start"
            Grid.Row="1"/>

        <ImageButton
            Source="feather_navigation_icon.png"
            Aspect="AspectFill"
            Clicked="GoToLocation_Button" 
            IsEnabled="{Binding IsNotBusy}"
            Margin="20"
            VerticalOptions="End"
            HorizontalOptions="End"
            WidthRequest="13"
            HeightRequest="13"
            BackgroundColor="Transparent"
            CornerRadius="10"
            Grid.Row="0"/>
    </Grid>
    
</ContentPage>

MainViewModel.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Maui.Controls.Maps;
using Microsoft.Maui.Maps;
using Map = Microsoft.Maui.Controls.Maps.Map;
using Dook.Model;
using Dook.Services;
using MvvmHelpers;
using MvvmHelpers.Commands;
using CommunityToolkit.Maui.Core.Extensions;

namespace Dook.ViewModel
{
    public class MainViewModel : BaseViewModel
    {
        public ObservableRangeCollection<Restroom> Restroom { get; set; }
        public AsyncCommand RefreshCommand { get; }
        public AsyncCommand<Location> AddCommand { get; }
        public AsyncCommand<Restroom> RemoveCommand { get; }

        public MainViewModel()
        {
            Title = "Map Controller";

            Restroom = new ObservableRangeCollection<Restroom>();

            AddCommand = new AsyncCommand<Location>(AddAsync);
            RemoveCommand = new AsyncCommand<Restroom>(RemoveAsync);
            RefreshCommand = new AsyncCommand(RefreshAsync);
        }

        async Task AddAsync(Location currentLocation)
        {
            var name = await App.Current.MainPage.DisplayPromptAsync("Location Name", "Name of Location");
           // var address = "Latitude: {pinlocation.Latitude}, Longitude: {pinlocation.Longitude}, Altitude: {location.Altitude}";
            var address = "test";
            var username = await App.Current.MainPage.DisplayPromptAsync("Username", "Username of Toilet Adder");
            var location = currentLocation;
            if(name == null || address == null || username == null) { return; }
            await RestroomService.AddPinAsync(name, address, username, location);
            await RefreshAsync();
        }
        
        async Task RemoveAsync(Restroom restroom)
        {
            await RestroomService.RemovePinAsync(restroom.Id);
            await RefreshAsync();
        }

        async Task RefreshAsync()
        {
            IsBusy = true;
            await Task.Delay(2000);
            Restroom.Clear();
            var restrooms = await RestroomService.GetPinAsync();
            Restroom.AddRange(restrooms);
            IsBusy = false;
        }

        public static Location GetLocation()
        {
            try
            {
                Location location = new();
                location = Geolocation.Default.GetLastKnownLocationAsync().Result;
                if (location != null)
                    return location;
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"Unable to get location: {ex.Message}");
                Application.Current.MainPage.DisplayAlert("Error!", ex.Message, "OK");
            }

            return null;
        }
    }
}

Solution

  • According to Jason's comment, as an answer:

    Storing Lat and Long as separate fields can fix the problem.