Xamarin Forms - Persistent views across multiple pages

The situation:

I was looking to displaying a sync message and throbber then result to the user of an app I'm currently developing. My issue was that if a page was added to or removed from the navigation stack, the view that contained the message would no longer be visible and as such the user would never know the result of the sync. I needed a way to get the view to stay on screen, no matter what page was displaying.

My solution was to remove the view from the previous page (the page the user was just looking at) and add it to the now visible page. I did this by creating a class that would handle my overlays functions such as creating the overlay, removing the view from its parent and adding it to the new parent. Once this was done, I could then simply make sure I'd defined a parent item on each page for the view and then append to it from within the 'OnAppearing()' method.

Coding a solution:

Since I was writing a sync service that needed to display a message and status to the user as it happens, I needed something that would work across many pages, so within the sync service, I created a few helper functions to manage this for me.

The first task was to code a view that the app would create on startup and then display as needed, I came up with this...

Creating the message/throbber bar:

public void CreateSyncThrobber(AbsoluteLayout objMainLayout)
{
    objSyncingLayout = new StackLayout() { Orientation = StackOrientation.Horizontal, BackgroundColor = Core.Colours.Grey, Padding = 10 };

    Label objLabel = new Label() { Text = "Syncing with server... ", TextColor = Color.White, HorizontalOptions = LayoutOptions.FillAndExpand };
    objSyncingLayout.Children.Add(objLabel);

    ActivityIndicator ai = new ActivityIndicator() { HorizontalOptions = LayoutOptions.End };
    ai.IsRunning = true;
    ai.IsEnabled = true;
    ai.WidthRequest = 20;
    ai.HeightRequest = 20;
    objSyncingLayout.Children.Add(ai);

    objMainLayout.Children.Add(objSyncingLayout, new Rectangle(.5, 1, 1, 40), AbsoluteLayoutFlags.PositionProportional | AbsoluteLayoutFlags.WidthProportional);
    objSyncingLayout.IsVisible = false;
            
    objSyncingCompleteLayout = new ExtendedStackLayout() { Orientation = StackOrientation.Horizontal, BackgroundColor = Core.Colours.Grey, Padding = 10 };

    objCompleteLabel = new Label() { TextColor = Color.White, HorizontalOptions = LayoutOptions.FillAndExpand };
    objSyncingCompleteLayout.Children.Add(objCompleteLabel);

    objMainLayout.Children.Add(objSyncingCompleteLayout, new Rectangle(.5, 1, 1, 40), AbsoluteLayoutFlags.PositionProportional | AbsoluteLayoutFlags.WidthProportional);
    objSyncingCompleteLayout.IsVisible = false;
}

The SyncService class contains the above method and a definition for 'objSyncingLayout' which is a StackLayout and once constructed, it's all added to objMainLayout which is a view given to the method by the following code.

I should mention at this point that I've had to change the root view of each page to an AbsoluteLayout so that I can force the overlay to the bottom of the page. I've then given the view the name of mainLayout by applying this property: 'x:Name="mainLayout"'.

So two more helper functions for my SyncService message display are as follows:

Adding the message/throbber to the page:

public void AddSyncThrobber(AbsoluteLayout objMainLayout)
{
    objMainLayout.Children.Add(objSyncingLayout, new Rectangle(.5, 1, 1, 40), AbsoluteLayoutFlags.PositionProportional | AbsoluteLayoutFlags.WidthProportional);
    objMainLayout.Children.Add(objSyncingCompleteLayout, new Rectangle(.5, 1, 1, 40), AbsoluteLayoutFlags.PositionProportional | AbsoluteLayoutFlags.WidthProportional);
}

Notice I'm defining the absolute layout position and width using a relative value (percentage) and the height by specifying an absolute value (px). As this bar contains a throbber and a message, its position is best suited to the bottom of our page and full width whilst keeping the height small enough that it doesn't effect the user experience. You can find out more about more about AbsoluteLayout values here.

And displaying the message/throbber:

public void DisplaySyncThrobber()
{
    if (objSyncingLayout != null)
    {
        objSyncingLayout.IsVisible = true;
    }
}

You will notice that I'm keeping things simple by just setting the IsVisible property when I need the view to display or hide. Nothing fancy going on here.

Hiding the message/throbber bar:

Since my sync service needs to display a sync result when the sync completes, I needed my 'hide when sync completes' function to be a little more complex than just setting IsVisible to false.

public async void HideSyncThrobber(Boolean blnSuccess, String strMessage = null)
{
    if (!String.IsNullOrWhiteSpace(strMessage))
    {
        if (!blnSuccess)
        {
            await App.Navigation.CurrentPage.DisplayAlert("Sync information", "Sync failed: " + strMessage, "Okay");
            objCompleteLabel.Text = "Sync failed";
        }
        else
        {
            objCompleteLabel.Text = "Sync info: " + strMessage;
        }
    }
    else
    {
        if (!blnSuccess)
        {
            objCompleteLabel.Text = "Sync failed.";
        }
        else
        {
            objCompleteLabel.Text = "Sync complete.";
        }
    }

    if (objSyncingLayout != null)
    {
        await Task.Delay(500);
        objSyncingLayout.IsVisible = false;
        objSyncingCompleteLayout.IsVisible = true;
        await Task.Delay(1500);
        objSyncingCompleteLayout.IsVisible = false;
    }
}

Notice that the first thing the method handles is error reporting, so if the sync fails and I have a message from the server, I opt to display this by calling the DisplayAlert method so the user can scroll through the message result.

If no message was provided, we just set the message label a simple message such as Sync failed or Sync complete.

Next, the method will check to see if the bar exists before hiding it. We achieve a simple transition by hiding the throbber view after a 500ms delay and then displaying the result view before removing it after a 1500ms.

To make sure your message/throbber view is available on each page, add a call to your AddSyncThrobber method:

protected override void OnAppearing()
{
    base.OnAppearing();

    App.SyncService.AddSyncThrobber(mainLayout);
}

Summary:

So we've updated all the pages that need to display the view by adding an AbsoluteLayout as the root item. We then added the helper classes to our sync service and covered the helper methods required to add, display and remove the view from the page.

Here's all the helper functions for easy copy paste:

// ------------------------- Helpers --------------------- //
public void CreateSyncThrobber(AbsoluteLayout objMainLayout)
{
    objSyncingLayout = new ExtendedStackLayout() { Orientation = StackOrientation.Horizontal, BackgroundColor = Core.Colours.Grey, Padding = 10 };

    Label objLabel = new Label() { Text = "Syncing with server... ", TextColor = Color.White, HorizontalOptions = LayoutOptions.FillAndExpand };
    objSyncingLayout.Children.Add(objLabel);

    ActivityIndicator ai = new ActivityIndicator() { HorizontalOptions = LayoutOptions.End };
    ai.IsRunning = true;
    ai.IsEnabled = true;
    ai.WidthRequest = 20;
    ai.HeightRequest = 20;
    objSyncingLayout.Children.Add(ai);

    objMainLayout.Children.Add(objSyncingLayout, new Rectangle(.5, 1, 1, 40), AbsoluteLayoutFlags.PositionProportional | AbsoluteLayoutFlags.WidthProportional);
    objSyncingLayout.IsVisible = false;
            
    objSyncingCompleteLayout = new ExtendedStackLayout() { Orientation = StackOrientation.Horizontal, BackgroundColor = Core.Colours.Grey, Padding = 10 };

    objCompleteLabel = new Label() { TextColor = Color.White, HorizontalOptions = LayoutOptions.FillAndExpand };
    objSyncingCompleteLayout.Children.Add(objCompleteLabel);

    objMainLayout.Children.Add(objSyncingCompleteLayout, new Rectangle(.5, 1, 1, 40), AbsoluteLayoutFlags.PositionProportional | AbsoluteLayoutFlags.WidthProportional);
    objSyncingCompleteLayout.IsVisible = false;
}

public void AddSyncThrobber(AbsoluteLayout objMainLayout)
{
    objMainLayout.Children.Add(objSyncingLayout, new Rectangle(.5, 1, 1, 40), AbsoluteLayoutFlags.PositionProportional | AbsoluteLayoutFlags.WidthProportional);
    objMainLayout.Children.Add(objSyncingCompleteLayout, new Rectangle(.5, 1, 1, 40), AbsoluteLayoutFlags.PositionProportional | AbsoluteLayoutFlags.WidthProportional);
}

public void DisplaySyncThrobber()
{
    if (objSyncingLayout != null)
    {
        objSyncingLayout.IsVisible = true;
    }
}

public async void HideSyncThrobber(Boolean blnSuccess, String strMessage = null)
{
    if (!String.IsNullOrWhiteSpace(strMessage))
    {
        if (!blnSuccess)
        {
            await App.Navigation.CurrentPage.DisplayAlert("Sync information", "Sync failed: " + strMessage, "Okay");
            objCompleteLabel.Text = "Sync failed";
        }
        else
        {
            objCompleteLabel.Text = "Sync info: " + strMessage;
        }
    }
    else
    {
        if (!blnSuccess)
        {
            objCompleteLabel.Text = "Sync failed.";
        }
        else
        {
            objCompleteLabel.Text = "Sync complete.";
        }
    }

    if (objSyncingLayout != null)
    {
        await Task.Delay(500);
        objSyncingLayout.IsVisible = false;
        objSyncingCompleteLayout.IsVisible = true;
        await Task.Delay(1500);
        objSyncingCompleteLayout.IsVisible = false;
    }
}

Published at

Tags: Xamarin,Xamarin Forms,C#,Android,iOS

Luke Alderton

Comments

Share with
Tags
Latest Comments
By Mark Gentry on Windows Server 2019 - Change product key does nothing
20 Aug 2021, 03:30 AM
By Cathy on In-Place Upgrade for a Windows Server domain controller
31 Jul 2021, 18:28 PM
By Mr. Greymatter on Raspberry Pi - Running Java app on Raspbian
16 Feb 2021, 07:35 AM
By Mikko Seittenranta on Xamarin Forms multiple instances of same app open
16 Feb 2021, 04:34 AM
By Andrew on Auto/Custom height on Xamarin Forms WebView for Android and iOS
22 Jan 2021, 22:15 PM
By Nick on Raspberry Pi - Running Java app on Raspbian
14 Oct 2020, 19:37 PM
By Ivan on Fixed: Value cannot be null Parameter Name: source
15 Sep 2020, 19:47 PM
By Anand on Raspberry Pi - Bluetooth using Bluecove on Raspbian
7 Sep 2020, 16:53 PM
Categories
App Development
Event
Game Development
Mapping
Modelling
Programming
Review
Robotics
Tutorial
Web Development