Updating of property values & bindings in .NET MAUI ContentView - data-binding

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" />
This is the result in an Android emulator (API 31). How do I modify the above control to obtain the correct behavior?

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.

Related

XFC0009 No property, BindableProperty, or event found for "SelectItem", or mismatching type between value and property

<Ctrl:CtrlPickerButton
x:Name="xEditSpeed"
Grid.Row="5"
Grid.Column="1"
Padding="0,10,0,10"
SelectItem="{Binding Speed}" />
*************** CtrlPickerButton.xaml ****************
<ContentView
x:Class="POP.Controls.CtrlPickerButton"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:Ctrl="clr-namespace:POP.Controls"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand">
<ContentView.Content>
<Frame
Padding="1"
BackgroundColor="{StaticResource ColorBorder000}"
CornerRadius="0"
HasShadow="False"
HorizontalOptions="FillAndExpand">
<Frame
Padding="0"
CornerRadius="0"
HasShadow="false">
<Ctrl:PickerCommon
x:Name="xPicker"
Margin="5,0,0,0"
HorizontalOptions="FillAndExpand"
TextColor="{DynamicResource ColorText000}"
VerticalOptions="FillAndExpand" />
</Frame>
</Frame>
</ContentView.Content>
</ContentView>
*************** CtrlPickerButton.cs ****************
public partial class CtrlPickerButton : ContentView
{
public CtrlPickerButton()
{
InitializeComponent();
}
public int SelectItem
{
get
{
return (int)GetValue(ItemChangedProperty);
}
set
{
SetValue(ItemChangedProperty, value);
xPicker.SelectedIndex = value;
}
}
public static readonly BindableProperty ItemChangedProperty = BindableProperty.Create(
propertyName: nameof(SelectItem),
returnType: typeof(int),
declaringType: typeof(CtrlPickerButton),
defaultValue: 1,
defaultBindingMode: BindingMode.TwoWay,
propertyChanged: SelectedItemProPertyChanged);
private static void SelectedItemProPertyChanged(BindableObject bindable, object oldValue, object newValue)
{
(bindable as CtrlPickerButton).SelectItem = (int)newValue;
}
}
The binding property naming convention was followed.
But I get a Missmatching error.
What went wrong?
So the thing is that there is a Naming convention that you need to follow when you create a Bindable property.
Your Property has a name that follows CamelCase. Eg below:
public int SelectedItem { get; set; }
Create your Bindable part for your property:
public static readonly BindableProperty SelectedItemProperty = BindableProperty.Create(
propertyName: nameof(SelectedItem),
returnType: typeof(int),
declaringType: typeof(CtrlPickerButton),
defaultValue: 1,
defaultBindingMode: BindingMode.TwoWay,
propertyChanged: SelectedItemProPertyChanged);
Focus on how the name of your BindableProperty's name is your PropertyName+ the word Property so "SelectedItemProperty", Now the first value that you need to pass in your Create method is the name of your property i.e. "SelectedItem"
Now your property's getter and setter change to update based on our Bindable property:
public int SelectedItem
{
get =>(int)GetValue(SelectedItemProperty);
set => SetValue(SelectedItemProperty, value);
}
More detailed information here: https://learn.microsoft.com/en-us/xamarin/xamarin-forms/xaml/bindable-properties#create-a-bindable-property
Hope this helps
Good luck!

How to access the BindingContext of custom control in Xamarin.Forms

