Skip to content
This repository has been archived by the owner on May 1, 2024. It is now read-only.

Behavior binding context is always null #2581

Closed
nicolas-garcia opened this issue May 2, 2018 · 7 comments
Closed

Behavior binding context is always null #2581

nicolas-garcia opened this issue May 2, 2018 · 7 comments
Assignees
Labels

Comments

@nicolas-garcia
Copy link

nicolas-garcia commented May 2, 2018

Hi!

Description

I found out that if I create a custom behavior and I bind properties in it, those properties values are always null, because the binding context of the behavior is also null.

Steps to Reproduce

  1. Create a custom behavior with bindable properties
  2. Use this behavior in your code, and bind a property to the bindable property of the behavior
  3. The bindable property value is always null

Expected Behavior

The bindable property value should not be null.

Actual Behavior

The bindable property value is null.

Basic Information

  • Version with issue:
    • Latest stable on NuGet.org (2.5.1.444934)
    • Latest stable on MyGet (2.6.0.251661)
    • Latest nightly on MyGet (3.1.0.444738-nightly)
  • Last known good version: None
  • IDE: Visual Studio Pro 2017 15.6.7

Reproduction Link

The following project reproduce the problem.
Compile the project, run the app. Enter a pin code in the entry box. If the pin code entered is 4242, the text should change to "OK!".
The actual behavior we have is a crash because the bindable property CustomCommand is null.

BehaviorBindingBugRepro.zip

Note that I already tried to set the binding context of the behavior like in the following code, but I still have the issue.

                <Entry.Behaviors>
                    <behaviors:CustomBehavior BindingContext="{Binding .}"
                                              TextLenght="4"
                                              CustomCommand="{Binding ValidationCommand}" />
                </Entry.Behaviors>

Thank you!

Edit:
I just found a workaround to make it work while waiting for the fix:

  • Set a x:Name on the Entry with the behavior
  • In the .xaml.cs file of the page with the entry, override the OnAppearing method and add the following code:
        protected override void OnAppearing()
        {
            base.OnAppearing();
            // Replace PinCodeEntry with the name you wrote in x:Name element
            foreach (var behavior in this.PinCodeEntry.Behaviors)
                behavior.BindingContext = this.BindingContext;
        }
@StephaneDelcroix
Copy link
Member

This is my mistake.

Mine.

Only mine.

I can't even remember what I was thinking while doing it, but inheriting Behaviors from BindableObject is a choice I regret almost once a week since then.

Behaviors instances are meant to be shared (and applied through styles). The BindingContext doesn't make any sense in that scenario.

What you have to do is override OnAttachedTo (and detach as well), and if you need the binding context, get the BindingContext of the targetObject. And, as in your case the behavior isn't shared, you should be just fine.

I'm so sorry.

Can I go back to hiding ?

@nicolas-garcia
Copy link
Author

No problem!
Just a lack of documentation maybe?
Anyway, thank you for the solution :)

@thomasgalliker
Copy link

I was just struggling with the same problem as @nicolas-garcia. There is a BindableBase<T> base class for behaviors on the net, which does exactly what @StephaneDelcroix described: It sets the BindingContext of the target object (T) to the behavior as soon as the BindingContext is available.

One implementation of BindableBase<T> can be found here but there are hundreds of it on github...

@StephaneDelcroix
Copy link
Member

@thomasgalliker don't. Behaviors can be shared (when set from Style). this is why they don't have a context

@thomasgalliker
Copy link

thomasgalliker commented Aug 6, 2019

Ups. Correct me if I'm wrong:

  • When I set Behaviors via Styles, it reuses the same Behavior instance?
  • When I set Behaviors directly on controls (e.g. Entry), it creates a new Behavior instance for each application?

I use a dependency property on a behavior in order to limit the max text length of an Entry. In the following case I'm binding the viewmodel property NameMaxLength to the behaviort:

<xControls:ValidatableEntry
    Text="{Binding Name, Mode=TwoWay}">
    <xControls:ValidatableEntry.Behaviors>
        <behaviors:MaxLengthTextBehavior MaxLength="{Binding NameMaxLength}" />
    </xControls:ValidatableEntry.Behaviors>
</xControls:ValidatableEntry>

Edit: Ok, I'm just too stupid for this: Entry (->InputView) exposes a MaxLength property which I can use to bind to. My mistake :)

