xamarinmvvmdata-bindingbindablebindableproperty

Can I disable a Button in my Xamarin ContentView custom control using a BindableProperty?


I want to be able to disable a button in a custom header, a ContentView, in my Xamarin.Forms app so that I can warn users that they have unsaved data before navigating away from the ContentPage which contains it.

I have created a BindableProperty for the HeaderView which my containing page binds its view model. I was inspired by this article: Add Custom Controls with Binding Properties to Your Xamarin.Forms App The XAML for the ContentView is here

<?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:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
             x:Class="HeaderView"
             x:Name="HeaderRoot">


       <Grid RowSpacing="0">
        <Grid.RowDefinitions>
            <RowDefinition Height="80"/>
            <RowDefinition Height="5"/>
        </Grid.RowDefinitions>

        <Grid Grid.Row="0" Padding="20,20,20,0" ColumnSpacing="0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="1.3*"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>

        <StackLayout Grid.Column="0">
                <FlexLayout HorizontalOptions="CenterAndExpand" VerticalOptions="Center">
                   <ImageButton x:Name="BackButton" TranslationY="-20" TranslationX="-20" BackgroundColor="Black"  HeightRequest="80" WidthRequest="80"  Source="angleleft.png" Padding="0,0,0,0" Margin="0,0,0,0" Clicked="OnBackClicked" IsEnabled="{Binding Source={x:Reference HeaderRoot}, Path=BindingContext.IsEnabled}" />
                </FlexLayout>
        </StackLayout>

        <FlexLayout Grid.Column="1" JustifyContent="Center" AlignItems="Center">
               <FlexLayout.TranslationY>
                <OnPlatform x:TypeArguments="x:Double">
                    <On Platform="Android" Value="-15"/>
                    <On Platform="iOS" Value="0"/>
                </OnPlatform>
            </FlexLayout.TranslationY>
           <Label x:Name="TitleLabel" FontFamily="{StaticResource TitleFont}" TextColor="White" FontSize="50" />
        </FlexLayout>

           </Grid>
    </Grid>
</ContentView>

The related code behind for the ContentView is here

    public partial class HeaderView : ContentView
    {
        public HeaderView()
        {
            InitializeComponent();

            if (DesignMode.IsDesignModeEnabled)
                return; // Avoid rendering exception in the editor.

            //Argument is null because the Header does not need navigation information.
            BindingContext = new HeaderViewModel(null);
        }
        public static BindableProperty CanNavigateProperty = 
                                 BindableProperty.Create(
            propertyName: "CanNavigate",
            returnType: typeof(bool),
            declaringType: typeof(HeaderView),
            defaultValue: true,
            defaultBindingMode: BindingMode.TwoWay,
            propertyChanged: HandleCanNavigatePropertyChanged);

        private static void HandleCanNavigatePropertyChanged(
            BindableObject bindable, 
            object oldValue, 
            object newValue)
        {
            HeaderView targetView = (HeaderView)bindable;

            if (targetView != null)
                targetView.BackButton.IsEnabled = (bool)newValue;
        }

//...

        public bool CanNavigate
        {
            get
            {
                return (bool)base.GetValue(CanNavigateProperty);
            }
            set
            {
                if(this.CanNavigate != value)
                {
                    base.SetValue(CanNavigateProperty, value);
                }
            }
        }


        protected void OnBackClicked(object sender, EventArgs e)
        {
            if (CanNavigate)
            {
                this.Navigation.PopAsync();
            }
        }

    }

Edit . The view model is quite simple.

    public class HeaderViewModel : ViewModelBase
    {
        public HeaderViewModel(INavigationService navigationService)
            : base(navigationService)
        {
            UserDTO dto = (Prism.PrismApplicationBase.Current as App).CurrentUser;
            UserName = string.Format("{0} {1}", dto.FirstName, dto.LastName);
        }

        private string userName;
        public string UserName 
        { 
            get
            {
                return userName; 
            }
            set
            {
                    SetProperty<string>(ref userName, value);
            }
        }
    }

I then have the following markup in the containing page

<mdaViews:HeaderView CanNavigate="{Binding IsSaved}" Title="COLLECTIONS" Grid.Row="0" />

I have verified that the property, IsSaved, does change its value. The binding works when used in the Text of a Label.

When I change the value of the IsSaved property, the Label will change from "true" to "false" as expected. However this same binding does not appear to change the CanNavigate value in my custom header. While debugging, the OnBackClicked handler always shows a value of CanNavigate == true. The propertyChanged event,HandleCanNavigatePropertyChanged, is never entered. If I am supposed to explicitly call this, I don't know how. If I am missing something or using the wrong approach, I would like to know.

Edit Some of the postings in SO seem to suggest that having a BindingContext set to a view model, might be part of the problem.

Here is such an example: BindableProperty in ContentView not working. I'm not sure what I should do about the view model, as it is working as expected.


Solution

  • I found the answer in the comment, "The page using the ContentView sets its BindingContext which will then be used for all child elements, including the custom ContentView. If you set the context in the content view again, you will overwrite it." by @Krumelur, who was responding to this answer https://stackoverflow.com/a/39989721/117995.

    When I set the BindingContext in my ContentView, I overwrote all that was available from the parent page. To fix this problem I moved all ViewModel processing to the code behind and removed the code where I set the BindingContext.