Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Please add read-only bindable property #75

Closed
tkouba opened this issue Apr 18, 2024 · 7 comments
Closed

Please add read-only bindable property #75

tkouba opened this issue Apr 18, 2024 · 7 comments
Assignees
Labels
enhancement New feature or request

Comments

@tkouba
Copy link

tkouba commented Apr 18, 2024

Read-only bindable property is defined like this:

public static readonly BindablePropertyKey SizePropertyKey = BindableProperty.CreateReadOnly(nameof(Size), typeof(SizeF), typeof(AxPage), SizeF.Zero);
public static readonly BindableProperty SizeProperty = SizePropertyKey.BindableProperty;

public SizeF Size
{
    get => (SizeF)GetValue(SizeProperty);
    private set => SetValue(SizePropertyKey, value);
}

please add an attribute for generating read-only bindable property. It may helps in clean code - no public setter, when you do not want it.

@KafkaWannaFly KafkaWannaFly self-assigned this Apr 23, 2024
@KafkaWannaFly KafkaWannaFly added the enhancement New feature or request label Apr 23, 2024
@KafkaWannaFly
Copy link
Owner

Hi @tkouba

I tested BindableProperty.CreateReadOnly based on your hint and documentation, but it doesn't work. I assume it would work as BindableProperty.Create.
Here is my sample code:

<!-- CustomLabel.xaml -->
<?xml version="1.0" encoding="utf-8"?>

<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Name="Self"
             x:Class="MauiApp1.Controls.CustomLabel">
    <VerticalStackLayout BindingContext="{x:Reference Self}">
        <Label Text="{Binding Label1}"></Label>
        <Label Text="{Binding Label2}"></Label>
    </VerticalStackLayout>
</ContentView>
// CustomLabel.xaml.cs
using BindableProps;

namespace MauiApp1.Controls;

public partial class CustomLabel : ContentView
{
    [BindableProp] private string _label1 = "Label 1 default";

    public static readonly BindablePropertyKey Label2PropertyKey =
        BindableProperty.CreateReadOnly(nameof(Label2), typeof(string), typeof(CustomLabel), "Label 2 default",
            propertyChanged: (bindable, oldValue, newValue) => ((CustomLabel)bindable).Label2 = (string)newValue);

    public static readonly BindableProperty Label2Property = Label2PropertyKey.BindableProperty;

    public string Label2
    {
        get => (string)GetValue(Label2Property);
        private set
        {
            OnPropertyChanging(nameof(Label2));
            SetValue(Label2PropertyKey, value);
            OnPropertyChanged(nameof(Label2));
        }
    }

    public CustomLabel()
    {
        InitializeComponent();
    }
}
<!-- MainPage.xaml -->
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:controls="clr-namespace:MauiApp1.Controls"
             x:Class="MauiApp1.MainPage">

    <ScrollView>
        <VerticalStackLayout
            Padding="30,0"
            Spacing="25">
            <controls:CustomLabel Label1="L1" Label2="L2"></controls:CustomLabel>
        </VerticalStackLayout>
    </ScrollView>

</ContentPage>

Expect to see L1 + L2
Actual result shows L1 + Label 2 default
image

I would like to know how you make BindableProperty.CreateReadOnly work in your project in order to implement this feature. Thanks.

@tkouba
Copy link
Author

tkouba commented Apr 29, 2024

Here is one example of using read-only property:

public class AxPage : ContentPage
{
    public static readonly BindablePropertyKey SizePropertyKey = BindableProperty.CreateReadOnly(nameof(Size), typeof(SizeF), typeof(AxPage), SizeF.Zero);
    public static readonly BindableProperty SizeProperty = SizePropertyKey.BindableProperty;

    public SizeF Size
    {
        get => (SizeF)GetValue(SizeProperty);
        private set => SetValue(SizePropertyKey, value);
    }

    protected override void OnSizeAllocated(double width, double height)
    {
        Size = new SizeF(Convert.ToSingle(width), Convert.ToSingle(height));
        base.OnSizeAllocated(width, height);
    }
}

Property is set only from class itself. I'm using this page descendant for portrait/lanscape detection in model.

Edit:

Using this page

<?xml version="1.0" encoding="utf-8" ?>
<local:AxPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:MauiAppReadOnlyProperty"
             x:Class="MauiAppReadOnlyProperty.MainPage"
             x:DataType="local:MainViewModel"
             Title="{Binding Title}"
             Size="{Binding Size}">

    <ScrollView>
        <VerticalStackLayout
            Padding="30,0"
            Spacing="25">

            <Label Text="{x:Binding Size, Source={x:RelativeSource AncestorType={x:Type local:AxPage}}}"
                   HorizontalOptions="FillAndExpand" HorizontalTextAlignment="Center"     />

            <Label Text="{x:Binding Size}"
                   HorizontalOptions="FillAndExpand" HorizontalTextAlignment="Center"     />

        </VerticalStackLayout>
    </ScrollView>

