0

I created a DoubleIntSlider custom view with 2 sliders inside it, with 2 BindableProperties associated one to each Slider.Value. But I cannot get the BindableProperties to update the properties assigned to it by the parent page. While using a regular Slider instead of my custom view with the exact same setup works correctly.

I have my FilterPage page defined like so:

public partial class FilterPage : ContentPage
{
    public RestaurantFilter Filter { get; }
    
    public FilterPage(RestaurantFilter filter)
    {
        Filter = filter;
        InitializeComponent();
    }
}

public class RestaurantFilter : INotifyPropertyChanged
{
    private int _minQuality = Restaurant.MIN_QUALITY;

    public int MinQuality
    {
        get => _minQuality;
        set => SetField(ref _minQuality, value);
    }

    private int _minPrice = Restaurant.MIN_PRICE;
    public int MinPrice 
    {
        get => _minPrice;
        set => SetField(ref _minPrice, value);
    }

    private int _maxPrice = Restaurant.MAX_PRICE;
    public int MaxPrice 
    {
        get => _maxPrice;
        set => SetField(ref _maxPrice, value);
    }

    // ...
}

In the XAML for this page, I have 2 views: a Slider and a custom DoubleIntSlider view, defined like so:

<Label 
    Text="Min Quality"
    HorizontalOptions="FillAndExpand" />
<views:IntegerSlider Value="{Binding .Filter.MinQuality}" Maximum="10" Minimum="1" />

<Label 
    Text="Price"
    HorizontalOptions="FillAndExpand" />
<views:DoubleIntSlider 
    MinValue="{Binding .Filter.MinPrice}" 
    MaxValue="{Binding .Filter.MaxPrice}" 
    Maximum="10" Minimum="1" />

With the binding context being the instance of the page. When touch the Sliders in the page, only the RestaurantFilter.MinQuality property has its setter called.


The DoubleIntSlider MinValue and MaxValue properties are defined like this:


public partial class DoubleIntSlider : StackLayout
{
    
    public static readonly BindableProperty MinValueProperty = BindableProperty.Create(
        nameof(MinValue),
        typeof(int),
        typeof(DoubleIntSlider),
        0,
        BindingMode.TwoWay,
        coerceValue: CoerceMinValue, 
        propertyChanged: MinChanged);

    public int MinValue
    {
        get => (int)GetValue(MinValueProperty);
        set => SetValue(MinValueProperty, value);
    }


    public static readonly BindableProperty MaxValueProperty = BindableProperty.Create(
        nameof(MaxValue),
        typeof(int),
        typeof(DoubleIntSlider),
        10,
        BindingMode.TwoWay,
        coerceValue: CoerceMaxValue, 
        propertyChanged: MaxChanged);   

    public int MaxValue
    {
        get => (int)GetValue(MaxValueProperty);
        set => SetValue(MaxValueProperty, value);
    }
    
    private static object CoerceMinValue(BindableObject bindable, object value)
    {
        var instance = (DoubleIntSlider)bindable;
        int minValue = (int)value;

        return minValue.Clamp(instance.Minimum, instance.MaxValue);
    }
    
    private static object CoerceMaxValue(BindableObject bindable, object value)
    {
        var instance = (DoubleIntSlider)bindable;
        int maxValue = (int)value;

        return maxValue.Clamp(instance.MinValue, instance.Maximum);
    }
    
    private static void MinChanged(BindableObject bindable, object oldValue, object newValue)
    {
        var ctrl = (DoubleIntSlider)bindable;
        ctrl.MinValue = (int)newValue;
    }
    
    private static void MaxChanged(BindableObject bindable, object oldValue, object newValue)
    {
        var ctrl = (DoubleIntSlider)bindable;
        ctrl.MaxValue = (int)newValue;
    }

    // ...
}

And the DoubleIntSlider view has, you guessed it, 2 sliders like so:

    <Slider
        Value="{Binding .MinValue}"
        Maximum="{Binding .Maximum}"
        Minimum="{Binding .Minimum}"
        HorizontalOptions="FillAndExpand"
        />
    
    <Slider
        Value="{Binding .MaxValue}"
        Maximum="{Binding .Maximum}"
        Minimum="{Binding .Minimum}"
        HorizontalOptions="FillAndExpand"
    />

