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
Related
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.
<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!
Following this example to create a grouping for CollectionView, I notice that none of the properties are INotifyPropertyChanged, nor is the base class an ObservableCollection.
While the latter is easy to fix by changing List to ObservableCollection:
public class AnimalGroup : ObservableCollection<Animal>
{
public string Name { get; private set; }
public AnimalGroup(string name, ObservableCollection<Animal> animals) : base(animals)
{
Name = name;
}
private string _someOtherPropertyIWantToChangeAtRuntime = "hey";
public string SomeOtherPropertyIWantToChangeAtRuntime { get => _someOtherPropertyIWantToChangeAtRuntime, set => SetProperty(ref _someOtherPropertyIWantToChangeAtRuntime, value); }
}
It isn't clear how to make Name, or any other property (e.g. SomeOtherPropertyIWantToChangeAtRuntime), I want to associate with the group as an INotifyPropertyChanged. Treating it is as a normal class by adding the interface to base causes this warning:
Base interface 'INotifyPropertyChanged' is redundant because AnimalGroup inherits 'ObservableCollection'
Yet, there is nothing for the setter to call, such as SetProperty(ref _name, Value) and the existing PropertyChanged object is just for monitoring a group's collection changes. It isn't invokable, just handleable.
If I ignore the warning and implement INotifyPropertyChanged anyway (and name my event PropChanged to avoid colliding with ObservableCollection.PropertyChanged),
protected bool SetProperty<T>(ref T backingStore, T value, [CallerMemberName]string propertyName = "", Action onChanged = null)
{
if (EqualityComparer<T>.Default.Equals(backingStore, value))
return false;
backingStore = value;
onChanged?.Invoke();
PropChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
return true;
}
public event PropertyChangedEventHandler PropChanged;
and let my ViewModel manage the value of SomeOtherPropertyIWantToChangeAtRuntime, the bound <Label> never sees any changes.
<CollectionView ItemsSource="{Binding AnimalGroups}" HorizontalOptions="FillAndExpand">
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Vertical"/>
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout>
<StackLayout Orientation="Horizontal" HorizontalOptions="FillAndExpand">
<Label
Text="{Binding Name}"
HorizontalOptions="Start"
FontSize="24.44"
TextColor="Black"
FontAttributes="Bold"
Margin="0,0,0,10"/>
<Label
Text="{Binding SomeOtherPropertyIWantToChangeAtRuntime}" FontSize="15"
TextColor="Black"
Margin="0,0,0,0">
<Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding BindingContext.FindGroupAndChangeTextCommand, Source{x:Reference thisPageName}" CommandParameter="{Binding Name}"/>
</Label.GestureRecognizers>
</Label>
...
ViewModel:
public ObservableCollection<AnimalGroup> AnimalGroups {get; private set;}
public ICommand FindGroupAndChangeTextCommand {get; private set;}
public void FindGroupAndChangeText(string name)
{
var group = AnimalGroups.FirstOrDefault(t => t.Name == name);
if (group != null)
group.SomeOtherPropertyIWantToChangeAtRuntime = DateTime.Now.ToString();
}
ViewModel()
{
AnimalGroups = LoadData(); // not shown
FindGroupAndChangeTextCommand = new Command(FindGroupAndChangeText);
}
The result is that the label remains "hey" (which is the default value) and never changes even though I can see that the above command fires and the code finds the group and sets the text.
Agree with Jason, ObservableCollection has inherited INotifyPropertyChanged interface , So you will get the warning
Base interface 'INotifyPropertyChanged' is redundant because AnimalGroup inherits 'ObservableCollection'
And please see following screenshot about ObservableCollection<T>.
If you want to change the item at the runtime like this GIF.
Based on your code. I add two properties in the Animal class. For achieve the change the text of properties at the runtime, we can achieve the INotifyPropertyChanged in Animal class. Here is AnimalGroup.cs
public class AnimalGroup : ObservableCollection<Animal>
{
public string Name { get; private set; }
public AnimalGroup(string name, ObservableCollection<Animal> animals) : base(animals)
{
Name = name;
}
}
public class Animal : INotifyPropertyChanged
{
string animalName;
public string AnimalName
{
set
{
if (animalName != value)
{
animalName = value;
OnPropertyChanged("AnimalName");
}
}
get
{
return animalName;
}
}
string animalArea;
public string AnimalArea
{
set
{
if (animalArea != value)
{
animalArea = value;
OnPropertyChanged("AnimalArea");
}
}
get
{
return animalArea;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
For testing click the command, I achieve the MyAnimalViewModel.cs like following code.
public class MyAnimalViewModel
{
public ObservableCollection<AnimalGroup> AnimalGroups { get; private set; } = new ObservableCollection<AnimalGroup>();
public ICommand FindGroupAndChangeTextCommand { protected set; get; }
public MyAnimalViewModel()
{
ObservableCollection<Animal> ts = new ObservableCollection<Animal>();
ts.Add(new Animal() { AnimalArea = "Asia", AnimalName = "cat" });
ts.Add(new Animal() { AnimalArea = "Asia", AnimalName = "dog" });
ObservableCollection<Animal> ts2 = new ObservableCollection<Animal>();
ts2.Add(new Animal() { AnimalArea = "Eourp", AnimalName = "keep" });
ts2.Add(new Animal() { AnimalArea = "Eourp", AnimalName = "gggg" });
AnimalGroups.Add(new AnimalGroup("Animal1", ts));
AnimalGroups.Add(new AnimalGroup("Animal2", ts2));
FindGroupAndChangeTextCommand = new Command<Animal>((key) =>
{
key.AnimalName = "testggggg";
});
}
}
I notice you want to achieve the group for CollectionView. Here is my edited layout.
<ContentPage.Content>
<CollectionView x:Name="MyCollectionView" ItemsSource="{Binding AnimalGroups}" IsGrouped="True" HorizontalOptions="FillAndExpand">
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Vertical"/>
</CollectionView.ItemsLayout>
<CollectionView.GroupHeaderTemplate>
<DataTemplate>
<Label Text="{Binding Name}"
BackgroundColor="LightGray"
FontSize="Large"
FontAttributes="Bold" >
</Label>
</DataTemplate>
</CollectionView.GroupHeaderTemplate>
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout>
<StackLayout Orientation="Horizontal" HorizontalOptions="FillAndExpand">
<Label
Text="{Binding AnimalArea}"
HorizontalOptions="Start"
FontSize="24.44"
TextColor="Black"
FontAttributes="Bold"
Margin="0,0,0,10"/>
<Label
Text="{Binding AnimalName}" FontSize="15"
TextColor="Black"
Margin="0,0,0,0">
<Label.GestureRecognizers>
<TapGestureRecognizer NumberOfTapsRequired="1"
Command="{ Binding BindingContext.FindGroupAndChangeTextCommand, Source={x:Reference Name=MyCollectionView} }" CommandParameter="{Binding .}"
/>
</Label.GestureRecognizers>
</Label>
</StackLayout>
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ContentPage.Content>
Here is layout background code.
public partial class Page2 : ContentPage
{
public Page2()
{
InitializeComponent();
this.BindingContext = new MyAnimalViewModel();
}
}
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];
}
}
All,
I am binding my Listview to a collection from a Viewmodel. CellView of the ListView includes an image. I would like to invoke a command in my viewmodel when I click the image in the list item.I am trying to avoid event handling in my model. Any idea ?
thanks !
Given below is the xaml and view model.
ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:jList="clr-namespace:JList;assembly=JList"
x:Class="JList.Pages.ItemDetailPage"
Title="Sub Items"
BindingContext="{Binding Source={StaticResource Locator}, Path=ItemDetailViewModel}"
>
<ContentPage.ToolbarItems >
<ToolbarItem Text="Add" Order="Primary" Priority="1" Command="{Binding AddItemCommand}"></ToolbarItem>
<ToolbarItem Text="Edit" Order="Primary" Priority="2" Command="{Binding EditItemCommand}"></ToolbarItem>
</ContentPage.ToolbarItems>
<StackLayout>
<SearchBar Placeholder="Search..." VerticalOptions="Fill" SearchCommand="{Binding SearchCommand}" Text="{Binding SearchString}" ></SearchBar>
<ListView RowHeight="200" ItemsSource="{Binding SubItemsCollection}" BackgroundColor="Gainsboro" SelectedItem="{Binding SubItemSelected, Mode=TwoWay}" x:Name="List" >
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell >
<StackLayout>
<Grid VerticalOptions="Fill" BackgroundColor="White" Padding="5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Image Source="{Binding ImagePath}" Aspect="AspectFit">
<Label Grid.Row="0" Grid.Column="0" Text="{Binding Name}" HorizontalTextAlignment="Center" VerticalTextAlignment="Center" TextColor="Chocolate" Font="Bold,20" />
</Grid>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
<ContentPage.Behaviors>
<jList:CustomBehavior />
</ContentPage.Behaviors>
View Model
namespace JList.Core.ViewModels
{
public class ItemDetailViewModel : ViewModelBase, IViewModel
{
private IItemService _itemService;
private ICommandFactory _cmdFactory;
private INavigationService _navService;
private ItemListViewModel _parent;
private IAppInstanceData _appData;
public ItemDetailViewModel(IItemService itemService, ICommandFactory cmdFactory, INavigationService navService, IAppInstanceData appData, ItemListViewModel parent)
{
_itemService = itemService;
_cmdFactory = cmdFactory;
_navService = navService;
_parent = parent;
ParentItemSelected = _parent.ItemSelected.Id;
_appData = appData;
// FetchSubItemsAsync();
}
public int ParentItemSelected { get; set; }
private string _searchString;
public String SearchString
{
get { return _searchString; }
set
{
if (_searchString != value)
{
_searchString = value;
OnPropertyChanged();
}
}
}
private ObservableCollection<SubItem> _subItemsCollection;
public ObservableCollection<SubItem> SubItemsCollection
{
get { return _subItemsCollection; }
set
{
if (_subItemsCollection != null)
{
if (!_subItemsCollection.SequenceEqual(value))
{
_subItemsCollection = value;
OnPropertyChanged();
}
}
else
{
_subItemsCollection = value;
OnPropertyChanged();
}
}
}
private async void FetchSubItemsAsync()
{
ParentItemSelected = _parent.ItemSelected.Id;
var items = await _itemService.GetAllSubItemsAsync(_parent.ItemSelected.Id);
var coll = new ObservableCollection<SubItem>();
foreach (var it in items)
{
coll.Add(it);
}
SubItemsCollection = coll;
}
public void RefreshAsync()
{
FetchSubItemsAsync();
}
private SubItem _itemSelected;
public SubItem SubItemSelected
{
get => _itemSelected;
set
{
_itemSelected = value;
// _navService.PushView(typeof(EditSubItemViewModel).ToString());
}
}
#region FetchCommand
private ICommand _fetchItemsCommand;
public ICommand FetchItemsCommand
{
get
{
if (_fetchItemsCommand == null)
_fetchItemsCommand = _cmdFactory.CreateCommand(FetchSubItemsAsync, () => true);
return _fetchItemsCommand;
}
}
#endregion
#region AddItemCommand
private ICommand _addItemCommand;
public ICommand AddItemCommand
{
get
{
if (_addItemCommand == null)
_addItemCommand = _cmdFactory.CreateCommand(AddItem, () => true);
return _addItemCommand;
}
}
public void AddItem()
{
_appData.IsEditSubItem = false;
_navService.PushView(typeof(SubItemViewModel).ToString());
}
#endregion
#region EditItemCommand
private ICommand _editItemCommand;
public ICommand EditItemCommand
{
get
{
if (_editItemCommand == null)
_editItemCommand = _cmdFactory.CreateCommand(EditItem, () => true);
return _editItemCommand;
}
}
public void EditItem()
{
_appData.IsEditSubItem = true;
_navService.PushView(typeof(SubItemViewModel).ToString());
}
#endregion
#region SearchCommand
private ICommand _searchCommand;
public ICommand SearchCommand
{
get
{
if (_searchCommand == null)
_searchCommand = _cmdFactory.CreateCommand(SearchItemAsync, () => true);
return _searchCommand;
}
}
private async void SearchItemAsync()
{
var items = await _itemService.GetAllSubItemsAsync(_parent.ItemSelected.Id);
var sstring = SearchString.ToLower();
items = items.Where(i => i.Name.ToLower().Contains(sstring));
var coll = new ObservableCollection<SubItem>();
foreach (var it in items)
{
coll.Add(it);
}
SubItemsCollection = coll;
}
#endregion
}
}
You can add TapGestureRecognizer to the image and bind the command in your ViewModel. Also, you are binding the command inside ViewCell, so you need to set the source of BindingContext.
<ContentPage x:Name="ABCPage">
...
<Image Source="abc">
<Image.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding Path=BindingContext.ImageCommand, Source={x:Reference Name=ABCPage}}"
CommandParameter="{Binding .}" />
</Image.GestureRecognizers>
</Image>
...
</ContentPage>