I have a CollectionView with ItemsSource set to ObservableCollection of type Employee.
The ItemTemplate of the CollectionView is a CustomControl that has 1 BindableProperty of Type Employee
MainPage.xaml:
<CollectionView ItemsSource="{Binding Employees}"
SelectedItem="{Binding SelectedEmployee}">
<CollectionView.ItemTemplate>
<DataTemplate>
<controls:CustomControl Employee="{Binding .}" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
The CustomControl has an image (checked image to indicate selection).
CustomControl.xaml:
<Frame HasShadow="True"
BackgroundColor="Blue">
<StackLayout Orientation="Horizontal">
<Label Text="{Binding Name}" />
<Image Source="check.png" />
</StackLayout>
</Frame>
CustomControl.xaml.cs:
public partial class CustomControl : ContentView
{
public CustomControl()
{
InitializeComponent();
}
public static BindableProperty EmployeeProperty = BindableProperty.Create(
propertyName: nameof(Employee),
returnType: typeof(Employee),
declaringType: typeof(CustomControl),
defaultValue: default(Employee),
defaultBindingMode: BindingMode.OneWay);
public Employee Employee
{
get
{
return (Employee)GetValue(EmployeeProperty);
}
set
{
SetValue(EmployeeProperty, value);
}
}
}
Model (Employee):
public class Employee: INotifyPropertyChanged
{
private int name;
public int Name
{
get
{
return name;
}
set
{
name = value;
OnPropertyChanged(nameof(Name));
}
}
private int isSelected;
public int IsSelected
{
get
{
return isSelected;
}
set
{
isSelected = value;
OnPropertyChanged(nameof(IsSelected));
}
}
#region PropertyChanged
public void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
I am trying to create simple animation (FadeIn/FadeOut) for the checked image in the CustomControl so when an item is selected the image will fade in, and when unselected it will fade out. I could use IsVisible and set it to true/false but that's ugly.
My idea was to listen to PropertyChanged event of the Employee (which supposed to be the context of my CustomControl), and when the property IsSelected is modified, I will start the animation to show/hide the image. something like this
public CustomControl()
{
InitializeComponent();
(this.BindingContext as Employee).PropertyChanged += CustomControl_PropertyChanged;
}
private void CustomControl_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Employee.IsSelected))
{
//do animation to show/hide image
}
}
But couldn't access the Context of my CustomControl!
When I declare the binding in MainPage.xaml I am passing a single Emplyee objet as BindingContext (that dot, right?):
<controls:CustomControl Employee="{Binding .}" />
but after the CustomControl is initializd, the BindingContext is still null!
public CustomControl()
{
InitializeComponent();
var context = this.BindingContext; //this is null
}
How can I observe the changes on the IsSelected property of the Employee object from my CustomControl?
In your custom control override the OnBindingContextChanged method, inside of that method you should be able to access the binding context that is set for your view.
Ex:
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
var context = this.BindingContext as Employee
}

Passing Commands to Button in ContentView

