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 theBindableProperty
. - 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 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:
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
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.
Post a Comment