ecfedele
ecfedele

Reputation: 316

Updating of property values & bindings in .NET MAUI ContentView

I'm attempting to try my hand at creating a card-based UI similar to Android's CardView layout using MAUI ContentView. The exemplary use case for such an element is the display of testing or scoring results, so I attempted to roll in some of the underlying logic directly into the new element - the user would simply have to supply an Attempted and a Possible set of integers, i.e. 92 and 100, from which it would display an "out-of" format of 92/100 as well as a percentage of 92.0%.

The issue is that I've tried a number of ways to do this, and none successfully update the Score and Percentage properties correctly. I realize that it may be an issue with the order (or simultaneity) of the properties being set, but I haven't been able to rectify it using BindableProperty.Create(..., propertyChanged:) or other methods.

ProgressViewCard.xaml:

<?xml version="1.0" encoding="utf-8" ?>
<ContentView x:Name="this"
             xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="ExampleApp.Controls.ProgressCardView">
    <Frame BindingContext="{x:Reference this}" BackgroundColor="{Binding BackgroundColor}" CornerRadius="5" HasShadow="True">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="3*"   />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="2*"   />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="10*" />
                <ColumnDefinition Width="4*"  />
                <ColumnDefinition Width="4*"  />
            </Grid.ColumnDefinitions>

            <!-- Configure the button functionality of the ProgressCardView -->
            <Button x:Name="InternalButton" Grid.RowSpan="3" Grid.ColumnSpan="2" Opacity="0.0" Clicked="buttonEvent" />

            <Label Text="{Binding Title}" HorizontalOptions="Start" VerticalOptions="Center" FontSize="17" TextColor="{Binding HeaderColor}" />
            <Label Text="{Binding Score}" Grid.Column="1" Grid.ColumnSpan="2" HorizontalOptions="End" VerticalOptions="Center" FontSize="12" TextColor="{Binding TextColor}" />
            <BoxView Grid.Row="1" Grid.ColumnSpan="3" HeightRequest="1" Color="{Binding TextColor}"/>
            <ProgressBar x:Name="CardProgressBar" Grid.Row="2" Grid.ColumnSpan="2" />
            <Label Text="{Binding Percentage}" Grid.Row="2" Grid.Column="2" HorizontalOptions="End" VerticalOptions="Center" FontSize="12" TextColor="{Binding TextColor}" />
        </Grid>
    </Frame>
</ContentView>

ProgressViewCard.xaml.cs:

namespace ExampleApp.Controls
{
    public partial class ProgressCardView : ContentView
    {
        private int attempted  = 0;
        private int possible   = 0;

        #region BindableProperties

        public static readonly BindableProperty TitleProperty = BindableProperty.Create(
            nameof(Title), 
            typeof(string), 
            typeof(ProgressCardView2), 
            string.Empty);

        public static readonly BindableProperty AttemptedProperty = BindableProperty.Create(
            nameof(Attempted),
            typeof(int),
            typeof(ProgressCardView2),
            0);

        public static readonly BindableProperty PossibleProperty = BindableProperty.Create(
            nameof(Possible),
            typeof(int),
            typeof(ProgressCardView2),
            1);

        public static readonly BindableProperty BackgroundColorProperty = BindableProperty.Create(
            nameof(BackgroundColor), 
            typeof(Color), 
            typeof(ProgressCardView2), 
            Color.FromArgb("#FFFFFF"));

        public static readonly BindableProperty HeaderColorProperty = BindableProperty.Create(
            nameof(HeaderColor), 
            typeof(Color), 
            typeof(ProgressCardView2), 
            Color.FromArgb("#FFFFFF"));

        public static readonly BindableProperty TextColorProperty = BindableProperty.Create(
            nameof(TextColor), 
            typeof(Color), 
            typeof(ProgressCardView2), 
            Color.FromArgb("#FFFFFF"));

        #endregion

        #region Getters and Setters

        public string Title 
        {
            get => (string) GetValue(ProgressCardView2.TitleProperty);
            set => SetValue(ProgressCardView2.TitleProperty, value);
        }

        public int Attempted
        {
            get => (int) GetValue(ProgressCardView2.AttemptedProperty);
            set => SetValue(ProgressCardView2.AttemptedProperty, value);
        }

        public int Possible
        {
            get => (int)GetValue(ProgressCardView2.PossibleProperty);
            set => SetValue(ProgressCardView2.PossibleProperty, value);
        }

        public string Score
        {
            get { return String.Format("{0}/{1}", this.attempted, this.possible); }
            set { this.Score = value; }
        }

        public string Percentage
        {
            get { return String.Format("{0:P1}", ((double) this.attempted) / ((double) this.possible)); }
            set { this.Score = value; }
        }

        public Color BackgroundColor
        {
            get => (Color) GetValue(ProgressCardView2.BackgroundColorProperty);
            set => SetValue(ProgressCardView2.BackgroundColorProperty, value);
        }

        public Color HeaderColor
        {
            get => (Color) GetValue(ProgressCardView2.HeaderColorProperty);
            set => SetValue(ProgressCardView2.HeaderColorProperty, value);
        }

        public Color TextColor
        {
            get => (Color) GetValue(ProgressCardView2.TextColorProperty);
            set => SetValue(ProgressCardView2.TextColorProperty, value);
        }

        #endregion

        #region Methods and Events

        public ProgressCardView2()
        {
            InitializeComponent();
        }

        private void buttonEvent(object sender, EventArgs e)
        {

        }

        #endregion
    }
}

The usage of the controls is as follows:

<ctrls:ProgressCardView Title="CS 101 Final"   Attempted="92" Possible="100" BackgroundColor="#ffffff" 
                        HeaderColor="#e74c3c" TextColor="#7f8c8d" />
