Xamarin Forms: Optional Rendering

One of the most interesting aspects of Xamarin Forms is the ability to hook into the custom rendering engine and redefine how a control will appear on the screen.  As renderers are defined at the platform level, you have the ability to selectively choose what a control will render or even do.  I decided to make use of this feature in Score Predict because of a loading difference between Windows Phone with Android and iPhone.

Note: It is well understood at this point that making special considerations for Windows Phone is not advisable given its relevance within the global smartphone community.

ACR Dialogs

Within just about any app you are going to want to display dialogs to alert the user, gather information from the user, or ask the user to wait for an operation to complete.  Because we are working in a totally platform agnostic way in Forms we need a library which we can use to display a dialog without caring how it gets display.  This is the purpose behind the ACR User Dialogs library.

Problem

The ACR User Dialogs plugin provides some very nice dialogs for loading which I wanted to use on Windows Phone, iOS, and Android.  On Android and iOS this worked without issue.  However, on Windows Phone I ran into a problem because WP will always load the next view in a Panorama so the user may “swipe” to it.  This created a problem which caused ACR to become confused and then made for a confusing experience.  I decided to fix this by removing the ACR loading dialog from Windows Phone, but I wanted to leave it on for Android and iOS. However, I wanted to do this without changing my view models, they should not care about the views implementation.

High Level Solution

Xamarin forms offers a “rendering” concept, whereby we can control what the visual output of a control is.  To that end, we want to create a custom control that shows/hides itself based on a property from the View Model and shows a message when it is visible.  The effect this control has will be different depending on the platform.  Thus, we will need to define a “renderer” for each platform we are supporting.

The goal will be that when the control is visible on WP it fill the entire view with a progress indicator.  A message is display directly beneath the progress indicator.  On Android and iOS we will invoke the ACR User Dialogs, the control will have NO visual component.

Understanding Xaml Rendering

Xaml renders elements on a Z axis in the order in which they appear.  Elements which are defined later are on top of elements which appear earlier.  We can use this to easily have our content loader view “fill” the entire screen.  Here is a sample of the control defined on a Xaml page.  It is ALWAYS the last control defined.

<controls:ContentLoader Message="{Binding LoaderMessage}" HorizontalOptions="Fill" 
     VerticalOptions="Fill" Grid.RowSpan="7" IsVisible="{Binding IsBusy}" />

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

This is nothing more than a simple class which inherits from ContentView, which is your generic run of the mill view that can be used for anything.  Here is out basic implementation

    public class ContentLoader : ContentView
    {

    }

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

Loading the View

Xaml is built with reusability in mind, so we will be loading what our view looks like externally.  We are going to define this using Forms Xaml.  The reason for this is, when we drop this view on a page, it will invoke the native renderers associated with that platform; less work for us to do.  Here is the view Xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="ScorePredict.Core.Controls.ContentLoaderView">

  <StackLayout HorizontalOptions="Fill" VerticalOptions="Fill"
          BackgroundColor="{StaticResource BackgroundColor}"
          Orientation="Vertical">
          <ActivityIndicator HorizontalOptions="Fill" VerticalOptions="Center" IsRunning="True" />
          <Label x:Name="messageLabel" TextColor="White" HorizontalOptions="CenterAndExpand"
               VerticalOptions="Center" />
     </StackLayout>
  
</ContentView>

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

As you can see, its similar to something you might define in Forms Xaml normally.  Using this code will invoke the native renderers for StackLayout, ActivityIndicator, and Label.

We will get to the part where this View is actually loaded via the renderer a bit later.

Enable the Binding

Our next task is to add the custom bindable property for Message, which will enable us to populate the Label as well as display a message when the loading dialogs are visible in iOS and Android.  To do this we add the requisite code to enable the property binding and the subsequent holder property.

I explained this in a previous post which focus on Data Binding with Forms Xaml.  This is the updated (and final version) of ContentLoader.

    public class ContentLoader : ContentView
    {
        public static BindableProperty MessageProperty =
            BindableProperty.Create<ContentLoader, string>(x => x.Message, null);

        public string Message
        {
            get { return GetValue(MessageProperty) as string; }
            set { SetValue(MessageProperty, value); }
        }
    }

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

As you can see, we are only defining the property here so that we can bind to it on our various Xaml pages (shown above).

