Tuesday, October 7, 2014

Xamarin Forms Custom Controls Without Native Renderers

My last blog post discussed how to create reusable custom content views. It involved leveraging the ContentPage element's ViewModel. This is great when you want to easily pull common code out and make it reusable. This will be a great place to start building.

As with my last blog post, I was not able to find examples of people doing this pattern. It seems like such a common thing to want to do. I digress.

Let's say that there is a repeated layout structure which only differs by the ViewModel properties. What about making a custom control which do not depend on a ViewModel?

A custom control which doesn't depend on a ViewModel internally, will need to have properties which we can bind to a ContentPage ViewModel's property. We want to end up with something like:
<localcontrol:CountLabelView
    CountText="{Binding path=MessageCount}"
    Text="{Binding path=MessageText}" />
The first task is to add a new Forms Xaml Page to the project. I'll call it "CountLabelView".

Then, my experience with building native controls comes in handy. We can add BindableProperty fields to our custom control's code behind. One for each property we want to have available to our control.
public static readonly BindableProperty TextProperty =
    BindableProperty.Create<CountLabelView, string>(
        p => p.Text, 
        "", 
        BindingMode.TwoWay, 
        null,
        new BindableProperty.BindingPropertyChangedDelegate<string>(TextChanged), 
        null, 
        null);

public string Text
{
    get { return (string)GetValue(TextProperty); }
    set { SetValue(TextProperty, value); }
}

static void TextChanged(
    BindableObject obj, 
    string oldPlaceHolderValue, 
    string newPlaceHolderValue)
{
    
}
Now we have a way for a ContentPage to bind properties to custom ContentView controls.

Next we can move our layout into the ContentView.
<?xml version="1.0" encoding="utf-8" ?>
<ContentView 
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:IntPonApp;assembly=IntPonApp"
    xmlns:custom="clr-namespace:CustomControls.Controls;assembly=CustomControls, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
    x:Class="IntPonApp.Controls.CountLabelView">
 
 <StackLayout 
        Orientation="Horizontal" 
        HorizontalOptions="FillAndExpand" 
        VerticalOptions="FillAndExpand" 
        Padding="0">
  <custom:RoundFrame 
            HorizontalOptions="CenterAndExpand" 
            VerticalOptions="CenterAndExpand" 
            Padding="7,1" 
            BorderRadius="40" 
            FillColor="#333333" 
            HasShadow="false">

   <Label    x:Name="CountTextLabel"
                Text="{Binding CountText}" 
                VerticalOptions="Center" 
                XAlign="Center" 
                TextColor="#FFFFFF" />

  </custom:RoundFrame>

  <StackLayout 
            Spacing="0" 
            HorizontalOptions="StartAndExpand" 
            VerticalOptions="CenterAndExpand" 
            Padding="0">

   <Label    x:Name="TextLabel"
                Text="{Binding Text}" 
                LineBreakMode="WordWrap" 
                XAlign="Center" 
                HorizontalOptions="StartAndExpand" 
                VerticalOptions="Center" 
                TextColor="#DEDEDE" />

  </StackLayout>
 </StackLayout>
 
</ContentView>
This is great except, if you run this, the application will fail (unless CountText and Text happen to exist in your ContentPage's ViewModel. This is problematic, but not an insurmountable obstacle. We may have a couple options:
  1. We can remove the binding from the elements, assign names to the elements which need binding, hook up onchange events (not useful in this case because we are using labels and not Entry elements) which would assign the value back to the BindableProperty, and then find the named elements and assign the value from the BindableProperty.
  2. We can assign names to the elements which need binding and then find the named elements and assign the BindingContext to the custom control object.
  3. We can assign the custom control's BindingContext to the custom control itself.
Option 1 would be a mess to implement and maintain. It has to be replicated for every control which needs binding and there is a good chance that the bindings won't work as one should expect.

Option 2: We are able to bind in a much more expected fashion and changes will cascade as we expect. However this will still result in extra 1 line of code per control needing to be bound.

Option 3: It gets rid of the inherited BindingContext and forces the control to be self sufficient and thus reusable regardless of the ViewModel. Plus, the bindings work as one expects and it is only ONE line of code.

In testing Option 3, I found that the BindingContext was not being inherited correctly to the child controls. I suspect this is a bug in Xamarin. So, at the moment Option 2 is the best option that works.

Below is the code behind for the custom control.
public partial class CountLabelView
{
    #region Properties

    public static readonly BindableProperty TextProperty =
        BindableProperty.Create<CountLabelView, string>(
            p => p.Text, 
            "", 
            BindingMode.TwoWay, 
            null,
            new BindableProperty.BindingPropertyChangedDelegate<string>(TextChanged), 
            null, 
            null);

    public string Text
    {
        get { return (string)GetValue(TextProperty); }
        set { SetValue(TextProperty, value); }
    }

    static void TextChanged(
        BindableObject obj, 
        string oldPlaceHolderValue, 
        string newPlaceHolderValue)
    {
        
    }


    public static readonly BindableProperty CountTextProperty =
        BindableProperty.Create<CountLabelView, string>(
            p => p.CountText, 
            "", 
            BindingMode.TwoWay, 
            null,
            new BindableProperty.BindingPropertyChangedDelegate<string>(CountTextChanged), 
            null, 
            null);

    public string CountText
    {
        get { return (string)GetValue(CountTextProperty); }
        set { SetValue(CountTextProperty, value); }
    }

    static void CountTextChanged(
        BindableObject obj, 
        string oldPlaceHolderValue, 
        string newPlaceHolderValue)
    {

    }

    #endregion Properties

    #region Constructor

    public CountLabelView()
    {
        InitializeComponent();
        CountText = "1";
        //this.BindingContext = this;
        CountTextLabel.BindingContext = this;
        Text1Label.BindingContext = this;
        Text2Label.BindingContext = this;
    }

    #endregion Constructor
}
A point of note, the constructor contains this line CountText = "1";. This is required because there is a custom native control surrounding the label with a binding. For some reason the rendering process executes initially before the binding finishes and not having an initial value on the control will result in the custom native control's height to be ~1px. I suspect that this is another bug in Xamarin.

Then you add the XML namespace to the ContentPage declaration and add the custom control.
xmlns:localcontrol="clr-namespace:IntPonApp.Controls;assembly=IntPonApp"
<localcontrol:CountLabelView
    CountText="{Binding path=MessageCount}"
    Text="{Binding path=MessageText}" />

2 comments:

Bobisback said...

Hey,

I would love to see your code for the "RoundFrame" you have in this code above. I am trying to do something similar but I am running into some major issues with it. Would love to see how you solved it.

Thanks,
Sean

Brock said...

Those can be found in RoundFrame. A couple caveats, I have not included projects, so you will have to add the two files to your on respective projects and the code has not been updated to use Xamarin.iOS, but that should be trivial. The core of the problem being solved should still work.