@StephaneDelcroix
Copy link
Member

Ups. Correct me if I'm wrong:

  • When I set Behaviors via Styles, it reuses the same Behavior instance?
  • When I set Behaviors directly on controls (e.g. Entry), it creates a new Behavior instance for each application?

you're correct

@softeip
Copy link

softeip commented Jul 11, 2023

I have a hack using Behavior for this. For me it works for iOS and Android. Other platform have not tried.

In XAML set binding for ItemsSource.
DO NOT set CarouselView.CurrentItem binding.

<CarouselView
    ItemsSource="{Binding Items}">
    <CarouselView.ItemTemplate>
        <DataTemplate>
            <YourView />
        </DataTemplate>
    </CarouselView.ItemTemplate>
    <CarouselView.Behaviors>
        <hacks:AndroidCarouselFixBehavior CurrentItem="{Binding CurrentItem, Mode=TwoWay}"/>
    </CarouselView.Behaviors>
</CarouselView>

Add this to your project

namespace MyProject.Hacks
{
    public sealed partial class AndroidCarouselFixBehavior : Behavior<CarouselView>
    {
        public static readonly BindableProperty CurrentItemProperty =
            BindableProperty.Create(nameof(CurrentItem), typeof(object), typeof(AndroidCarouselFixBehavior),
                defaultBindingMode: BindingMode.TwoWay,
                propertyChanged: OnCurrentItemChanged);

        public object CurrentItem
        {
            get => (object)GetValue(CurrentItemProperty);
            set => SetValue(CurrentItemProperty, value);
        }

        private static partial void OnCurrentItemChanged(BindableObject bindable, object oldValue, object newValue);

        protected override void OnAttachedTo(BindableObject bindable)
        {
            base.OnAttachedTo(bindable);
            BindingContext = bindable.BindingContext;
            bindable.BindingContextChanged += Bindable_BindingContextChanged;
        }

        protected override void OnDetachingFrom(BindableObject bindable)
        {
            base.OnDetachingFrom(bindable);
            bindable.BindingContextChanged -= Bindable_BindingContextChanged;
            BindingContext = null;
        }

        private void Bindable_BindingContextChanged(object sender, EventArgs e)
        {
            BindingContext = (sender as BindableObject).BindingContext;
        }
    }

#if !ANDROID
    public sealed partial class AndroidCarouselFixBehavior : Behavior<CarouselView>
    {
        private static partial void OnCurrentItemChanged(BindableObject bindable, object oldValue, object newValue)
        {
            
        }

        protected override void OnAttachedTo(CarouselView bindable)
        {
            base.OnAttachedTo(bindable);
            bindable.SetBinding(CarouselView.CurrentItemProperty, new Binding(CurrentItemProperty.PropertyName, source: this));
        }

        protected override void OnDetachingFrom(CarouselView bindable)
        {
            base.OnDetachingFrom(bindable);
            bindable.RemoveBinding(CarouselView.CurrentItemProperty);
        }
    }
#endif
}

#if ANDROID
namespace MyProject.Hacks
{
    public sealed partial class AndroidCarouselFixBehavior
    {
        CarouselView Bindable { get; set; }

        private static partial void OnCurrentItemChanged(BindableObject bindable, object oldValue, object newValue)
        {
            var behavior = bindable as AndroidCarouselFixBehavior;

            var carousel = behavior?.Bindable;
            var source = carousel?.ItemsSource;
            var item = behavior.CurrentItem;

            if (source == null || item == null || carousel.CurrentItem == item)
                return;

            var index = Array.IndexOf(source.Cast<object>().ToArray(), item);
            if (index < 0)
                return;

            carousel.ScrollTo(index);
        }

        protected override void OnAttachedTo(CarouselView bindable)
        {
            base.OnAttachedTo(bindable);
            Bindable = bindable;
            bindable.CurrentItemChanged += Bindable_CurrentItemChanged;
        }

        protected override void OnDetachingFrom(CarouselView bindable)
        {
            base.OnDetachingFrom(bindable);
            Bindable = null;
            bindable.CurrentItemChanged -= Bindable_CurrentItemChanged;
        }

        private void Bindable_CurrentItemChanged(object sender, CurrentItemChangedEventArgs e)
        {
            if (e.CurrentItem is null)
                return;

            CurrentItem = e.CurrentItem;
        }
    }
}
#endif

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

4 participants