</local:AxPage>

code behind:

namespace MauiAppReadOnlyProperty
{
    public partial class MainPage
    {
        public MainPage()
        {
            InitializeComponent();
            BindingContext = new MainViewModel();
        }
    }
}

and view model

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;

namespace MauiAppReadOnlyProperty
{
    public class MainViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler? PropertyChanged;

        void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        bool SetValue<T>([NotNullIfNotNull(nameof(newValue))] ref T field, T newValue, Action? onChanged = null, [CallerMemberName] string? propertyName = null)
        {
            if (EqualityComparer<T>.Default.Equals(field, newValue))
            {
                return false;
            }
            field = newValue;
            OnPropertyChanged(propertyName);
            onChanged?.Invoke();
            return true;
        }

        string title = "Default title";
        public string Title { get => title; set => SetValue(ref title, value); }


        SizeF size;
        public SizeF Size
        {
            get => size;
            set => SetValue(ref size, value, OnSizeChanged);
        }
        void OnSizeChanged()
        {
            if (Size == SizeF.Zero)
            {
                Title = "Unknown size";
            }
            else if (Size.Height > Size.Width)
            {
                Title = "Portrait";
            }
            else
            {
                Title = "Landscape";
            }

        }
    }
}

@tkouba tkouba removed their assignment Apr 29, 2024
@KafkaWannaFly KafkaWannaFly self-assigned this Apr 29, 2024
@KafkaWannaFly
Copy link
Owner

KafkaWannaFly commented Apr 30, 2024

Hi @tkouba

As I see, the prop of BindableProperty.CreateReadOnly is used in different way compare to BindableProperty.Create, I expect they should be similar except a private setter. My goal is to maximize compatibility between those two.
For example, you are using [BindableProp] for BindableProperty.Create. I release [BindableReadOnlyProp] for BindableProperty.CreateReadOnly. Then, you can replace all [BindableProp] with [BindableReadOnlyProp] and your app is still fine.
However, based on our above conversation, doing so would break the app. I need to seek for a way to achieve the compatibility. Maybe it is not how we define BindableProperty.CreateReadOnly, but it about how we use the prop in our controls and pages. I might need to update documentation, so everyone can do the right way.
Can you upload and share a link to your example repo, please? I will start with a working example.

Thanks.

@tkouba
Copy link
Author

tkouba commented May 1, 2024

Hi @KafkaWannaFly,

I created repo with simple read-only bindable property https://github.com/tkouba/MauiAppReadOnlyProperty. This property is not autogenerated, but it could be. AxPage has read-only property Size. And there are model, which uses this property for porptrait/landscape detection and label on page which shows current size of page.

@tkouba tkouba removed their assignment May 1, 2024
KafkaWannaFly added a commit that referenced this issue May 4, 2024
* Refactor to reduce duplicate

* BindableReadOnlyProp and a simple test case

* Abtract test class

* Unit test

* Config version

* Clean up code

* Update documentation

* Update cicd

* Filter test

* Upgrade cpdeql version
@KafkaWannaFly
Copy link
Owner

Hi @tkouba
Please upgrade to v1.4.0 and try. As I test on your sample project, funny thing happen.

[BindableReadOnlyProp] private SizeF _size = SizeF.Zero; // Value changed but always show {0, 0} on UI
[BindableReadOnlyProp] private bool _isLandscape = false; // Work as expected

Can be fixed like this. I guess this was the problem of MAUI itself, have no idea why.

[BindableReadOnlyProp(DefaultBindingMode = (int)BindingMode.TwoWay)] private SizeF _size = SizeF.Zero;

@tkouba
Copy link
Author

tkouba commented May 10, 2024

Yes it works. Great work @KafkaWannaFly

In MAIU, definition of CreateReadOnly method is

public static BindablePropertyKey CreateReadOnly(string propertyName, [DynamicallyAccessedMembers(ReturnTypeMembers)] Type returnType, [DynamicallyAccessedMembers(DeclaringTypeMembers)] Type declaringType, object defaultValue, BindingMode defaultBindingMode = BindingMode.OneWayToSource, ValidateValueDelegate validateValue = null, BindingPropertyChangedDelegate propertyChanged = null, BindingPropertyChangingDelegate propertyChanging = null, CoerceValueDelegate coerceValue = null, CreateDefaultValueDelegate defaultValueCreator = null)

So default binding mode for read-only properties is BindingMode.OneWayToSource. I think, you can follow MAUI and change source generator to set binding mode to 3 instead of 0.

@KafkaWannaFly
Copy link
Owner

@tkouba Thanks for your advice.
I have updated as you suggest. You can upgrade your version to 1.4.1 and enjoy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants