Home > Software engineering >  How do I trigger a data refresh on my FlyoutContentTemplate view model when navigating back from my
How do I trigger a data refresh on my FlyoutContentTemplate view model when navigating back from my

Time:02-28

I am building a Xamarin Forms application using Shell in which I am not building your typical flyout. I have created a template each for the header, footer and content. The content contains data which is fetched from a database.

I'm defining these in my app.xaml file.

<Style TargetType="Shell" ApplyToDerivedTypes="True">
    <Setter Property="FlyoutFooterTemplate" Value="{DataTemplate common:FlyoutFooterTemplate}"/>
    <Setter Property="FlyoutHeaderTemplate" Value="{DataTemplate common:FlyoutHeaderTemplate}"/>
    <Setter Property="FlyoutContentTemplate" Value="{DataTemplate common:FlyoutContentTemplate}"/>
</Style>

I have created the FlyoutContentTemplate as a RefreshView.

<?xml version="1.0" encoding="utf-8" ?>
<RefreshView 
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:InFlight.ViewModels.Common"
    xmlns:model="clr-namespace:InFlight.Core.Models;assembly=InFlight.Core"  
    x:Class="InFlight.Views.Common.FlyoutContentTemplate"
    x:DataType="local:FlyoutContentTemplateViewModel"
    Command="{Binding LoadFlightsCommand}"
    IsRefreshing="{Binding IsBusy, Mode=TwoWay}">
    <CollectionView    
        ItemsSource="{Binding Flights}"
        SelectionMode="None">
        <CollectionView.ItemTemplate>
            <DataTemplate>
                <StackLayout Padding="10" x:DataType="model:Flight">
                    <Label Text="{Binding CallSign}"
                            LineBreakMode="NoWrap"
                            Style="{StaticResource LabelMedium}" />
                    <Label Text="{Binding FlightNotes}" 
                            LineBreakMode="NoWrap"
                            Style="{StaticResource LabelSmall}" />
                </StackLayout>
            </DataTemplate>
        </CollectionView.ItemTemplate>
        <CollectionView.Footer>
            <Button Command="{Binding AddFlightCommand}" Text="Add Flight" />
        </CollectionView.Footer>
    </CollectionView>
</RefreshView>

The code behind simply sets the BindingContext.

using InFlight.ViewModels.Common;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;

namespace InFlight.Views.Common
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class FlyoutContentTemplate : RefreshView
    {
        readonly FlyoutContentTemplateViewModel viewModel;

        public FlyoutContentTemplate()
        {
            InitializeComponent();
            this.BindingContext = viewModel = new FlyoutContentTemplateViewModel();
        }
    }
}

The view model is fairly simple and handles the LoadFlightsCommand triggered by the RefreshView and the navigation to the AddEditFlightPage.

using InFlight.Core.Models;
using InFlight.Core.Respositories;
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Threading.Tasks;
using Xamarin.Forms;

namespace InFlight.ViewModels.Common
{
    public class FlyoutContentTemplateViewModel : BaseViewModel
    {
        public ObservableCollection<Flight> Flights { get; set; }
        public Command LoadFlightsCommand { get; }
        public Command AddFlightCommand { get; }
        readonly IFlightRepository flightRepository;

        public FlyoutContentTemplateViewModel()
        {
            flightRepository = DependencyService.Get<IFlightRepository>();
            
            Flights = new ObservableCollection<Flight>();
            LoadFlightsCommand = new Command(ExecuteLoadFlightsCommand);
            AddFlightCommand = new Command(async () => await OnAddFlightCommand());

            ExecuteLoadFlightsCommand();
        }

        private async Task OnAddFlightCommand()
        {
            await Shell.Current.GoToAsync("AddEditFlightPage");
            Shell.Current.FlyoutIsPresented = false;
        }

        private void ExecuteLoadFlightsCommand()
        {
            IsBusy = true;

            try
            {
                Flights.Clear();
                var flts = flightRepository.GetFlights();

                foreach (var flight in flts)
                {
                    Flights.Add(flight);
                }                
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex);
            }
            finally
            {
                IsBusy = false;
            }
        }
    }
}

This all seems to work well, but at the moment I need to pull to refresh in order to trigger the LoadFlightsCommand.

The issue is that I want to trigger the data refresh when navigating back from my add page. I've seen posts where people tap into the OnAppearing event in order to trigger the refresh, but as I am using a template and refresh view, I don't see how I can do that.

Have I maybe taken the wrong approach and using Shell for a purpose it shouldn't be used for?

I'm thinking that the solution probably involves the events of the shell itself?

Any advice would be much appreciated.

CodePudding user response:

First Approach

I don't have the full picture on how you are defining your Shell's routes/navigation, but I believe what you are trying to achieve could be done using Shell event OnNavigated or OnNavigating.

  • First thing, Shell (AppShell.xaml.cs) need to have access to the instance of your FlyoutContentTemplateViewModel in roder to call the method ExecuteLoadFlightsCommand() from there.
  • Turn the accessibility of ExecuteLoadFlightsCommand() to public.

AppShell.xaml.cs

  public FlyoutContentTemplateViewModel flyoutContentTemplateViewModel;

    public AppShell()
    {
        flyoutContentTemplateViewModel = new();
        InitializeComponent();
    }

  protected override void OnNavigated(ShellNavigatedEventArgs args)
    {
        var previousRouteString = args?.Previous?.Location?.OriginalString;
        var currentRouteString = args?.Current?.Location?.OriginalString;
        
        if (previousRouteString != null && previousRouteString.Contains("[DEPENDS ON YOUR ROUTES NAME]") &&
            currentRouteString.Contains("[DEPENDS ON YOUR ROUTES NAME]"))
        {
           flyoutContentTemplate.ExecuteLoadFlightsCommand();
        }

        base.OnNavigated(args);
    }

In your FlyoutContentTemplate(), use the same ViewModel instance from the field that we have added in your AppShell.

public FlyoutContentTemplate()
{
   InitializeComponent();
   BindingContext = viewModel = (Shell.Current as AppShell).flyoutContentTemplateViewModel;
}

Second Approach

If you don't want to store your VM in your AppShell then you might use DependencyService.

  • Extract an interface from your FlyoutContentTemplateViewModel: on visual studio select the class name, right click, in the menu click "Quick Actions and refactoring", after that click "Extract interface", VS will generate an interface called IFlyoutContentTemplateViewModel:
 public interface IFlyoutContentTemplateViewModel
    {
        Command AddFlightCommand { get; }
        ObservableCollection<Flight> Flights { get; set; }
        bool IsBusy { get; }
        Command LoadFlightsCommand { get; }

        Task OnAddFlightCommand()
        void ExecuteLoadFlightsCommand();
    }
  • FlyoutContentTemplate.xaml.cs
public FlyoutContentTemplate()
{
    InitializeComponent();
    BindingContext = viewModel = new FlyoutContentTemplateViewModel();
    DependencyService.RegisterSingleton<IFlyoutContentTemplateViewModel>(viewModel);
}
  • AppShell.xaml.cs
...
if (previousRouteString != null && previousRouteString.Contains("[DEPENDS ON YOUR ROUTES NAME]") &&
    currentRouteString.Contains("[DEPENDS ON YOUR ROUTES NAME]"))
 {
        DependencyService.Resolve<IFlyoutContentTemplateViewModel>()?.ExecuteLoadFlightsCommand();
 }

Third Approach

Calling ExecuteLoadFlightsCommand() from OnDisappearing() of AddEditFlightPage, instead of AppShell.OnNavigated().

AddEditFlightPage.xaml.cs

    protected override void OnDisappearing()
    {
        (DependencyService.Resolve<IFlyoutContentTemplateViewModel>())?.ExecuteLoadFlightsCommand();
        base.OnDisappearing();
    }
  • Related