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:
- 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
.
- 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.
- 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}" />