The reason Message is defined as a bindable property is that it enables me to change the value depending on WHY the screen is busy (ie Load vs Refresh).  In order to support this we must take

Rendering the Custom Loader

So this is where the real fun begins.  We have to remember, that we will want to render this three different ways (two really).  Let’s start with Windows Phone, the only platform that this control will have a visual appearance.

The first thing is to understand how rendering on Xamarin.Forms works.  At a high level, the Element is rendered initially using OnElementChanged.  This method seems to create the initial control and is followed by subsequent invocations of OnElementPropertyChanged which is caused whenever a property on the control changes.

Windows Phone

Because Windows Phone will feature a visual component to the ContentLoader control we must use OnElementChanged to load our custom view when the control is initially created; when OldElement is null.  The following code performs this operation.

        protected override void OnElementChanged(ElementChangedEventArgs<View> e)
        {
            base.OnElementChanged(e);

            if (e.OldElement == null)
            {
                var view = (ContentLoader) e.NewElement;
                view.LoadFromXaml(typeof (ContentLoaderView));
            }
        }

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

Calls to the base method are imperative here, do not leave it out.  Using this, when the control is visible, you will get a blank area with only a Progress Indicator, nothing else, no message.

Our next step is to have that message not only display, but change as the value of the Message property changes on the control.  To handle this, we must implement our logic in OnElementPropertyChanged.  The following accomplishes our goal:

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            var contentLoader = sender as ContentLoader;
            if (sender != null && e.PropertyName == "Message")
            {
                var view = contentLoader.Content;
                if (view != null && view.FindByName<Label>("messageLabel") != null
                    && !string.IsNullOrEmpty(contentLoader.Message))
                {
                    view.FindByName<Label>("messageLabel").Text = contentLoader.Message;
                }
            }
        }

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

The code is very straightforward.  After ensuring that we have the right control and that the target control is available, we set the value.  You will remember messageLabel  was defined in our custom view Xaml.

So what will happen here is the visibility of the control is bound to the IsBusy property on our View Models and is already defined appropriately by ContentView.  We have added logic so that the value stored in our bindable Message property on ContentLoader is visible when the view is visible.

Android and iOS

With Android and iOS I did not want a visual component to be rendered, but instead when the IsVisible flag is changed to true I want to show a dialog via ACR.  However, how we do this is going to be similar to Windows Phone in that the emphasis of our code will use OnElementPropertyChanged.

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            var loader = (ContentLoader)sender;
            if (e.PropertyName == "Message")
                _loaderMessage = loader.Message;

            if (e.PropertyName == "IsVisible" && !string.IsNullOrEmpty(_loaderMessage))
            {
                if (loader.IsVisible)
                {
                    _dialogService = new UserDialogService();
                    _dialogService.ShowLoading(_loaderMessage);
                }
                else
                {
                    _dialogService.HideLoading();
                }
            }
        }

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

In this bit (iOS version) we are focusing on two specific property changes: Message and IsVisible.  Remember what we said earlier, this method gets fired whenever the value of a property on the element changes.  Both IsVisible and Message (custom) are bindable properties whose value will change based on the view model.

When Message changes we update a local private variable.  The value of this variable is checked when IsVisible changes.  Now, we talked about how Acr provides nice cross platform dialogs.  Using the Cross Platform features is great when you are working in a shared environment.  In this case, however, we are in a platform specific rendering, so we can safetly new up the native implementation of UserDialogService.

Much the same code can be found for Android as well

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            var loader = (ContentLoader) sender;
            if (e.PropertyName == "Message")
                _loaderMessage = loader.Message;

            if (e.PropertyName == "IsVisible" && !string.IsNullOrEmpty(_loaderMessage))
            {
                if (loader.IsVisible)
                {
                    _dialogService = new UserDialogService();
                    _dialogService.ShowLoading(_loaderMessage);
                }
                else
                {
                    _dialogService.HideLoading();
                }
            }
        }

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

Result

The result of this approach is that, when Windows Phone runs, we get a visual progress indicator that does not carry the problems of multiple dialogs being presented at once (loading two tabs).  But we have enough control with Forms to render out a blank view that contains logic to show our dialogs.

One thought on “Xamarin Forms: Optional Rendering

Leave a comment