I have a ContentView with a button inside a grid. I made bindable property for the text value on the button, and the command. The text value is being set properly, but not the button? I don't understand what's going on here. Anyone have any insights?
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="Aboo.Components.BusyButton"
BindingContext="{Binding Source={RelativeSource Self}}">
<ContentView.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button x:Name="Button" Text="{Binding Text}" Command="{Binding Command}" Grid.ColumnSpan="3" />
<ActivityIndicator Grid.Column="1" IsRunning="{Binding IsBusy}" Color="White"/>
</Grid>
</ContentView.Content>
</ContentView>
And code-behind:
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class BusyButton : ContentView
{
public BusyButton()
{
InitializeComponent();
}
public static readonly BindableProperty TextProperty =
BindableProperty.Create("Text", typeof(string), typeof(BusyButton), default(string));
public string Text
{
get => (string) GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
public static readonly BindableProperty CommandProperty =
BindableProperty.Create("Command", typeof(ICommand), typeof(BusyButton), default(ICommand));
public ICommand Command
{
get => (ICommand) GetValue(CommandProperty);
set => SetValue(CommandProperty, value);
}
public static readonly BindableProperty IsBusyProperty =
BindableProperty.Create("IsBusy", typeof(bool), typeof(BusyButton), default(bool), propertyChanged: IsBusyChanged);
private static void IsBusyChanged(BindableObject bindable, object oldvalue, object newvalue)
{
var busyButton = bindable as BusyButton;
if (busyButton == null)
return;
busyButton.Button.IsEnabled = !(bool) newvalue;
}
public bool IsBusy
{
get => (bool) GetValue(IsBusyProperty);
set => SetValue(IsBusyProperty, value);
}
}
Using the button like this:
Please delete the BindingContext="{Binding Source={RelativeSource Self}}" this line.
Add the Content.BindingContext = this; in your BusyButton constructor. Based on my test BindingContext="{Binding Source={RelativeSource Self}}" not worked in the nasted binding.
Please add a name x:Name="MyButton" for Button in ContentView, then change the button name in IsBusyChanged method.
Here is my test GIF.
Here is my code.BusyButton.xaml
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="App2.BusyButton"
x:Name="MyContentview"
>
<ContentView.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button x:Name="MyButton" Text="{Binding Text}" Command="{Binding Command}" Grid.ColumnSpan="3" />
<ActivityIndicator Grid.Column="1" IsRunning="{Binding IsBusy,Mode=TwoWay}" Color="White"/>
</Grid>
</ContentView.Content>
</ContentView>
BusyButton.xaml.cs
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class BusyButton : ContentView
{
public BusyButton()
{
InitializeComponent();
Content.BindingContext = this;
}
public static readonly BindableProperty TextProperty =
BindableProperty.Create("Text", typeof(string), typeof(BusyButton), default(string));
public string Text
{
get => (string)GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
public static readonly BindableProperty CommandProperty =
BindableProperty.Create(nameof(Command) , typeof(ICommand), typeof(BusyButton), default(ICommand));
public ICommand Command
{
get => (ICommand)GetValue(CommandProperty);
set => SetValue(CommandProperty, value);
}
public static readonly BindableProperty IsBusyProperty =
BindableProperty.Create("IsBusy", typeof(bool), typeof(BusyButton), default(bool), propertyChanged: IsBusyChanged);
private static void IsBusyChanged(BindableObject bindable, object oldvalue, object newvalue)
{
var busyButton = bindable as BusyButton;
if (busyButton == null)
return;
busyButton.MyButton.IsEnabled = !(bool)newvalue;
}
public bool IsBusy
{
get => (bool)GetValue(IsBusyProperty);
set => SetValue(IsBusyProperty, value);
}
}
I test it in the MainPage.xaml.
<StackLayout>
<!-- Place new controls here -->
<local:BusyButton x:Name="busyButton" Text="{Binding Name}" Command="{Binding ChangeCommand}" IsBusy="{Binding Changed, Mode=TwoWay}"></local:BusyButton>
</StackLayout>
Here is background code.MainPage.xaml.cs
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
Content.BindingContext = new MyModelView();
}
}
Here is my test MyModelView.
public class MyModelView: INotifyPropertyChanged
{
public ICommand ChangeCommand { protected set; get; }
public string Name { get; set; }
bool _changed = false;
public bool Changed
{
get
{
return _changed;
}
set
{
if (_changed != value)
{
_changed = value;
OnPropertyChanged("Changed");
}
}
}
public MyModelView() {
Name = "test";
ChangeCommand = new Command(() =>
{
Changed = true;
Console.WriteLine("================test Command execute=================");
});
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Here is my mode.
https://github.com/851265601/XFormsNestedBindingContentView

Xamarin Forms how to add behaviors to custom control

I have created a custom control,which is a ContentView with a Label and an Entry
The xaml of the custom controls looks like this:
<Label Text="{Binding Source={x:Reference ValidationControl}, Path=Caption}"/>
<Entry Text="{Binding Source={x:Reference ValidationControl}, Path=Value, Mode=TwoWay}" />
The code behind of the custom control looks like this:
public static readonly BindableProperty CaptionProperty = BindableProperty.Create(
nameof(Caption), typeof(string), typeof(ValidationEntry), default(string));
public string Caption
{
get => (string)GetValue(CaptionProperty);
set => SetValue(CaptionProperty, value);
}
public static readonly BindableProperty ValueProperty = BindableProperty.Create(
nameof(Value), typeof(string), typeof(ValidationEntry), default(string));
public string Value
{
get => (string)GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
I’m using the custom control in the following way
<controls:ValidationEntry Caption=”Name:” Value="{Binding FullName, Mode=TwoWay}" />
My question is how to add behaviors to the custom control?
I would like to add them in the place that I’m using the control. i.e.
<controls:ValidationEntry Caption="Name:"
Value="{Binding FullName, Mode=TwoWay}">
<controls:ValidationEntry.EntryBehaviors>
<behaviors:EntryLengthValidatorBehavior IgnoreSpaces="True"/>
</controls:ValidationEntry.EntryBehaviors>
</controls:ValidationEntry>
You can create a behaviors directly, I add a NumericValidationBehavior in my custom entry to check the data if it is double.If type of the data is not double, the color of text will be set to red.
Here is xaml code.
<StackLayout>
<local:MyEntry local:NumericValidationBehavior.AttachBehavior="true">
</local:MyEntry>
</StackLayout>
Here is NumericValidationBehavior.cs
public static class NumericValidationBehavior
{
public static readonly BindableProperty AttachBehaviorProperty =
BindableProperty.CreateAttached(
"AttachBehavior",
typeof(bool),
typeof(NumericValidationBehavior),
false,
propertyChanged: OnAttachBehaviorChanged);
public static bool GetAttachBehavior(BindableObject view)
{
return (bool)view.GetValue(AttachBehaviorProperty);
}
public static void SetAttachBehavior(BindableObject view, bool value)
{
view.SetValue(AttachBehaviorProperty, value);
}
static void OnAttachBehaviorChanged(BindableObject view, object oldValue, object newValue)
{
var entry = view as Entry;
if (entry == null)
{
return;
}
bool attachBehavior = (bool)newValue;
if (attachBehavior)
{
entry.TextChanged += OnEntryTextChanged;
}
else
{
entry.TextChanged -= OnEntryTextChanged;
}
}
static void OnEntryTextChanged(object sender, TextChangedEventArgs args)
{
double result;
bool isValid = double.TryParse(args.NewTextValue, out result);
((Entry)sender).TextColor = isValid ? Color.Default : Color.Red;
}
}
Update
I create a custom view with ContentView
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="BeHavDemo.MyView">
<ContentView.Content>
<StackLayout>
<Label Text="xxxx"/>
<Entry Text="eeeee" />
</StackLayout>
</ContentView.Content>
</ContentView>
Then I create a behavior.
public class MyBeha : Behavior<MyView>
{
protected override void OnAttachedTo(BindableObject view)
{
base.OnAttachedTo(view);
var myview=view as MyView;
StackLayout stackLayout = (StackLayout)myview.Content;
Label label = (Label)stackLayout.Children[0];
Entry entry=(Entry) stackLayout.Children[1];
}
}

Can't get InputTransparent to work on custom PopOver Control

I've got the following Xamarin.Forms TemplatedView:
using System;
using System.Threading.Tasks;
using System.Windows.Input;
using Xamarin.Forms;
namespace STM.Framework.Controls
{
[ContentProperty(nameof(Content))]
public class PopOverLayout : TemplatedView
{
public PopOverLayout()
{
}
#region BoxViewTapCommand
public static BindableProperty BoxViewTapCommandProperty = BindableProperty.Create(nameof(BoxViewTapCommand), typeof(ICommand), typeof(PopOverLayout), default(ICommand), defaultValueCreator: BoxViewTapCommandCreator);
private static object BoxViewTapCommandCreator(BindableObject bindable)
{
return new Command(p => BoxViewTappedExecute(bindable as PopOverLayout, p));
}
private static void BoxViewTappedExecute(PopOverLayout bindable, object parameter)
{
bindable.IsOverlayVisible = !bindable.IsOverlayVisible;
if (!bindable.IsOverlayVisible)
{
bindable.OverlayContent.InputTransparent = true;
}
}
public ICommand BoxViewTapCommand
{
get { return (ICommand) GetValue(BoxViewTapCommandProperty); }
set { SetValue(BoxViewTapCommandProperty, value); }
}
#endregion BoxViewTapCommand
#region OverlayContent
public static BindableProperty OverlayContentProperty = BindableProperty.Create(nameof(OverlayContent), typeof(VisualElement), typeof(PopOverLayout), default(VisualElement));
public VisualElement OverlayContent
{
get { return (VisualElement) GetValue(OverlayContentProperty); }
set { SetValue(OverlayContentProperty, value); }
}
#endregion OverlayContent
#region Content
public static BindableProperty ContentProperty = BindableProperty.Create(nameof(Content), typeof(VisualElement), typeof(PopOverLayout), default(VisualElement));
public VisualElement Content
{
get { return (VisualElement) GetValue(ContentProperty); }
set { SetValue(ContentProperty, value); }
}
#endregion Content
#region IsOverlayVisible
public static BindableProperty IsOverlayVisibleProperty = BindableProperty.Create(nameof(IsOverlayVisible), typeof(bool), typeof(PopOverLayout), default(bool));
public bool IsOverlayVisible
{
get { return (bool) GetValue(IsOverlayVisibleProperty); }
set { SetValue(IsOverlayVisibleProperty, value); }
}
#endregion IsOverlayVisible
}
}
App.xaml:
<Style TargetType="{x:Type controls:PopOverLayout}">
<Setter Property="BackgroundColor" Value="#88000000" />
<Setter Property="IsOverlayVisible" Value="False" />
<Setter Property="ControlTemplate">
<Setter.Value>
<ControlTemplate>
<Grid BackgroundColor="{TemplateBinding BackgroundColor}" >
<Grid.RowDefinitions>
<RowDefinition>
<RowDefinition.Height>
<OnIdiom x:TypeArguments="GridLength" Phone="0" Tablet="50" />
</RowDefinition.Height>
</RowDefinition>
<RowDefinition Height="*" />
<RowDefinition>
<RowDefinition.Height>
<OnIdiom x:TypeArguments="GridLength" Phone="0" Tablet="50" />
</RowDefinition.Height>
</RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition>
<ColumnDefinition.Width>
<OnIdiom x:TypeArguments="GridLength" Phone="0" Tablet="50" />
</ColumnDefinition.Width>
</ColumnDefinition>
<ColumnDefinition Width="*" />
<ColumnDefinition>
<ColumnDefinition.Width>
<OnIdiom x:TypeArguments="GridLength" Phone="0" Tablet="50" />
</ColumnDefinition.Width>
</ColumnDefinition>
</Grid.ColumnDefinitions>
<controls:EnhancedContentPresenter Grid.Row="0" Grid.Column="0" Grid.RowSpan="3" Grid.ColumnSpan="3"
Content="{TemplateBinding Content}" />
<BoxView Grid.Row="0" Grid.Column="0" Grid.RowSpan="3" Grid.ColumnSpan="3"
InputTransparent="{TemplateBinding IsOverlayVisible, Converter={StaticResource BooleanInversionConverter}, Mode=OneWay}"
IsVisible="{TemplateBinding IsOverlayVisible}"
BackgroundColor="{TemplateBinding BackgroundColor}">
<BoxView.GestureRecognizers>
<TapGestureRecognizer Command="{TemplateBinding BoxViewTapCommand}"></TapGestureRecognizer>
</BoxView.GestureRecognizers>
</BoxView>
<controls:EnhancedContentPresenter Grid.Row="1" Grid.Column="1" Content="{TemplateBinding OverlayContent}"
InputTransparent="{TemplateBinding IsOverlayVisible, Converter={StaticResource BooleanInversionConverter}, Mode=OneWay}"
IsVisible="{TemplateBinding IsOverlayVisible}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Converter:
public class BooleanInversionConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType != typeof(bool))
throw new NotSupportedException();
if (value.GetType() != typeof(bool))
throw new NotSupportedException();
var casted = (bool)value;
return !casted;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType != typeof(bool))
throw new NotSupportedException();
if (value.GetType() != typeof(bool))
throw new NotSupportedException();
var casted = (bool)value;
return !casted;
}
}
ContentPresenter:
public class EnhancedContentPresenter : ContentPresenter
{
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
if (BindingContext != null && this.Content != null)
SetInheritedBindingContext(this.Content, BindingContext);
}
}
Due to the nature of the control it is pretty obvious what i am trying to do i guess: If the overlay is hidden i don't want the overlay to capture tap (or other) events and instead behave like it didn't even exist.
However i can't seem to get this working because something keeps capturing touch events, even if the overlay is hidden and InputTransparent is set to true.
Does this problem ring a bell to anyone?
I'm not sure whether something is still wrong with templatedView (because they sure lack a lot of wpf features), but doing it like this worked just fine:
[ContentProperty(nameof(Content))]
public class PopOverLayout : RelativeLayout
{
#region OverlayContent
public static BindableProperty OverlayContentProperty = BindableProperty.Create(nameof(OverlayContent), typeof(View), typeof(PopOverLayout), default(View), propertyChanged: OverlayContentChanged);
private static void OverlayContentChanged(BindableObject bindable, object oldValue, object newValue)
{
var layout = bindable as PopOverLayout;
if (layout == null)
return;
layout.UpdateOverlayContent();
}
public View OverlayContent
{
get { return (View) GetValue(OverlayContentProperty); }
set { SetValue(OverlayContentProperty, value); }
}
#endregion OverlayContent
#region Content
public static BindableProperty ContentProperty = BindableProperty.Create(nameof(Content), typeof(View), typeof(PopOverLayout), default(View), propertyChanged: ContentChanged);
private static void ContentChanged(BindableObject bindable, object oldvalue, object newvalue)
{
var layout = bindable as PopOverLayout;
if (layout == null)
return;
layout.UpdateDisplayContent();
}
public View Content
{
get { return (View) GetValue(ContentProperty); }
set { SetValue(ContentProperty, value); }
}
#endregion Content
#region IsOverlayVisible
public static BindableProperty IsOverlayVisibleProperty = BindableProperty.Create(nameof(IsOverlayVisible), typeof(bool), typeof(PopOverLayout), default(bool), propertyChanged: IsOverlayVisibleChanged);
private static void IsOverlayVisibleChanged(BindableObject bindable, object oldvalue, object newvalue)
{
var layout = bindable as PopOverLayout;
if (layout == null)
return;
bool newState = (bool) newvalue;
if (newState)
{
layout.ShowPopup();
}
else
{
layout.HidePopup();
}
}
public bool IsOverlayVisible
{
get { return (bool) GetValue(IsOverlayVisibleProperty); }
set { SetValue(IsOverlayVisibleProperty, value); }
}
#endregion IsOverlayVisible
protected override void OnParentSet()
{
base.OnParentSet();
UpdateDisplayContent();
UpdateOverlayContent();
}
private View content;
private RelativeLayout popup;
private void UpdateDisplayContent()
{
if (content != null)
this.Children.Remove(content);
if (Content != null)
{
content = Content;
this.Children.Add(content,
Constraint.Constant(0),
Constraint.Constant(0),
Constraint.RelativeToParent(layout => layout.Width),
Constraint.RelativeToParent(layout => layout.Height));
Content.InputTransparent = IsOverlayVisible;
}
}
private void UpdateOverlayContent()
{
if (popup != null)
this.Children.Remove(popup);
if (OverlayContent != null && IsOverlayVisible)
{
ShowPopup();
}
}
private void HidePopup()
{
if (popup != null)
this.Children.Remove(popup);
OverlayContent.InputTransparent = true;
Content.InputTransparent = false;
}
private void ShowPopup()
{
if (popup != null)
this.Children.Remove(popup);
popup = new RelativeLayout();
popup.GestureRecognizers.Add(new TapGestureRecognizer() {Command = new Command(PopUpTapped)});
popup.BackgroundColor = BackgroundColor;
this.Children.Add(popup,
Constraint.Constant(0),
Constraint.Constant(0),
Constraint.RelativeToParent(layout => layout.Width),
Constraint.RelativeToParent(layout => layout.Height));
popup.Children.Add(OverlayContent,
Constraint.RelativeToParent(layout => layout.X + 20),
Constraint.RelativeToParent(layout => layout.Y + 20),
Constraint.RelativeToParent(layout => layout.Width - 40),
Constraint.RelativeToParent(layout => layout.Height - 40));
OverlayContent.InputTransparent = false;
Content.InputTransparent = true;
}
private void PopUpTapped(object o)
{
this.IsOverlayVisible = !IsOverlayVisible;
}
}

Resources