I tried all the possible BindingModes for the BindableProperty objects. I also tried with and without a propertyChanged delegate. I also tried putting the coerce code directly in the DoubleIntSlider property setter.

But I could not get the setters for RestaurantFilter.MinPrice and RestaurantFilter.MaxPrice to be called when the sliders are updated.



Edit:

Here DoubleIntSlider's constructor:

public DoubleIntSlider()
{
    PropertyChanged += (_, __) =>
    {
        UpdateBar(); // This method only reads the values
    };
    
    
    InitializeComponent();
}

And the complete XAML for DoubleIntSlider:

<?xml version="1.0" encoding="utf-8"?>
<StackLayout xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:views="clr-namespace:App1.Views;assembly=App1"
             x:Class="App1.Views.DoubleIntSlider"
             x:Name="this"
             BindingContext="{x:Reference this}"
             Padding="10">

    <Slider
        Value="{Binding .MinValue}"
        Maximum="{Binding .Maximum}"
        Minimum="{Binding .Minimum}"
        HorizontalOptions="FillAndExpand"
        />
    
    <StackLayout Orientation="Horizontal"
                 HeightRequest="50"
                 Spacing="0"
                 Padding="10"
                 x:Name="BarLayout">
        <Rectangle x:Name="BarLeftMargin"
                   Stroke="Gray"
                   StrokeThickness="4"
                   StrokeDashArray="1,1"
                   StrokeDashOffset="6"
                   HeightRequest="5"
                   HorizontalOptions="Center"
                   VerticalOptions="Center" />
        <Rectangle Fill="Blue"
                   HorizontalOptions="FillAndExpand"
                   VerticalOptions="FillAndExpand" />
        <Rectangle x:Name="BarRightMargin"
                   Stroke="Gray"
                   StrokeThickness="4"
                   StrokeDashArray="1,1"
                   StrokeDashOffset="6"
                   HorizontalOptions="Center"
                   HeightRequest="5"
                   VerticalOptions="Center" />
    </StackLayout>
    
    <Slider
        Value="{Binding .MaxValue}"
        Maximum="{Binding .Maximum}"
        Minimum="{Binding .Minimum}"
        HorizontalOptions="FillAndExpand"
    />
</StackLayout>
  • The setters might not get called when XAML updates (IIRC, XAML can change the BindableProperty's value directly), but the propertyChanged method should always get called. [Not related, but I am curious: why do all of your binding values start with "."? I've never seen that, in any Microsoft doc or example, except "." by itself, to represent the whole object.] – ToolmakerSteve Jun 20 '23 at 20:12
  • Hey @ToolmakerSteve , thanks for answering! Shouldn't the `MinValueProperty` for example, when updated, also update `FilterPage.Filter.MinValue` automatically per the XAML binding? That is what I am missing here (not talking about `DoubleIntSlider.MinValue`'s setter). Also for the binding starting with `.`. I think it does not change anything. Quite like adding a . at the end of a domain name. I should probably drop this habit. – Clément Couture Jun 21 '23 at 09:05
  • Hmm. I see what you are saying. I was thinking of a different situation. Yes, two-way binding, which you have, should set the source's property. HOWEVER, you don't show all needed information: 1) The constructor of your custom control. 2) The XAML. Add those to question; they affect the fix that is needed. – ToolmakerSteve Jun 21 '23 at 20:36
  • @ToolmakerSteve I just added those. Thank you for looking into this! – Clément Couture Jun 21 '23 at 21:04
  • Remove `BindingContext="{x:Reference this}"`. See [data binding not carrying through to custom component](https://stackoverflow.com/a/73014571/199364). – ToolmakerSteve Jun 21 '23 at 21:43
  • NO F***ING WAY!!! That is so unintuitive! Thank you so much! It works now. Is this because when setting the binding context in the component, the binding then loops? – Clément Couture Jun 22 '23 at 18:32

1 Answers1

0

According to ToolmakerSteve, you can remove BindingContext="{x:Reference this}" to fix it.

For more information you can refer to the pervious answer: .Net MAUI data binding not carrying through to custom component.

Jianwei Sun - MSFT
  • 2,289
  • 1
  • 3
  • 7