Easily Load Appointments in .NET MAUI Scheduler with SQLite and Perform CRUD
Tags: dotnetmaui, sqlite, mobile, crud
Published: 2024-02-08 03:19:49

---
title: Easily Load Appointments in .NET MAUI Scheduler with SQLite and Perform CRUD
published: true
date: 2024-02-01 12:18:18 UTC
tags: dotnetmaui, sqlite, mobile, crud
canonical_url: https://www.syncfusion.com/blogs/post/load-maui-scheduler-sqlite-crud.aspx
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m4rgaw578p1z5xbryx40.jpg
---
In this blog, we’ll see how to load appointments in the .NET MAUI Scheduler with the SQLite database, perform CRUD actions (create, read, update, and delete) on the SQLite database, and dynamically update the changes in the .NET MAUI Scheduler.

The Syncfusion [.NET MAUI Scheduler](https://www.syncfusion.com/maui-controls/maui-scheduler "Syncfusion .NET MAUI Scheduler") is a powerful tool that provides a wide range of scheduling functionalities, allowing for the efficient management of appointments.

[SQLite](https://en.wikipedia.org/wiki/SQLite "SQLite site") is a lightweight, open-source, and self-contained relational database management system (RDBMS). It is popular for embedded systems, mobile apps, and desktop software due to its ease of use and efficiency. It seamlessly integrates with .NET MAUI apps to load and save data objects in shared code.

## Agenda:

- [Create SQLite connection, define model, and populate appointments.](#SQLiteconnection) 
- [Build a schedule appointment editor.](#ScheduleAppointment)
- [Perform CRUD operations on SQLite database and update .NET MAUI Scheduler.](#PerformCRUD)

Let’s get started!

**Note:** Before proceeding, see the [getting started with .NET MAUI Scheduler documentation](https://help.syncfusion.com/maui/scheduler/getting-started).

## <a name="SQLiteconnection">Create SQLite connection, define model, and populate appointments</a>

Let’s explore the simple steps to connect SQLite with the Syncfusion .NET MAUI Scheduler.

### Step 1: Install the required package

First, we are going to install the **sqlite-net-pcl** package to connect the SQLite database.

Refer to the following image.

![Install sqlite-net-pcl NuGet package](https://www.syncfusion.com/blogs/wp-content/uploads/2024/02/sqlite-net-pcl-NuGet-Package.png)

### Step 2: Create the SQLite connection

Next, define the SQLite connection using the **SQLiteConnection** API and set up the database path property in the **SchedulerDatabase.cs** file.

```csharp
readonly SQLiteConnection _database;

 public SchedulerDatabase(string dbPath)
 {
     _database = new SQLiteConnection(dbPath);
 }
```

Now, create an instance for the **SQLite connection** with the database **Path** property and initialize it in the **App.Xaml.cs** file to use the database.

Refer to the following code example.

```csharp
public static SchedulerDatabase Database
{
    get
    {
        if (database == null)
        {
            database = new SchedulerData-base(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MAUISchedulerDatabase.db3"));
        }
        return database;
    }
}

```

### Step 3: Define the database table model

Then, create a model class named Appointment to hold the property values from the database table in the **Appointment.cs** file.

```csharp
public class Appointment
{
      [PrimaryKey, AutoIncrement, Unique]
      public int ID { get; set; }
      public DateTime From { get; set; }
      public DateTime To { get; set; }
      public bool AllDay { get; set; }
      public string EventName { get; set; }
      public string Notes { get; set; }
}
```

Subsequently, create a table named **Appointment** in that SQLite database.

**SchedulerDatabase.cs**

```csharp
_database.CreateTable<Appointment>();
```

### Step 4: Populate SQLite database appointments

In the **SchedulerViewModel** class, populate the data from the SQLite database.

```csharp
public ObservableCollection<SchedulerAppointment> Appointments { get; set; }

var dataBaseAppointments = App.Database.GetSchedulerAppointment();
if (dataBaseAppointments != null && dataBaseAppointments.Count > 0)
{
    foreach (Appointment appointment in dataBaseAppointments)
    {
        Appointments.Add(new SchedulerAppointment()
        {
            StartTime = appointment.From,
            EndTime = appointment.To,
            Subject = appointment.EventName,
            IsAllDay = appointment.AllDay,
            Id = appointment.ID
        });
    }
}
```

Then, retrieve the appointments from the SQLite database in the SchedulerDatabase.cs file.

```csharp
public List<Appointment> GetSchedulerAppointment()
{
    return _database.Table<Appointment>().ToList();
}
```

### Step 5: Bind appointments to the Scheduler

Initialize the .NET MAUI Scheduler control and bind its **AppointmentsSource** property to the **Appointments** property of the **SchedulerViewModel** class.

**MainPage.xaml**

```xml
xmlns:scheduler="clr-namespace:Syncfusion.Maui.Scheduler;assembly=Syncfusion.Maui.Scheduler"

<ContentPage.BindingContext>
    <local:SchedulerViewModel/>
</ContentPage.BindingContext>

<scheduler:SfScheduler x:Name="Scheduler"
                       View="Week"
                       Tapped="Scheduler_Tapped"
                       AppointmentsSource="{Binding Appointments}"
                       ShowWeekNumber="True"
                       AllowedViews="Day,Week,WorkWeek,Agenda,Month,TimelineDay,TimelineWeek,TimelineWorkWeek,TimelineMonth">
</scheduler:SfScheduler>
```

After executing the previous code examples, we’ll get output like in the following image.

<figure>

  <img src="https://www.syncfusion.com/blogs/wp-content/uploads/2024/02/Populating-appointments-from-SQLite-database-to-.NET-MAUI-Scheduler.jpg" alt="Populating appointments from SQLite database to .NET MAUI Scheduler" style="width:100%">

  <figcaption>Populating appointments from SQLite database to .NET MAUI Scheduler</figcaption>

</figure>
## <a name="ScheduleAppointment">Build a schedule appointment editor</a>

Create an appointment editor that enables the addition, saving, and deletion of appointments in the .NET MAUI Scheduler.

Refer to the following code example to create a model class to store the basic details of the appointment editor in the AppointmentEditorModel.cs file.

```csharp
public class AppointmentEditorModel : INotifyPropertyChanged
{
        private string subject , notes;
        private TimeSpan startTime , endTime;
        private bool isAllDay , isEditorEnabled= true;
        private DateTime startDate, endDate;

        public string Subject
        {
            get { return subject; }
            set
            {
                subject = value;
                RaisePropertyChanged(nameof(this.Subject));
            }
        }

        public string Notes
        {
            get { return notes; }
            set
            {
                notes = value;
                RaisePropertyChanged(nameof(this.Notes));
            }
        }

        public TimeSpan StartTime
        {
            get { return startTime; }
            set
            {
                startTime = value;
                RaisePropertyChanged(nameof(this.StartTime));
            }
        }

        public TimeSpan EndTime
        {
            get { return endTime; }
            set 
            {
                endTime = value;
                RaisePropertyChanged(nameof(this.EndTime));
            }
        }

        public bool IsAllDay
        {
            get { return isAllDay; }
            set
            {
                isAllDay = value;
                RaisePropertyChanged(nameof(this.IsAllDay));
            }
        }

        public DateTime StartDate
        {
            get { return startDate; }
            set
            {
                startDate = value; 
                RaisePropertyChanged(nameof(this.StartDate));
            }
        }

        public DateTime EndDate
        {
            get { return endDate; }
            set 
            {
                endDate = value;
                RaisePropertyChanged(nameof(this.EndDate));
            }
        }

        public bool IsEditorEnabled
        {
            get { return isEditorEnabled; }
            set 
            {
                isEditorEnabled = value;
                RaisePropertyChanged(nameof(this.IsEditorEnabled));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void RaisePropertyChanged(string propertyname)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyname));
            }
        }
}
```

Now, let’s create the appointment editor using the.NET MAUI [Popup](https://help.syncfusion.com/maui/popup/getting-started "Getting Started with .NET MAUI Popup") and [Text Input Layout](https://help.syncfusion.com/maui/textinputlayout/getting-started "Getting Started with .NET MAUI TextInputLayout") controls. Ensure that the **AppointmentEditorModel** property is correctly bound in each appointment editor, allowing seamless two-way value updates between the appointment and the editor.

**MainPage.xaml**

```xml
xmlns:inputLayout="clr-namespace:Syncfusion.Maui.Core;assembly=Syncfusion.Maui.Core"
xmlns:popup="clr-namespace:Syncfusion.Maui.Popup;assembly=Syncfusion.Maui.Popup"

<ContentPage.BindingContext>
    <local:SchedulerViewModel/>
</ContentPage.BindingContext>

<popup:SfPopup x:Name="sfPopup" IsOpen="{Binding IsOpen}" BindingContext="{Binding}" WidthRequest="550" HeightRequest="500" AutoSizeMode="Both" ShowHeader="False" ShowFooter="False">
    <popup:SfPopup.ContentTemplate>
        <DataTemplate>
            <Grid RowDefinitions="*,auto" Margin="20">
                <ScrollView>
                    <StackLayout>
                        <inputLayout:SfTextInputLayout x:Name="eventName_layout" Hint="Event name" ContainerBackground="Transparent" OutlineCornerRadius="8" ContainerType="Outlined">
                            <Entry x:Name="eventNameText" Text="{Binding AppointmentEditorModel.Subject}" />
                        </inputLayout:SfTextInputLayout>

                        <Grid ColumnDefinitions="0.5*,0.5*">
                            <inputLayout:SfTextInputLayout Hint="Start date" OutlineCornerRadius="8" ContainerBackground="Transparent" ContainerType="Outlined">
                                <DatePicker x:Name="startDate_picker" Date="{Binding AppointmentEditorModel.StartDate}" />
                            </inputLayout:SfTextInputLayout>

                            <inputLayout:SfTextInputLayout Grid.Column="1" Margin="10,0,0,0" Hint="Start time" OutlineCornerRadius="8" ContainerBackground="Transparent" ContainerType="Outlined">
                                <TimePicker x:Name="startTime_picker" IsEnabled="{Binding AppointmentEditorModel.IsEditorEnabled}" VerticalOptions="Start" Time="{Binding AppointmentEditorModel.StartTime, Mode=TwoWay}" />
                            </inputLayout:SfTextInputLayout>
                        </Grid>

                        <Grid ColumnDefinitions="0.5*,0.5*">
                            <inputLayout:SfTextInputLayout Hint="End date" OutlineCornerRadius="8" ContainerBackground="Transparent" ContainerType="Outlined">
                                <DatePicker x:Name="endDate_picker" Date="{Binding AppointmentEditorModel.EndDate}" />
                            </inputLayout:SfTextInputLayout>

                            <inputLayout:SfTextInputLayout Hint="End time" Margin="10,0,0,0" Grid.Column="1" OutlineCornerRadius="8" ContainerBackground="Transparent" ContainerType="Outlined">
                                <TimePicker x:Name="endTime_picker" IsEnabled="{Binding AppointmentEditorModel.IsEditorEnabled}" HorizontalOptions="Start" Time="{Binding AppointmentEditorModel.EndTime}" />
                            </inputLayout:SfTextInputLayout>
                        </Grid>

                        <Grid Margin="2,-12,0,0" x:Name="allDay" VerticalOptions="Start" ColumnDefinitions="50,*">
                            <Label Grid.Column="0" VerticalTextAlignment="Center" Text="AllDay"/>
                            <Switch Toggled="SwitchAllDay_Toggled" IsToggled="{Binding AppointmentEditorModel.IsAllDay}" Margin="10,0,0,0" x:Name="switchAllDay" Grid.Column="1" HorizontalOptions="Start" VerticalOptions="Center" />
                        </Grid>

                        <inputLayout:SfTextInputLayout x:Name="organizer_layout" Hint="Notes" OutlineCornerRadius="8" ContainerBackground="Transparent" ContainerType="Outlined">
                            <Editor x:Name="organizerText" Text="{Binding AppointmentEditorModel.Notes}" />
                        </inputLayout:SfTextInputLayout>
                    </StackLayout>
                </ScrollView>

                <Grid HeightRequest="40" Grid.Row="1" ColumnDefinitions="*,auto">
                    <Button x:Name="DeleteButton" Command="{Binding DeleteAppointment}" WidthRequest="80" Text="Delete" HorizontalOptions="Start" />

                    <HorizontalStackLayout HorizontalOptions="End" Grid.Column="1">
                        <Button x:Name="cancelButton" WidthRequest="80" Text="Cancel" Command="{Binding CancelEditAppointment}" />
                        <Button Margin="10,0,0,0" x:Name="saveButton" Command="{Binding AddAppointment}" Text="Save" WidthRequest="80" TextColor="White" />
                    </HorizontalStackLayout>
                </Grid>
            </Grid>
        </DataTemplate>
    </popup:SfPopup.ContentTemplate>
</popup:SfPopup>
```

Refer to the following code example to initialize the **AppointmentEditorModel** in the **SchedulerViewModel.cs** file.

```csharp
public AppointmentEditorModel AppointmentEditorModel { get; set; }

this.AppointmentEditorModel = new AppointmentEditorModel();

private bool isOpen;
public bool IsOpen
{
    get { return isOpen; }
    set
    {
        isOpen = value;
        OnPropertyChanged(nameof(this.IsOpen));
    }
}
```

The appointment editor pop-up will be displayed upon tapping the scheduler, providing the options to add, edit, or delete the appointments. Refer to the following code examples.

**MainPage.xaml.cs**

```csharp
private void Scheduler_Tapped(object sender, SchedulerTappedEventArgs e)
{
    if (this.BindingContext is SchedulerViewModel schedulerViewModel)
    {
        SchedulerAppointment appointment;
        DateTime selectedDate;

        if (e.Appointments != null && e.Appointments.Count > 0)
        {
            appointment = (SchedulerAppointment)e.Appointments[0];
            selectedDate = appointment.StartTime;
        }
        else
        {
            appointment = null;
            selectedDate = (DateTime)e.Date;
        }
        schedulerViewModel.UpdateEditor(appointment, selectedDate);
        sfPopup.Show();
    }
}
```

**SchedulerViewModel.cs**

```csharp
private SchedulerAppointment appointment;
private DateTime selectedDate;

internal void UpdateEditor(SchedulerAppointment appointment, DateTime selectedDate)
{
    this.appointment = appointment;
    this.selectedDate = selectedDate;

    if (this.appointment != null)
    {
        AppointmentEditorModel.Subject = this.appointment.Subject;
        AppointmentEditorModel.Notes = this.appointment.Notes;
        AppointmentEditorModel.StartDate = this.appointment.StartTime;
        AppointmentEditorModel.EndDate = this.appointment.EndTime;
        AppointmentEditorModel.IsEditorEnabled = true;

        if (!this.appointment.IsAllDay)
        {
            AppointmentEditorModel.StartTime = new TimeSpan(this.appointment.StartTime.Hour, this.appointment.StartTime.Minute, this.appointment.StartTime.Second);
            AppointmentEditorModel.EndTime = new TimeSpan(this.appointment.EndTime.Hour, this.appointment.EndTime.Minute, this.appointment.EndTime.Second);
            AppointmentEditorModel.IsAllDay = false;
            AppointmentEditorModel.IsEditorEnabled = true;
        }
        else
        {
            AppointmentEditorModel.StartTime = new TimeSpan(12, 0, 0);
            AppointmentEditorModel.EndTime = new TimeSpan(12, 0, 0);
            AppointmentEditorModel.IsEditorEnabled = false;
            AppointmentEditorModel.IsAllDay = true;
        }
    }
    else
    {
        AppointmentEditorModel.Subject = "";
        AppointmentEditorModel.Notes = "";
        AppointmentEditorModel.IsAllDay = false;
        AppointmentEditorModel.StartDate = this.selectedDate;
        AppointmentEditorModel.StartTime = new TimeSpan(this.selectedDate.Hour, this.selectedDate.Minute, this.selectedDate.Second);
        AppointmentEditorModel.EndDate = this.selectedDate;
        AppointmentEditorModel.EndTime = new TimeSpan(this.selectedDate.Hour + 1, this.selectedDate.Minute, this.selectedDate.Second);
    }
}
```

After executing the previous code examples, we’ll get an output like in the following image.

<figure>

  <img src="https://www.syncfusion.com/blogs/wp-content/uploads/2024/02/Schedule-appointment-editor.jpg" alt="Schedule appointment editor" style="width:100%">

  <figcaption>Schedule appointment editor</figcaption>

</figure>

## <a name="PerformCRUD">Perform CRUD operations on SQLite database and update the .NET MAUI Scheduler</a>

Let’s see how to perform CRUD actions on the SQLite database while synchronizing the changes with the .NET MAUI Scheduler control.

When you tap on  the Scheduler, you can utilize the appointment editor pop-up to add, edit, and delete appointment details. On clicking the **Save** or **Delete** buttons in the appointment editor, both the Scheduler and SQLite database will be updated.

### Saving an appointment

You can add appointments by providing the required details and clicking the **Save** button in the appointment editor. The following code examples illustrates the command associated with the **Save** button.

**SchedulerViewModel.cs**

```csharp
public Command AddAppointment { get; set; }

AddAppointment = new Command(AddAppointmentDetails);

private void AddAppointmentDetails()
{
    var endDate = AppointmentEditorModel.EndDate;
    var startDate = AppointmentEditorModel.StartDate;
    var endTime = AppointmentEditorModel.EndTime;
    var startTime = AppointmentEditorModel.StartTime;

    if (endDate < startDate)
    {
        Application.Current.MainPage.DisplayAlert("", "End date should be greater than start date", "OK");
    }
    else if (endDate == startDate)
    {
        if (endTime <= startTime)
        {
            Application.Current.MainPage.DisplayAlert("", "End time should be greater than start time", "OK");
        }
        else
        {
            AppointmentDetails();
        }
    }
    else
    {
        AppointmentDetails();
    }
}

private void AppointmentDetails()
{
    if (appointment == null)
    {
        appointment = new SchedulerAppointment();
        appointment.Subject = AppointmentEditorModel.Subject;
        appointment.StartTime = AppointmentEditorModel.StartDate.Date.Add(AppointmentEditorModel.StartTime);
        appointment.EndTime = AppointmentEditorModel.EndDate.Date.Add(AppointmentEditorModel.EndTime);
        appointment.IsAllDay = AppointmentEditorModel.IsAllDay;
        appointment.Notes = AppointmentEditorModel.Notes;

        if (this.Appointments == null)
        {
            this.Appointments = new ObservableCollection<SchedulerAppointment>();
        }

        appointment.Id = Appointments.Count;
        //// Add the appointments in the Scheduler.
        Appointments.Add(appointment);
    }
    else
    {
        appointment.Subject = AppointmentEditorModel.Subject;
        appointment.StartTime = AppointmentEditorModel.StartDate.Date.Add(AppointmentEditorModel.StartTime);
        appointment.EndTime = AppointmentEditorModel.EndDate.Date.Add(AppointmentEditorModel.EndTime);
        appointment.IsAllDay = AppointmentEditorModel.IsAllDay;
        appointment.Notes = AppointmentEditorModel.Notes;
    }

    SaveSchedulerAppointmentAsync();

    this.IsOpen = false;
}
```

The appointment details will be updated based on the user input, and the **SaveSchedulerAppointmentAsync()** method handles the addition or editing of appointments in both the Scheduler and the SQLite database.

Refer to the following code examples for saving an appointment in the SQLite database.

**SchedulerViewModel.cs**

```csharp
private void SaveSchedulerAppointmentAsync()
{
    //// - add or edit the appointment in the database collection.
    var editAppointment = new Appointment()
    {
        From = appointment.StartTime,
        To = appointment.EndTime,
        AllDay = appointment.IsAllDay,
        Notes = appointment.Notes,
        EventName = appointment.Subject,
        ID = (int)appointment.Id
    };
    App.Database.SaveSchedulerAppointmentAsync(editAppointment);
}
```

**SchedulerDatabase.cs**

```csharp
//Insert an appointment in the database.
 public int SaveSchedulerAppointmentAsync(Appointment appointment)
 {
     if (appointment == null)
     {
         throw new Exception("Null");
     }

     return _database.InsertOrReplace(appointment);
 }
```

### Deleting an appointment

By clicking the **Delete** button, we can delete an appointment in the .NET MAUI Scheduler and in the SQLite database. The following code examples demonstrate the command associated with the **Delete** button.

  **SchedulerViewModel.cs**

```csharp
public Command DeleteAppointment { get; set; }

DeleteAppointment = new Command(DeleteSchedulerAppointment);

private void DeleteSchedulerAppointment()
{
    if (appointment == null)
    {
        this.IsOpen = false;
        return;
    }

    //// Remove the appointments in the Scheduler.
    Appointments.Remove(this.appointment);
    //// Delete appointment in the database.
    var deleteAppointment = new Appointment()
    {
        From = appointment.StartTime,
        To = appointment.EndTime,
        AllDay = appointment.IsAllDay,
        Notes = appointment.Notes,
        EventName = appointment.Subject,
        ID = (int)appointment.Id
    };

    App.Database.DeleteSchedulerAppointmentAsync(deleteAppointment);
    this.IsOpen = false;
}
```

**SchedulerDatabase.cs**

```csharp
//Delete an appointment in the database.
public int DeleteSchedulerAppointmentAsync(Appointment appointment)
{
      return _database.Delete(appointment);
}
```

After executing the previous code examples, we’ll get output like in the following image.

<figure>

  <img src="https://www.syncfusion.com/blogs/wp-content/uploads/2024/02/Performing-CRUD-operations-in-SQLite-database-using-.NET-MAUI-Scheduler-test.gif" alt="Performing CRUD operations in SQLite database using .NET MAUI Scheduler" style="width:100%">

  <figcaption>Performing CRUD operations in SQLite database using .NET MAUI Scheduler</figcaption>

</figure>

When rerunning the app, the .NET MAUI Scheduler will fetch the updated appointments from the SQLite database, and the output will be as follows.

<figure>

  <img src="https://www.syncfusion.com/blogs/wp-content/uploads/2024/02/Updating-the-.NET-MAUI-Scheduler-appointments-loaded-from-SQLite-database.jpg" alt="Updating the .NET MAUI Scheduler appointments loaded from SQLite database" style="width:100%">

  <figcaption>Updating the .NET MAUI Scheduler appointments loaded from SQLite database</figcaption>

</figure>

## GitHub reference

For more details, refer to the complete project on the [GitHub repository](https://github.com/SyncfusionExamples/Loading-Appointments-in-.NET-MAUI-Scheduler-with-SQLite-and-Performing-CRUD-operations "Load appointments in .NET MAUI Scheduler with SQLite and Perform CRUD GitHub demo").

## Conclusion

Thanks for reading! In this blog, we learned how to load appointments in the [.NET MAUI Scheduler](https://www.syncfusion.com/maui-controls/maui-scheduler "Syncfusion .NET MAUI Scheduler") with SQLite and perform effortless CRUD operations. Try out the steps discussed in this blog post and leave your feedback in the comments section below!

The newest version of Essential Studio is available for existing customers on the [License and Downloads](https://www.syncfusion.com/account/login "Essential Studio License and Downloads page") page. If you’re not a Syncfusion customer, sign up for our [30-day free trial](https://www.syncfusion.com/downloads "Get the free 30-day evaluation of Essential Studio products") to explore our features.

For questions, you can contact us through our [support forum](https://www.syncfusion.com/forums "Syncfusion Support Forum"), [support portal](https://support.syncfusion.com/ "Syncfusion Support Portal"), or [feedback portal](https://www.syncfusion.com/feedback/maui "Syncfusion Feedback Portal"). We are always happy to assist you!

## Related blogs

- [Chart of the Week: Create a .NET MAUI Stacked Column Chart for the Top-Earning Female Athletes](https://www.syncfusion.com/blogs/post/maui-stackedcolumn-chart-athlete.aspx "Blog: Chart of the Week: Create a .NET MAUI Stacked Column Chart for the Top-Earning Female Athletes")
- [Syncfusion .NET MAUI 2024 Road Map](https://www.syncfusion.com/blogs/post/dotnet-maui-2024-road-map.aspx "Blog: Syncfusion .NET MAUI 2024 Road Map")
- [Chart of the Week: Creating a .NET MAUI Bar Chart to Visualize Type 1 Diabetes Prevalence](https://www.syncfusion.com/blogs/post/dotnet-maui-bar-chart-diabetes.aspx "Blog: Chart of the Week: Creating a .NET MAUI Bar Chart to Visualize Type 1 Diabetes Prevalence")
- [Introducing the New .NET MAUI PullToRefresh Control](https://www.syncfusion.com/blogs/post/dotnet-maui-pull-to-refresh.aspx "Blog: Introducing the New .NET MAUI PullToRefresh Control")