<ctrls:ProgressCardView Title="ME 302 Midterm" Attempted="68" Possible="85"  BackgroundColor="#ffffff" 
                        HeaderColor="#e74c3c" TextColor="#7f8c8d" />

Screenshot of undesired custom MAUI control behavior

This is the result in an Android emulator (API 31). How do I modify the above control to obtain the correct behavior?

Upvotes: 1

Views: 2620

Answers (2)

ToolmakerSteve
ToolmakerSteve

Reputation: 21213

An alternative with less coding. Don't need propertyChanged methods.

Just add OnPropertyChanged lines to the set methods:

public int Possible
{
    get => (int)GetValue(ProgressCardView2.PossibleProperty);
    set {
        if (SetValue(ProgressCardView2.PossibleProperty, value)) {
            OnPropertyChanged(nameof(Score));
            OnPropertyChanged(nameof(Percentage));
        }
    }
}

Repeat for Attempted.

Either approach works fine.

Upvotes: 2

Liqun Shen-MSFT
Liqun Shen-MSFT

Reputation: 7990

You have done most of the work. I tried your code and made some changes which worked for me.

First, in Custom control ProgressCardView, delete this first two lines:

private int attempted  = 0;
private int possible   = 0;

because this has nothing to do with the BindableProperty and you use the following code which will cause the value always be 0. You could set default value in BindableProperty.Create.

public string Score
{
    get { return String.Format("{0}/{1}", this.attempted, this.possible); } 
}

Second, in BindableProperty, you could define propertyChanged event handler, which could define a callback method when property changed. For more info, you could refer to Detect property changes

And for your case, the change of AttemptedProperty and PossibleProperty would cause the result changed, so we could add propertyChanged to these two BindableProperty. Such like the following:

public static readonly BindableProperty AttemptedProperty = BindableProperty.Create(
    nameof(Attempted),
    typeof(int),
    typeof(ProgressViewCard),
    0,
    propertyChanged:OnThisPropertyChanged);

public static readonly BindableProperty PossibleProperty = BindableProperty.Create(
    nameof(Possible),
    typeof(int),
    typeof(ProgressViewCard),
    1,
    propertyChanged:OnThisPropertyChanged);

For convenience, I think these two properties could share the same propertyChanged callback method, that's to count the value.

Then we could implement the OnThisPropertyChanged callback method:

private static void OnThisPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
    var myCard = bindable as ProgressViewCard;
    myCard.OnPropertyChanged(nameof(Score));
    myCard.OnPropertyChanged(nameof(Percentage));
}

Here is the complete code for ProgressViewCard.cs which worked for me:

#region BindableProperties

public static readonly BindableProperty TitleProperty = BindableProperty.Create(
    nameof(Title),
    typeof(string),
    typeof(ProgressViewCard),
    string.Empty);

public static readonly BindableProperty AttemptedProperty = BindableProperty.Create(
    nameof(Attempted),
    typeof(int),
    typeof(ProgressViewCard),
    0,
    propertyChanged:OnThisPropertyChanged);

public static readonly BindableProperty PossibleProperty = BindableProperty.Create(
    nameof(Possible),
    typeof(int),
    typeof(ProgressViewCard),
    1,
    propertyChanged:OnThisPropertyChanged);

private static void OnThisPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
    var a = bindable as ProgressViewCard;
    a.OnPropertyChanged(nameof(Score));
    a.OnPropertyChanged(nameof(Percentage));
}


public static readonly BindableProperty BackgroundColorProperty = BindableProperty.Create(
    nameof(BackgroundColor),
    typeof(Color),
    typeof(ProgressViewCard),
    Color.FromArgb("#FFFFFF"));

public static readonly BindableProperty HeaderColorProperty = BindableProperty.Create(
    nameof(HeaderColor),
    typeof(Color),
    typeof(ProgressViewCard),
    Color.FromArgb("#FFFFFF"));

public static readonly BindableProperty TextColorProperty = BindableProperty.Create(
    nameof(TextColor),
    typeof(Color),
    typeof(ProgressViewCard),
    Color.FromArgb("#FFFFFF"));

#endregion

#region Getters and Setters

public string Title
{
    get => (string)GetValue(ProgressViewCard.TitleProperty);
    set => SetValue(ProgressViewCard.TitleProperty, value);
}

public int Attempted
{
    get => (int)GetValue(ProgressViewCard.AttemptedProperty);
    set => SetValue(ProgressViewCard.AttemptedProperty, value);
}

public int Possible
{
    get => (int)GetValue(ProgressViewCard.PossibleProperty);
    set => SetValue(ProgressViewCard.PossibleProperty, value);
}

public string Score
{
    get { return String.Format("{0}/{1}", this.Attempted, this.Possible); }
    set { this.Score = value; }
}

public string Percentage
{
    get { return String.Format("{0:P1}", ((double)this.Attempted) / ((double)this.Possible)); }
    set { this.Score = value; }
}

public Color BackgroundColor
{
    get => (Color)GetValue(ProgressViewCard.BackgroundColorProperty);
    set => SetValue(ProgressViewCard.BackgroundColorProperty, value);
}

public Color HeaderColor
{
    get => (Color)GetValue(ProgressViewCard.HeaderColorProperty);
    set => SetValue(ProgressViewCard.HeaderColorProperty, value);
}

public Color TextColor
{
    get => (Color)GetValue(ProgressViewCard.TextColorProperty);
    set => SetValue(ProgressViewCard.TextColorProperty, value);
}

#endregion

#region Methods and Events

public ProgressViewCard()
{
    InitializeComponent();
}

private void buttonEvent(object sender, EventArgs e)
{

}

#endregion

Hope it works for you.

Upvotes: 3

Related Questions