Xamarin Forms - Persistent views across multiple pages

« Back to list of posts

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;
    }
}

By Luke Alderton at 13 Dec 2016, 10:50 AM

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

Comments


Post a comment

Please correct the following:
Share with