Xamarin Slider: 'Value is an invalid value for Maximum' (Only when navigating away from a page)

Viewed 1491

I have a need to display different UI elements per item in a CollectionView. The base type of each item in the CollectionView is a Setting. There are different types of settings: AnalogSetting, BooleanSetting, RangeSetting, etc. Each derived class has a different set of UI elements to allow the user to interact with it. BooleanSetting may have a simple toggle switch while the AnalogSetting has a slider. The amount and types of settings are delivered at runtime and can change on the fly.

The simplest way I could manage this was to use a DataTemplateSelector and then define different DataTemplates for each derived Setting class. There is a particular DataTemplate that causes my app to crash whenever I navigate away from the page that is displaying the CollectionView.

I have the following CollectionView:

...
<CollectionView 
    x:Name="SettingListView"
    ItemsSource="{Binding Settings}"
    ItemTemplate="{StaticResource settingSelector}"
    SelectionMode="None">
        </CollectionView>

The particular DataTemplate that is causing me grief:

<DataTemplate x:Key="AnalogSetting">
    <StackLayout x:DataType="model:AnalogSetting">
        <Label Text="{Binding Name}"/>
        <Slider Maximum="{Binding MaximumValue}" Minimum="{Binding MinimumValue}" Value="{Binding AnalogValue}">
            <Slider.Behaviors>
                <behaviors:SliderBehavior Command="{Binding SetValue}" />
            </Slider.Behaviors>
        </Slider>
    </StackLayout>
</DataTemplate>

The SliderBehavior class being used in the DataTemplate:

public class SliderBehavior : Behavior<Slider>
{
    public static readonly BindableProperty CommandProperty = BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(SliderBehavior), null);

    public Slider Bindable { get; private set; }

    public ICommand Command
    {
        get { return (ICommand)GetValue(CommandProperty); }
        set { SetValue(CommandProperty, value); }
    }

    protected override void OnAttachedTo(Slider bindable)
    {
        base.OnAttachedTo(bindable);
        Bindable = bindable;
        Bindable.BindingContextChanged += OnBindingContextChanged;
        Bindable.ValueChanged += OnValueChanged;
    }

    protected override void OnDetachingFrom(Slider bindable)
    {
        base.OnDetachingFrom(bindable);
        Bindable.BindingContextChanged -= OnBindingContextChanged;
        Bindable.ValueChanged -= OnValueChanged;
        Bindable = null;
    }

    private void OnBindingContextChanged(object sender, EventArgs e)
    {
        OnBindingContextChanged();
        BindingContext = Bindable.BindingContext;
    }

    private void OnValueChanged(object sender, ValueChangedEventArgs e)
    {
        double value = Math.Round(e.NewValue);

        if (value != Math.Round(e.OldValue))
        {
            Command?.Execute(value.ToString());
        }

        Bindable.Value = value;
    }
}

}

The error given to me whenever I navigate away from the page that is displaying this DataTemplate:

System.ArgumentException: 'Value is an invalid value for Maximum
Parameter name: value'

The relevant portion of the callstack given to me when this exception is thrown:

    0xFFFFFFFFFFFFFFFF in System.Diagnostics.Debugger.Mono_UnhandledException_internal  
