I have a base XAML View that have a custom control with bindable property that is used to show/hide a Grid control.
I need to change this property from a XAML View that inherit from the base View.
the custom control view
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
using Syncfusion.SfBusyIndicator.XForms;
using System.Runtime.CompilerServices;
namespace TEST_SF
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class VolosLoading : ContentView
{
private static Grid _LoadingContainer = null;
public bool Mostra
{
get { return (bool)GetValue(MostraProperty); }
set { SetValue(MostraProperty, value); OnPropertyChanged(nameof(Mostra)); }
}
public static BindableProperty MostraProperty = BindableProperty.Create(
propertyName: nameof(Mostra),
returnType: typeof(bool),
declaringType: typeof(VolosLoading),
defaultValue: false,
defaultBindingMode: BindingMode.TwoWay
, propertyChanged: MostraPropertyChanged
);
private static void MostraPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
_LoadingContainer.IsEnabled = (bool)newValue;
_LoadingContainer.IsVisible = (bool)newValue;
}
public VolosLoading()
{
InitializeComponent();
_LoadingContainer = (Grid)FindByName("LoadingContainer");
OnPropertyChanged(nameof(Mostra));
}
}
}
its view
<?xml version="1.0" encoding="UTF-8"?>
<ContentView
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:busyindicator="clr-namespace:Syncfusion.SfBusyIndicator.XForms;assembly=Syncfusion.SfBusyIndicator.XForms"
x:Class="TEST_SF.VolosLoading">
<ContentView.Content>
<Grid x:Name="LoadingContainer" IsEnabled="{Binding Mostra}" IsVisible="{Binding Mostra}">
<Grid BackgroundColor="LightGray" Opacity="0.6" />
<busyindicator:SfBusyIndicator x:Name="Loading" AnimationType="DoubleCircle"
ViewBoxWidth="150" ViewBoxHeight="150"
TextColor="Green" BackgroundColor="Transparent"
HorizontalOptions="Center" VerticalOptions="Center" />
</Grid>
</ContentView.Content>
</ContentView>
the base class
namespace TEST_SF.Base
{
public class BaseClass
{
public static VolosLoading PageLoading = new VolosLoading { Mostra = false };
}
}
the base view
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:TEST_SF"
x:Class="TEST_SF.BasePage">
<ContentPage.ControlTemplate>
<ControlTemplate>
<StackLayout>
<Label BackgroundColor="Red" Text="Welcome to Xamarin.Forms!"
VerticalOptions="StartAndExpand"
HorizontalOptions="CenterAndExpand" />
<local:VolosLoading></local:VolosLoading>
<ContentPresenter></ContentPresenter>
</StackLayout>
</ControlTemplate>
</ContentPage.ControlTemplate>
</ContentPage>
Then I have a view that inherit from the base View with a button that calls a command that execute this code:
PageLoading.Mostra = !PageLoading.Mostra;
The class is:
class MainPage_Repo : Base.BaseClass
its view:
<local:BasePage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:TEST_SF"
x:Class="TEST_SF.MainPage">
<ContentPage.BindingContext>
<local:MainPage_Repo />
</ContentPage.BindingContext>
<StackLayout>
<Button VerticalOptions="Center" HorizontalOptions="Center" Text="Loading SF" Command="{Binding MyMockCommand}" CommandParameter="1" />
</StackLayout>
</local:BasePage>
The problems are that the Grid is visible at start, and when the button is pressed nothing changes, the value of Mostra is changed correctly but he Grid is always visible.
How can i solve this?
If you have static properties in ContentView, weird problems are expected to happen, as they are shared across instances, in your case that relates to the _LoadingContainer
Even if this doesn't resolve your problem that is something that can cause huge problems and shouldn't be done.
The problem is when you are calling:
_LoadingContainer = (Grid)FindByName("LoadingContainer");
This line is creating a copy of the Grid container defined in the XAML by x:Name="LoadingContainer". Then, when the Mostra property changes, you are then doing:
_LoadingContainer.IsEnabled = (bool)newValue;
_LoadingContainer.IsVisible = (bool)newValue;
The above is accessing and changing the properties of the copy of the Grid view, not the actual LoadingContainer itself.
Replace all instances of _LoadingContainer with LoadingContainer, and your problem should be solved.
Also, you can remove the bindings to the IsVisible and IsEnabled properties of the Grid. These will cause unnecessary complexity in the compiled code.
EDIT
In addition, in your MostraPropertyChanged handler, you should change it to the following:
private static void MostraPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (VolosLoading)bindable;
if (control != null)
{
control.LoadingContainer.IsEnabled = (bool)newValue;
control.LoadingContainer.IsVisible = (bool)newValue;
}
}
Related
How can I Pass a Binding from a Page to a View?
I have this Page(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:views="clr-namespace:DataBindingTests.Views"
xmlns:model="clr-namespace:DataBindingTests.ViewModels"
x:Class="DataBindingTests.Pages.CoolePage"
Title="CoolePage"
x:DataType="model:CoolesModel">
<VerticalStackLayout>
<Label Text="{Binding YourName}"></Label>
<views:MainView YourName="{Binding YourName}"></views:MainView>
<Button Command="{Binding ChangeNameCommand}"></Button>
</VerticalStackLayout>
</ContentPage>
And its CodeBehind:
using DataBindingTests.ViewModels;
namespace DataBindingTests.Pages;
public partial class CoolePage : ContentPage
{
public CoolePage()
{
this.BindingContext = new CoolesModel();
InitializeComponent();
}
}
If I pass a String into my MainView it works and all events are fired. When I use the binding it doesn't. In this simple test, the app should display two times the same name, but only the Label of the ContentPage has the YourName property printed
<views:MainView YourName="Lars"></views:MainView> <-- Works
<views:MainView YourName="{Binding YourName}"></views:MainView> <-- doesn't work
This is the Xaml of the MainView
<?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"
xmlns:views="clr-namespace:DataBindingTests.Views"
x:Class="DataBindingTests.Views.MainView">
<VerticalStackLayout>
<Label Text="{Binding YourName}"
VerticalOptions="Center"
HorizontalOptions="Center" />
</VerticalStackLayout>
</ContentView>
This is the CodeBehind of the MainView
namespace DataBindingTests.Views;
public partial class MainView : ContentView
{
public String YourName
{
get
{
String value = (String)GetValue(MainView.YourNameProperty);
return value;
}
set
{
SetValue(MainView.YourNameProperty, value);
}
}
public static readonly BindableProperty YourNameProperty = BindableProperty.Create(nameof(YourName)
, typeof(String)
, typeof(MainView), defaultBindingMode:BindingMode.TwoWay, propertyChanged: OnYourNameChanged);
static void OnYourNameChanged(BindableObject bindable, object oldValue, object newValue)
{
Console.WriteLine(newValue);
}
public MainView()
{
this.BindingContext = this; // Ignore ParentContext
InitializeComponent();
}
}
You can just remove code this.BindingContext = this; from the constructor of MainView.xaml.cs:
public MainView()
{
//this.BindingContext = this;
InitializeComponent();
}
Update:
the above code would only work because the Property in the View and
the Page have the same name.
In this condition, you can modify the code of MainView.xaml as follows:
<?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:Class="MauiApp929.MainView"
x:Name="TestControlView"
>
<VerticalStackLayout>
<Label Text="{Binding Source={x:Reference TestControlView}, Path=YourName}"
VerticalOptions="Center"
HorizontalOptions="Center" />
</VerticalStackLayout>
</ContentView>
MainView.xaml.cs
public partial class MainView : ContentView
{
public String YourName
{
get
{
String value = (String)GetValue(YourNameProperty);
return value;
}
set
{
SetValue(YourNameProperty, value);
}
}
public static readonly BindableProperty YourNameProperty = BindableProperty.Create(nameof(YourName)
, typeof(String)
, typeof(MainView), defaultBindingMode: BindingMode.TwoWay, propertyChanged: OnYourNameChanged);
static void OnYourNameChanged(BindableObject bindable, object oldValue, object newValue)
{
Console.WriteLine("-----------------> "+newValue);
}
public MainView()
{
InitializeComponent();
// this.BindingContext = this;
}
}
CoolesModel.cs
public class CoolesModel
{
// public string YourName { get; set; }
public string Name { get; set; }
public string TestName { get; set; }
public ICommand ChangeNameCommand => new Command(changeMethod);
private void changeMethod()
{
}
public CoolesModel() {
//YourName = "abc";
Name = "abc";
TestName = "test123...";
}
}
MainPage.xaml.cs
<?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:mauiapp929="clr-namespace:MauiApp929"
x:Class="MauiApp929.MainPage">
<ScrollView>
<VerticalStackLayout>
<Label Text="{Binding Name}"></Label>
<mauiapp929:MainView YourName="{Binding TestName}"></mauiapp929:MainView>
<Button Command="{Binding ChangeNameCommand}"></Button>
</VerticalStackLayout>
</ScrollView>
</ContentPage>
In this simple test, the app should display two times the same name, but only the Label of the ContentPage has the YourName property printed
You're overwriting your binding context half way through for some reason, and the context your page binding resolves (the normal way of using it, the parent context) is different than what you actually see on the screen (which is your this.BindingContext = this). And you never set your second context's property.
I am trying to create a custom ContentView that can be used throughout my Xamarin.Froms application in de xaml views. The binding on IconColor is not working. I followed the documentation ContentView but it does not seem to work. The Color of the FontImageSource is never set. What am I missing here?
<?xml version="1.0" encoding="UTF-8" ?>
<ContentView
x:Class="Peripass.Mobile.Framework.UIControls.PeripassIcon"
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">
<ContentView.Resources>
</ContentView.Resources>
<ContentView.Content>
<StackLayout HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
<Image>
<Image.Source>
<FontImageSource x:Name="_icon" Glyph="" Color="{Binding IconColor}" Size="20" FontFamily="{OnPlatform Android=PeripassIcon.ttf#}" />
</Image.Source>
</Image>
</StackLayout>
</ContentView.Content>
</ContentView>
using System.Text;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace Peripass.Mobile.Framework.UIControls {
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class PeripassIcon : ContentView {
public static readonly BindableProperty IconSizeProperty = BindableProperty.Create(nameof(IconSize), typeof(double), typeof(PeripassIcon), 26.0);
public static readonly BindableProperty IconColorProperty = BindableProperty.Create(nameof(IconColor), typeof(Color), typeof(PeripassIcon), Color.White);
public static readonly BindableProperty IconNameProperty = BindableProperty.Create(nameof(IconName), typeof(IconType?), typeof(PeripassIcon));
public PeripassIcon() {
InitializeComponent();
}
public IconType IconName {
get => (IconType)GetValue(IconNameProperty);
set => SetValue(IconNameProperty, value);
}
public double IconSize {
get => (double)GetValue(IconSizeProperty);
set => SetValue(IconSizeProperty, value);
}
public Color IconColor {
get => (Color)GetValue(IconColorProperty);
set => SetValue(IconColorProperty, value);
}
protected override void OnPropertyChanged(string propertyName = null) {
base.OnPropertyChanged(propertyName);
if (propertyName == IconNameProperty.PropertyName) {
_icon.Glyph = $"&#x{(int)IconName:X4};";
}
}
}
}
<StackLayout Grid.Row="1" Grid.Column="0">
<uiControls:PeripassIcon IconName="{Binding Model.Icon}" IconColor="#EE4022" IconSize="85.5"/>
</StackLayout>
I needed to set the BindingContext like this:
<?xml version="1.0" encoding="UTF-8" ?>
<ContentView
x:Class="Peripass.Mobile.Framework.UIControls.PeripassIcon"
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:Name="this">
<ContentView.Resources>
</ContentView.Resources>
<ContentView.Content>
<StackLayout BindingContext="{x:Reference this}" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
<Image>
<Image.Source>
<FontImageSource x:Name="_icon" Glyph="" Color="{Binding IconColor}" Size="{Binding IconSize}" FontFamily="{OnPlatform Android=PeripassIcon.ttf#}" />
</Image.Source>
</Image>
</StackLayout>
</ContentView.Content>
</ContentView>
A best practice in MVVM is to keep Views separate from ViewModels, so the best approach is to set BindingContext to the ViewModel (not View via {x:Reference this}).
Make sure that either in Code behind or in Xaml the page binding context is set to your ViewModel, the rest is defined correctly.
I am having trouble with setting the BindableProperty IconName in a custom ContentView in Xamarin.Forms. It is never set and stays empty. The OnPropertyChanged method is executed for every property except the IconName. When I change the type from enum to a string, it works as expected. What am I missing?
The OnPropertyChanged method is needed otherwise non of the properties are set and remain their default value..
<?xml version="1.0" encoding="UTF-8" ?>
<ContentView
x:Class="Peripass.Mobile.Framework.UIControls.PeripassIcon"
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">
<ContentView.Resources>
</ContentView.Resources>
<ContentView.Content>
<StackLayout HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
<Image>
<Image.Source>
<FontImageSource x:Name="_icon" Glyph="" Color="Black" Size="20" FontFamily="{OnPlatform Android=PeripassIcon.ttf#}" />
</Image.Source>
</Image>
</StackLayout>
</ContentView.Content>
</ContentView>
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace Peripass.Mobile.Framework.UIControls
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class PeripassIcon : ContentView
{
public static readonly BindableProperty IconSizeProperty = BindableProperty.Create(nameof(IconSize), typeof(double), typeof(PeripassIcon), 26.0);
public static readonly BindableProperty IconColorProperty = BindableProperty.Create(nameof(IconColor), typeof(Color), typeof(PeripassIcon), Color.White);
public static readonly BindableProperty IconNameProperty = BindableProperty.Create(nameof(IconName), typeof(IconType), typeof(PeripassIcon), IconType.NuclearExplosion);
public PeripassIcon()
{
InitializeComponent();
}
public IconType IconName
{
get => (IconType)GetValue(IconNameProperty);
set => SetValue(IconNameProperty, value);
}
public double IconSize
{
get => (double)GetValue(IconSizeProperty);
set => SetValue(IconSizeProperty, value);
}
public Color IconColor
{
get => (Color)GetValue(IconColorProperty);
set => SetValue(IconColorProperty, value);
}
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == IconNameProperty.PropertyName) {
_icon.Glyph = $"&#x{(int)IconName:X4};";
}
if (propertyName == IconColorProperty.PropertyName) {
_icon.Color = IconColor;
}
if (propertyName == IconSizeProperty.PropertyName) {
_icon.Size = IconSize;
}
}
}
}
<StackLayout Grid.Row="1" Grid.Column="0">
<uiControls:PeripassIcon IconName="NuclearExplosion" IconColor="#EE4022" IconSize="85.5"/>
</StackLayout>
The problem you are experiencing has to do with the fact that OnPropertyChanged is only triggered when a property's value changes (it is not enough for it to be set!).
In your code you set the default value of IconName to IconType.NuclearExplosion. Later on, when you call the control you set
<uiControls:PeripassIcon IconName="NuclearExplosion" IconColor="#EE4022" IconSize="85.5"/>
which sets IconName to the default value, again... so its value does not really changes, and thus OnPropertyChanged is not triggered.
WARNING
That being said, i have to mention that the "normaly" you would use bindings between BindableProperties and the counterparts in the view. For instance, you would bind the value of Color property in FontImageSource in your ContentView to BindableProperty IconColor. For details see the documentation on ContentView.
I'm trying to create a simple Xamarin.Forms custom control and I've encountered on a problem with binding.
This was my initial custom control:
<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="CubisMobile.Controls.TestControl"
x:Name="TestControlView">
<Label Text="{Binding TestText}" />
public partial class TestControl : ContentView
{
public static readonly BindableProperty TestTextProperty = BindableProperty.Create(nameof(TestText), typeof(string), typeof(TestControl));
public string TestText
{
get { return (string)GetValue(TestTextProperty); }
set { SetValue(TestTextProperty, value); }
}
public TestControl()
{
InitializeComponent();
BindingContext = this;
}
}
And I was trying to use it this way:
...
<StackLayout>
<controls:TestControl TestText="{Binding Title}" />
<Label Text="{Binding Title}" />
</StackLayout>
...
I added the second label to test if the Title property works fine, and it does.
But text does not show up on the custom control. When I set a constant value like TestText="Testing" it works as it should. I found this answer on StackOverflow, tried the following, but it also didn't work (custom control 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="CubisMobile.Controls.TestControl"
x:Name="TestControlView">
<Label Text="{Binding Source={x:Reference TestControlView}, Path=TestText}" />
I really don't understand why this binding doesn't work.
The answer you found is the good one, I did the same in my library:
<tabs:TabItem x:Class="Sharpnado.Presentation.Forms.CustomViews.Tabs.UnderlinedTabItem"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:tabs="clr-namespace:Sharpnado.Presentation.Forms.CustomViews.Tabs;assembly=Sharpnado.Presentation.Forms"
x:Name="RootLayout">
<ContentView.Content>
<Grid BackgroundColor="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Label Style="{StaticResource TabTextHeader}"
FontFamily="{Binding Source={x:Reference RootLayout}, Path=FontFamily}"
FontSize="{Binding Source={x:Reference RootLayout}, Path=LabelSize}"
Text="{Binding Source={x:Reference RootLayout}, Path=Label}"
TextColor="{Binding Source={x:Reference RootLayout}, Path=UnselectedLabelColor}">
And the code behind:
public static readonly BindableProperty FontFamilyProperty = BindableProperty.Create(
nameof(FontFamily),
typeof(string),
typeof(TabItem),
null,
BindingMode.OneWay);
public string FontFamily
{
get => (string)GetValue(FontFamilyProperty);
set => SetValue(FontFamilyProperty, value);
}
The only issue I see in the code you shown is the setting of the BindingContext:
public TestControl()
{
InitializeComponent();
BindingContext = this; // Remove this line
}
I have test your code , we need to pay attention to several places:
1. Suppose the class name of ContentView is TestControl, you can try to the following code as you mentioned:
<?xml version="1.0" encoding="UTF-8"?>
<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="CustomeViewApp1.controls.TestControl"
x:Name="TestControlView"
>
<ContentView.Content>
<Label Text="{Binding Source={x:Reference TestControlView}, Path=TestText}" />
</ContentView.Content>
2. remove code BindingContext = this; in TestControl.xaml.cs
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class TestControl : ContentView
{
public static readonly BindableProperty TestTextProperty = BindableProperty.Create(nameof(TestText), typeof(string), typeof(TestControl));
public string TestText
{
get { return (string)GetValue(TestTextProperty); }
set { SetValue(TestTextProperty, value); }
}
public TestControl()
{
InitializeComponent();
//BindingContext = this;
}
}
The test xaml I used is as follows:
<StackLayout Orientation="Horizontal" HorizontalOptions="Center">
<controls:TestControl TestText="{Binding Title}" VerticalOptions="Center"/>
<Label Text="{Binding Type}" FontSize="Medium" TextColor="#F0BB7F"
FontAttributes="Bold" VerticalOptions="Center"/>
</StackLayout>
And you can check the full demo I test here.
The provided answers work fine. However, these require you to manually set the binding source for each property. This can become tedious if a lot of properties need binding.
A simpler approach will be to override the OnChildAdded event exposed by the framework and set the binding context there. This will automatically set the binding context for any child added.
To do this follow these steps:
In the code-behind file add the following method:
protected override void OnChildAdded(Xamarin.Forms.Element child)
{
base.OnChildAdded(child); //must be called for base implementations to be applied
child.BindingContext = this; //this sets the binding context for added children
}
In your xaml bind your controls to the public bindable properties. For example:
I've created a custom view called HeaderTemplate. This control has an image. What am I trying to achieve, is to click on Image and perform some action using MVVM.
Please find below the xml and cs of that control.
HeaderTemplate.xml
<?xml version="1.0" encoding="UTF-8" ?>
<ContentView
x:Class="PS.Views.Templates.HeaderTemplate"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:behaviors="clr-namespace:PS.Behaviors">
<ContentView.Content>
<StackLayout
Padding="10"
BackgroundColor="Transparent"
Orientation="Horizontal">
<Image
x:Name="ImageSource_2"
HeightRequest="50"
HorizontalOptions="EndAndExpand"
Source="{Binding ImageSource2}">
<Image.GestureRecognizers>
<TapGestureRecognizer NumberOfTapsRequired="1" Tapped="ImageSource2_Tapped" />
</Image.GestureRecognizers>
</Image>
</StackLayout>
</ContentView.Content>
</ContentView>
HeaderTemplate.xml.cs
public partial class HeaderTemplate : ContentView
{
public HeaderTemplate()
{
InitializeComponent();
BindingContext = this;
}
public static readonly BindableProperty ImageSource2Property =
BindableProperty.Create(nameof(ImageSource2), typeof(string), typeof(HeaderTemplate));
public string ImageSource2
{
get => (string)GetValue(ImageSource2Property);
set => SetValue(ImageSource2Property, value);
}
public static readonly BindableProperty ImageSource2TapCommandProperty =
BindableProperty.Create(nameof(ImageSource2TapCommand),
typeof(ICommand),
typeof(HeaderTemplate),
null);
public ICommand ImageSource2TapCommand
{
get => (ICommand)GetValue(ImageSource2TapCommandProperty);
set => SetValue(ImageSource2TapCommandProperty, value);
}
private void ImageSource2_Tapped(object sender, EventArgs e)
{
if (ImageSource2TapCommand == null) return;
if (ImageSource2TapCommand.CanExecute(null))
{
ImageSource2TapCommand.Execute(null);
}
}
}
My Page (HolidaysView) has this custom control along with Image click/tap command.
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
x:Class="PS.Views.HolidaysView"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:behaviors="clr-namespace:PS.Behaviors"
xmlns:templates="clr-namespace:PS.Views.Templates"
xmlns:viewModelBase="clr-namespace:PS.ViewModels.Base"
viewModelBase:ViewModelLocator.AutoWireViewModel="true">
<ContentPage.Content>
<StackLayout>
<templates:HeaderTemplate
HeightRequest="60"
ImageSource2="upload.png"
ImageSource2TapCommand="{Binding NavigateToCommand}" />
</StackLayout>
</ContentPage.Content>
</ContentPage>
and binded view model of this page contains the command
public class HolidaysViewModel : ViewModelBase
{
public HolidaysViewModel()
{
}
public ICommand NavigateToCommand => new Command(async () => await NavigateTo());
private async Task NavigateTo()
{
await NavigationService.NavigateToAsync<HolidayRequestViewModel>();
}
}
It's not working. I don't know where I'm wrong.
Am I missing something?
I've researched alots on this but still I'm not able to find any solution uptill now.
Thank you!
To make it work you will need to do just a couple of changes.
First, in the ContenView class file remove the BindingContext = this; line.
Then you will need to add PropertyChanged handlers to both of your BindableProperty
public static readonly BindableProperty ImageSource2Property =
BindableProperty.Create(nameof(ImageSource2),
typeof(string),
typeof(HeaderTemplate),
defaultValue: default(string),
propertyChanged: OnImageSourcePropertyChanged);
public static readonly BindableProperty ImageSource2TapCommandProperty =
BindableProperty.Create(
propertyName: nameof(ImageSource2TapCommand),
returnType: typeof(ICommand),
declaringType: typeof(HeaderTemplate),
defaultValue: default(ICommand),
propertyChanged: OnTapCommandPropertyChanged);
If you can't spot the difference I am talking about these: OnImageSourcePropertyChanged and OnTapCommandPropertyChanged. The other changes in the Create method are not necessary I just added the propertyName so it's more clear.
You need to implement those two methods of course:
static void OnTapCommandPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
if(bindable is HeaderTemplate headerTemplate && newValue is ICommand command)
{
headerTemplate.ImageSource2TapCommand = command;
}
}
static void OnImageSourcePropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is HeaderTemplate headerTemplate && newValue is string imageSource)
{
headerTemplate.ImageSource_2.Source = ImageSource.FromFile(imageSource);
}
}
With these changes you should be able to tap on your Image and navigate as you want.
The reason why?
Since you are Binding values on your "Main Page" to your Custom Control when the latter is first created these values are null, this is why you need to listen to the value changes and this is possible by adding the onPropertyChanged implementation on the Create methods.
There's also a very good explanation here in this post.
Your full class should look something similar to:
public partial class HeaderTemplate : ContentView
{
public HeaderTemplate()
{
InitializeComponent();
}
public static readonly BindableProperty ImageSource2Property =
BindableProperty.Create(nameof(ImageSource2),
typeof(string),
typeof(HeaderTemplate),
defaultValue: default(string),
propertyChanged: OnImageSourcePropertyChanged);
public string ImageSource2
{
get => (string)GetValue(ImageSource2Property);
set => SetValue(ImageSource2Property, value);
}
public static readonly BindableProperty ImageSource2TapCommandProperty =
BindableProperty.Create(
propertyName: nameof(ImageSource2TapCommand),
returnType: typeof(ICommand),
declaringType: typeof(HeaderTemplate),
defaultValue: default(ICommand),
propertyChanged: OnTapCommandPropertyChanged);
public ICommand ImageSource2TapCommand
{
get => (ICommand)GetValue(ImageSource2TapCommandProperty);
set => SetValue(ImageSource2TapCommandProperty, value);
}
private void ImageSource2_Tapped(object sender, EventArgs e)
{
if (ImageSource2TapCommand == null) return;
if (ImageSource2TapCommand.CanExecute(null))
{
ImageSource2TapCommand.Execute(null);
}
}
static void OnTapCommandPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
if(bindable is HeaderTemplate headerTemplate && newValue is ICommand command)
{
headerTemplate.ImageSource2TapCommand = command;
}
}
static void OnImageSourcePropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is HeaderTemplate headerTemplate && newValue is string imageSource)
{
headerTemplate.ImageSource_2.Source = ImageSource.FromFile(imageSource);
}
}
}
Hope this helps.-
Note: Try to follow the example posted in one of the other answers to change your TapEvent with the Command property.
TapGestureRecognizer class has a Command property accepting ICommand
I just tried it in a custom control similar to yours and is working fine.
So, for binding a ICommand to a TapGestureRecognizer, use the Command property, not the Tap event.
<Image
x:Name="ImageSource_2"
HeightRequest="50"
HorizontalOptions="EndAndExpand"
Source="{Binding ImageSource2}">
<Image.GestureRecognizers>
<TapGestureRecognizer
NumberOfTapsRequired="1"
Command="{Binding ImageSource2TapCommand}" />
</Image.GestureRecognizers>
</Image>
Reference Adding a tap gesture recognizer: Using ICommand
Set the Source of the BindingContext for the ICommand to your ContentPage.
MyViewCommand="{{Binding Path=BindingContext.NavigateToCommand, Source={x:Reference MyContentPage}}}"
Example:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage 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="MyApp.Views.PageWithCustomView"
xmlns:templates="clr-namespace:MyApp.Views.Templates"
x:Name="MyContentPage"
>
<ContentPage.Content>
<StackLayout>
<templates:HeaderTemplate
HeightRequest="60"
TemplateImageSource="upload.png"
TemplateTapCommand="{{Binding Path=BindingContext.NavigateToCommand, Source={x:Reference MyContentPage}}}" />
</StackLayout>
</ContentPage.Content>
</ContentPage>