0x1 in System.Diagnostics.Debugger.Mono_UnhandledException at /Users/builder/jenkins/workspace/archive-mono/2020-02/android/release/mcs/class/corlib/System.Diagnostics/Debugger.cs:125,4   
0x26 in Android.Runtime.DynamicMethodNameCounter.148    
0xBA in Xamarin.Forms.BindableObject.SetValueCore at D:\a\1\s\Xamarin.Forms.Core\BindableObject.cs:368,5    
0x121 in Xamarin.Forms.Internals.TypedBinding<Common.Models.AnalogSetting,double>.ApplyCore at D:\a\1\s\Xamarin.Forms.Core\TypedBinding.cs:218,5    
0x5D in Xamarin.Forms.Internals.TypedBinding<Common.Models.AnalogSetting,double>.Apply at D:\a\1\s\Xamarin.Forms.Core\TypedBinding.cs:135,4 
0x51 in Xamarin.Forms.BindableObject.ApplyBindings at D:\a\1\s\Xamarin.Forms.Core\BindableObject.cs:480,5   
0x5D in Xamarin.Forms.BindableObject.SetInheritedBindingContext at D:\a\1\s\Xamarin.Forms.Core\BindableObject.cs:209,4  
0x2 in Xamarin.Forms.Element.SetChildInheritedBindingContext at D:\a\1\s\Xamarin.Forms.Core\Element.cs:470,4    
0x8 in Xamarin.Forms.Element.<OnBindingContextChanged>b__82_0 at D:\a\1\s\Xamarin.Forms.Core\Element.cs:308,5   
0x2F in Xamarin.Forms.BindableObjectExtensions.PropagateBindingContext<Xamarin.Forms.Element> at D:\a\1\s\Xamarin.Forms.Core\BindableObjectExtensions.cs:28,5   
0x13 in Xamarin.Forms.Element.OnBindingContextChanged at D:\a\1\s\Xamarin.Forms.Core\Element.cs:306,4   
0x7 in Xamarin.Forms.VisualElement.OnBindingContextChanged at D:\a\1\s\Xamarin.Forms.Core\VisualElement.cs:812,4    
0xD in Xamarin.Forms.View.OnBindingContextChanged at D:\a\1\s\Xamarin.Forms.Core\View.cs:158,4  
0x10 in Xamarin.Forms.BindableObject.BindingContextPropertyChanged at D:\a\1\s\Xamarin.Forms.Core\BindableObject.cs:500,4   C#
0x12E in Xamarin.Forms.BindableObject.SetValueActual at D:\a\1\s\Xamarin.Forms.Core\BindableObject.cs:463,5 
0x17C in Xamarin.Forms.BindableObject.SetValueCore at D:\a\1\s\Xamarin.Forms.Core\BindableObject.cs:397,5   
0x56 in Xamarin.Forms.BindableObject.SetValue at D:\a\1\s\Xamarin.Forms.Core\BindableObject.cs:334,4    
0x5 in Xamarin.Forms.BindableObject.SetValue at D:\a\1\s\Xamarin.Forms.Core\BindableObject.cs:311,68    
0x7 in Xamarin.Forms.BindableObject.set_BindingContext at D:\a\1\s\Xamarin.Forms.Core\BindableObject.cs:41,11   
0x13 in Xamarin.Forms.Platform.Android.TemplatedItemViewHolder.Recycle at D:\a\1\s\Xamarin.Forms.Platform.Android\CollectionView\TemplatedItemViewHolder.cs:38,4    
0x16 in Xamarin.Forms.Platform.Android.ItemsViewAdapter<Xamarin.Forms.GroupableItemsView,Xamarin.Forms.Platform.Android.IGroupableItemsViewSource>.OnViewRecycled at D:\a\1\s\Xamarin.Forms.Platform.Android\CollectionView\ItemsViewAdapter.cs:64,5    
0x32 in Xamarin.Forms.Platform.Android.SelectableItemsViewAdapter<Xamarin.Forms.GroupableItemsView,Xamarin.Forms.Platform.Android.IGroupableItemsViewSource>.OnViewRecycled at D:\a\1\s\Xamarin.Forms.Platform.Android\CollectionView\SelectableItemsViewAdapter.cs:53,4    
0x11 in 

A couple things:

  1. OnDetachingFrom in my custom Behavior<Slider> class never gets called, even after the OnDisappearing function of the owning page is invoked
  2. I'm aware of the requirement to set the sliders Maximum value before the Minimum value. It's very possible the oddities of the Slider class is biting me here.
  3. I believe the AnalogSetting model is being destroyed when the page is navigated away from, but the Behavior<Slider> class is still attempting to use the values that it bound to initially. I don't know this 100%, but it's my current guess.

Debugging this has been a pain, since none of my code is in the callstack. Any tips on how to go about figuring out more information within the callstack would be appreciated.

I'm open to suggestions in regards to using something other than DataTemplateSelector and Behaviors to get what I'm after.

2 Answers

We too had an issue with our sliders when it came to us using a DataTemplate, what we did is as follows: Since each DataTemplate is really just a glorified XAML element we can safely set a x:Name property on our slider and it will be unique and won't cause any conflicts during runttime/compile about non unique names.

So In our SliderTemplate.xaml we set a Max and Min

    <Slider 
          x:Name="NoteSlider"
          Maximum="5"
          Minimum="0"
          ...
        />

Then in our SliderTemplate.xaml.cs:

  1. Define our view model we are using

  2. Override OnBindingContextChanged to actually instantiate it

  3. Set the correct min/max values.

    private MyViewModel controlModel;

        public SliderInputTemplate()
        {
            InitializeComponent();
        }
    
        protected override void OnBindingContextChanged()
        {
            base.OnBindingContextChanged();
            controlModel = this.BindingContext as MyViewModel ;
            BindingContext = controlModel;        }
    
        protected override void OnAppearing()
        {
            base.OnAppearing();
            if(controlModel != null)
            {
                //controlModel.UpdateSlider();
                if(controlModel.Max > controlModel.Min)
                {
                    NoteSlider.Maximum = controlModel.Max;
                    NoteSlider.Minimum = controlModel.Min;
                    NoteSlider.Value = controlModel.SliderValue;
                }
    
                ...
            }
        }
    

I want to say the reason it throws that exception is because odds are the VM is disposed when you are navigating away, but the view is still sorta alive and thus it gets in a bad state. That is how we solved that issue

I was receiving the same error when leaving the page. To fix it I added the FallbackValue parameter to the Maximum attribute in the XAML, and set it to a number greater than what any of my minimum numbers would possibly be.

<Slider Maximum="{Binding Max, FallbackValue=1000000}" Minimum="{Binding Min}" Value="{Binding SettingValue}"></Slider>
Related