I’m trying to make a custom drop-down in xamarin forms with some bindable properties. I’m having a label and a list view below the label using relative layout so that listview will always below the label and its IsVisible property will be toggled.
I have created a custom view as below:
Dropdown.xaml
<?xml version="1.0" encoding="UTF-8"?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="CustomViewXam.CustomViews.Dropdown">
<StackLayout>
<RelativeLayout>
<Label x:Name="selectedLabel" TextColor="Red" Text="xcx"
BackgroundColor="Silver" FontSize="15"
HeightRequest="50"
RelativeLayout.WidthConstraint =
"{ConstraintExpression
Type=RelativeToParent,
Property=Width,
Factor=0.5,
Constant=0}"/>
<ListView x:Name="listView" BackgroundColor="Black"
RelativeLayout.WidthConstraint =
"{ConstraintExpression
Type=RelativeToView,
ElementName=selectedLabel,
Property=Width,
Factor=1,
Constant=0}"
RelativeLayout.YConstraint=
"{ConstraintExpression
Type=RelativeToView,
ElementName=selectedLabel,
Property=Height,
Factor=1,
Constant=0}"
>
</ListView>
</RelativeLayout>
</StackLayout>
</ContentView>
Dropdown.xaml.cs
public partial class Dropdown : ContentView
{
public Dropdown()
{
InitializeComponent();
BindingContext = this;
}
public string TitleText
{
get { return base.GetValue(TitleTextProperty).ToString(); }
set { base.SetValue(TitleTextProperty, value); }
}
private static BindableProperty TitleTextProperty = BindableProperty.Create(
propertyName: "TitleText",
returnType: typeof(string),
declaringType: typeof(string),
defaultValue: "",
defaultBindingMode: BindingMode.TwoWay,
propertyChanged: TitleTextPropertyChanged);
private static void TitleTextPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (Dropdown)bindable;
control.selectedLabel.Text = newValue.ToString();
}
public static readonly BindableProperty ItemsSourceProperty =
BindableProperty.Create<Dropdown, IEnumerable<object>>(p => p.ItemsSource,
null, BindingMode.OneWay, null, (bindable, oldValue, newValue) => { ((Dropdown)bindable).LoadItems(newValue); });
public IEnumerable<object> ItemsSource
{
get { return (IEnumerable<object>)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public void LoadItems(IEnumerable<object> tiles)
{
try
{
var list = tiles;
}
catch (Exception e)
{ // can throw exceptions if binding upon disposal
}
}
}
And I'm using this custom view as below in my xaml page
<local:Dropdown TitleText="dssdasd" ItemsSource="{Binding TitleList}" />
Where TitleList is an ObservableCollection in ViewModel
private ObservableCollection<string> _titleList;
public ObservableCollection<string> TitleList
{
get
{
return _titleList;
}
set
{
if (_titleList!= value)
{
_titleList= value;
NotifyPropertyChanged("TitleList");
}
}
}
Problem:
The Text is visible on the UI and text is coming properly, but the listview below is empty and data is not coming. LoadItems method in Custom dropdown is not getting called even TitleList list is updated. Can anyone please guide me whats wrong i'm doing in the above code.
Note: I'm having BindingContext set to my view model in my view.
Note: I'm having BindingContext set to my view model in my view.
Do you mean you use the code in your Dropdown's construction method to set it?
public Dropdown()
{
InitializeComponent();
// Try to remove the code below
BindingContext = this;
}
I recommend you to set the BindingContext in your page xaml and remove this code in the view:
public MainPage()
{
InitializeComponent();
MyViewModel viewModel = new MyViewModel();
BindingContext = viewModel;
}
In this way the LoadItems() will fire at the first time we initialize the Dropdown's ItemsSource. But when you want to update the list, LoadItems() will not be called. because the instance of the list hasn't been changed. You can try to modify the method like:
public void LoadItems(IEnumerable<object> tiles)
{
try
{
var list = tiles;
listView.ItemsSource = tiles;
}
catch (Exception e)
{ // can throw exceptions if binding upon disposal
}
}
Moreover if you want to see the listView's data on the view, you should set its ItemTemplate for instance:
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Grid>
<Label Text="{Binding}"/>
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
Related
I need to react to the content of an ObservableCollection's property changing. I define the property like this:
public static readonly BindableProperty MyCollectionProperty = BindableProperty.Create(
nameof(MyContentView.MyCollection),
typeof(ObservableCollection<object>),
typeof(MyContentView),
propertyChanged: OnMyCollectionPropertyChanged);
public ObservableCollection<object> MyCollection
{
get => (ObservableCollection<object>)GetValue(MyContentView.MyCollectionProperty);
set => SetValue(MyContentView.MyCollectionProperty, value);
}
private static void OnMyCollectionPropertyChanged(BindableObject bindable, object oldvalue, object newvalue)
{
// Do Something
}
The method OnMyCollectionPropertyChanged is called when the whole bound collection object changes (i.e. a completely new collection) but of course not when the collection's content changes.
What is the correct way to react to the events of the INotifyCollectionChanged interface implemented by ObservableCollection? Do I need to manually sign up for them when the collection object is assigned or is there a syntax of BindableProperty.Create() which allows to do so directly, something similar to the propertyChanged method?
I make a code sample of Custom BindableProperty for Type ObservableCollection for your reference.
ContentView:
<ContentView.Content>
<StackLayout>
<ListView x:Name="listview1" ItemsSource="{Binding Items}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout>
<Label Text="{Binding Name}" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentView.Content>
public partial class View1 : ContentView
{
public ObservableCollection<object> Items
{
get { return (ObservableCollection<object>)GetValue(ItemsProperty); }
set
{
SetValue(ItemsProperty, value);
}
}
public static BindableProperty ItemsProperty = BindableProperty.Create("ItemsSource",
typeof(ObservableCollection<object>),
typeof(View1),
null,
BindingMode.TwoWay,
propertyChanged: (bindable, oldValue, newValue) => OnItemsSourceChanged(bindable, oldValue, newValue));
private static void OnItemsSourceChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (View1)bindable;
control.listview1.ItemsSource = (ObservableCollection<object>)newValue;
}
public View1()
{
InitializeComponent();
}
}
Useage:
Xaml:
<ContentPage.Content>
<local:View1 Items="{Binding models}" />
</ContentPage.Content>
Code behind:
public partial class Page17 : ContentPage
{
public ObservableCollection<object> models { get; set; }
public Page17()
{
InitializeComponent();
models = new ObservableCollection<object>()
{
new modela(){Name="A" },
new modela(){Name="B" }
};
this.BindingContext = this;
}
}
public class modela
{
public string Name { get; set; }
}
I have a collectionview that is bound to an ObservableRangeCollectionin my ViewModel.
In my ViewModel there is a Method that runs onAppearing and I want my ColletionViewto be filled from there, but when I do so the collectionveiw dose not display the content only when i reload the content is shown.
View:
<RefreshView Grid.Row="1"
Grid.RowSpan="2"
Command="{Binding RefreshCommand}"
IsRefreshing="{Binding IsBusy, Mode=OneWay}">
<RefreshView.RefreshColor>
<OnPlatform x:TypeArguments="Color">
<On Platform="iOS" Value="White"/>
</OnPlatform>
</RefreshView.RefreshColor>
<CollectionView x:Name="Collection"
ItemsSource="{Binding Locations, Mode=OneWay}"
ItemTemplate="{StaticResource ListDataTemplate}"
RemainingItemsThresholdReachedCommand="{Binding LoadMoreCommand}"
RemainingItemsThreshold="10"
SelectionMode="Single"
BackgroundColor="Transparent"
ItemsLayout="VerticalList"
SelectedItem="{Binding SelectedItem}"
SelectionChangedCommand="{Binding SelectedCommand}">
<CollectionView.EmptyView>
<StackLayout Padding="12">
<Label HorizontalOptions="Center" Text="Keine Daten vorhanden!" TextColor="White"/>
</StackLayout>
</CollectionView.EmptyView>
</CollectionView>
</RefreshView>
ViewModel:
namespace YourPartys.ViewModels
{
public class ListViewModel : ViewModelBase
{
#region Variables
#endregion
#region Propertys
LocationModel selectedItem;
public LocationModel SelectedItem
{
get => selectedItem;
set => SetProperty(ref selectedItem, value);
}
public ObservableRangeCollection<LocationModel> Locations { get;set; } = new ObservableRangeCollection<LocationModel>();
double distance;
public double Distance
{
get => distance;
set => SetProperty(ref distance, value);
}
#endregion
#region Commands
public ICommand FilterButtonCommand { get; }
public ICommand RefreshCommand { get; }
public ICommand SelectedCommand { get; }
public ICommand LoadMoreCommand { get; }
#endregion
//Constructor
public ListViewModel()
{
FilterButtonCommand = new Command(OpenFilter);
RefreshCommand = new AsyncCommand(Refresh);
SelectedCommand = new AsyncCommand(Select);
}
public override async void VModelActive(Page sender, EventArgs eventArgs)
{
base.VModelActive(sender, eventArgs);
var locs = await FirestoreService.GetLocations("Locations");
Locations.AddRange(locs);
}
private void OpenFilter(object obj)
{
PopupNavigation.Instance.PushAsync(new ListFilterPage());
}
private async Task Refresh()
{
IsBusy = true;
var locs = await FirestoreService.GetLocations("Locations");
Locations.AddRange(locs);
IsBusy = false;
}
private async Task Select()
{
if (SelectedItem == null)
return;
var route = $"{nameof(DetailPage)}?Locationid={SelectedItem.Locationid}";
SelectedItem = null;
await AppShell.Current.GoToAsync(route);
}
}
}
There are several problems in your demo.
1.Since you set the BindingContext for your page in xaml as follows:
<ContentPage.BindingContext>
<viewmodels:MainViewModel/>
</ContentPage.BindingContext>
you didn't need to recreate another object MainViewModel in a CS file and reference it. These are two different objects.
MainViewModel viewModel;
viewModel = new MainViewModel();
protected override void OnAppearing()
{
base.OnAppearing();
viewModel.VModelActive(this, EventArgs.Empty);
}
So, you can get the BindingContext in MainPage.xaml.cs in function OnAppearing as follows:
protected override void OnAppearing()
{
base.OnAppearing();
viewModel = (MainViewModel)this.BindingContext;
viewModel.VModelActive(this, EventArgs.Empty);
}
The whole code is
public partial class MainPage : ContentPage
{
MainViewModel viewModel;
public MainPage()
{
InitializeComponent();
// viewModel = new MainViewModel();
}
protected override void OnAppearing()
{
base.OnAppearing();
viewModel = (MainViewModel)this.BindingContext;
viewModel.VModelActive(this, EventArgs.Empty);
}
}
2.when we set the text color of the Label to White,this makes it hard to see the text,so you can reset it to another color,for example Black:
<Label Text="{Binding Name}"
FontSize="30"
TextColor="White"/>
I am confused with how Xamarin binding works.
OneWay
Indicates that the binding should only propagate changes from source
(usually the View Model) to target (the BindableObject). This is the
default mode for most BindableProperty values.
So by default, if the values are set in the view model it will be reflected in the xaml pages.
But in the Xamarin default template, below is the code to insert a new item. Page doesn't have any two way binding mode set in the markup.
<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="Christianity.Mobile.Views.NewItemPage"
Title="New Item">
<ContentPage.ToolbarItems>
<ToolbarItem Text="Cancel" Clicked="Cancel_Clicked" />
<ToolbarItem Text="Save" Clicked="Save_Clicked" />
</ContentPage.ToolbarItems>
<ContentPage.Content>
<StackLayout Spacing="20" Padding="15">
<Label Text="Text" FontSize="Medium" />
<Entry Text="{Binding Item.Text}" d:Text="Item name" FontSize="Small" />
<Label Text="Description" FontSize="Medium" />
<Editor Text="{Binding Item.Description}" d:Text="Item description" FontSize="Small" Margin="0" />
</StackLayout>
</ContentPage.Content>
</ContentPage>
Here I can see that default values of item are populated when a new page is created and also, edited name and description are available while saving the item.
My question - is two way binding implemented by default without having any binding mode set?
public partial class NewItemPage : ContentPage
{
public Item Item { get; set; }
public NewItemPage()
{
InitializeComponent();
Item = new Item
{
Text = "Item name",
Description = "This is an item description."
};
BindingContext = this;
}
async void Save_Clicked(object sender, EventArgs e)
{
MessagingCenter.Send(this, "AddItem", Item);
await Navigation.PopModalAsync();
}
async void Cancel_Clicked(object sender, EventArgs e)
{
await Navigation.PopModalAsync();
}
}
UPDATE
Here is my code to load data asynchronously
<?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="Christianity.Mobile.Views.ItemDetailPage"
Title="{Binding Title}">
<StackLayout Spacing="20" Padding="15">
<Label Text="Text:" FontSize="Medium" />
<Label Text="{Binding Item.Title}" d:Text="Item title" FontSize="Small"/>
<Label Text="Description:" FontSize="Medium" />
<Label Text="{Binding Item.Description}" d:Text="Item description" FontSize="Small"/>
</StackLayout>
</ContentPage>
public class ItemDetailViewModel : BaseViewModel
{
public ItemListItemDTO SelectedItem { get; set; }
public ItemDTO Item { get; set; }
public ICommand LoadItemCommand;
public ItemDetailViewModel(IPageService pageService, ItemListItemDTO selectedItem)
{
SelectedItem = selectedItem;
LoadItemCommand = new Command(async () => await LoadItem());
}
public async Task LoadItem()
{
IsBusy = true;
try
{
// Both are not working
Item = await ItemsDataStore.GetItemAsync(SelectedItem.Id);
//await Device.InvokeOnMainThreadAsync(async () =>
//{
// Item = await ItemsDataStore.GetItemAsync(SelectedItem.Id);
//});
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
finally
{
IsBusy = false;
}
}
}
According to Xamarin.Forms Binding Mode, you can see that most bindable properties have a default binding mode of OneWay but the following properties have a default binding mode of TwoWay:
Date property of DatePicker
Text property of Editor, Entry, SearchBar, and EntryCell
IsRefreshing property of ListView
SelectedItem property of MultiPage
SelectedIndex and SelectedItem properties of Picker
Value property of Slider and Stepper
IsToggled property of Switch
On property of SwitchCell
Time property of TimePicker
These particular properties are defined as TwoWay for a very good reason:
When data bindings are used with the Model-View-ViewModel (MVVM) application architecture, the ViewModel class is the data-binding source, and the View, which consists of views such as Slider, are data-binding targets. MVVM bindings resemble the Reverse Binding sample more than the bindings in the previous samples. It is very likely that you want each view on the page to be initialized with the value of the corresponding property in the ViewModel, but changes in the view should also affect the ViewModel property.
The properties with default binding modes of TwoWay are those properties most likely to be used in MVVM scenarios.
Update:
For example, you get data using Web Api, then loading into ItemDTO Item, please comfirm that you have implement INotifyPropertyChanged interface for ItemDTO class to notify data changed.
public class ItemDTO:ViewModelBase
{
private string _Text;
public string Text
{
get { return _Text; }
set
{
_Text = value;
RaisePropertyChanged("Text");
}
}
private string _Description;
public string Description
{
get
{ return _Description; }
set
{
_Description = value;
RaisePropertyChanged("Description");
}
}
}
The ViewModelBase is the class that implement INotifyPropertyChanged.
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
I want to bind a CustomLabel to a VM by creating a new bindable property.
In OneWay mode, the first VM data has properly changed the property of the CustomLabel. but It didn't work from second time.
Although The VM event has occur, the Bindable Property of CustomView has not fired its PropertyChanged event.
It works properly in TwoWay mode though.
I've been testing for two days and searching for the cause, but I coudn't find it well.
Anybody tell me how to do?
// HomeViewModel.cs
public class HomeViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _customName = "-";
public string CustomName
{
get
{
Debug.WriteLine("Get_CustomName");
return _customName;
}
set
{
if (value != _customName)
{
Debug.WriteLine("Set_CustomName");
_customName = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CustomName)));
}
}
}
}
// MainPage.cs
public partial class MainPage : ContentPage
{
HomeViewModel Vm = new HomeViewModel();
public MainPage()
{
InitializeComponent();
BindingContext = Vm;
}
void ButtonTrue_Clicked(object sender, EventArgs e)
{
Vm.CustomName = "True";
}
void ButtonFalse_Clicked(object sender, EventArgs e)
{
Vm.CustomName = "False";
}
}
<!-- MainPage.xaml -->
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Ex_Binding"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="Ex_Binding.MainPage">
<StackLayout Padding="50,0" VerticalOptions="Center">
<StackLayout Orientation="Horizontal" HorizontalOptions="Center">
<Label Text="Custom Result : " />
<local:CustomLabel x:Name="lbCustom" MyText="{Binding CustomName}" HorizontalOptions="Center" />
</StackLayout>
<StackLayout Orientation="Horizontal">
<Button Text="TRUE" BackgroundColor="LightBlue" HorizontalOptions="FillAndExpand" Clicked="ButtonTrue_Clicked" />
<Button Text="FALSE" BackgroundColor="LightPink" HorizontalOptions="FillAndExpand" Clicked="ButtonFalse_Clicked" />
</StackLayout>
</StackLayout>
</ContentPage>
// CustomLabel.cs
public class CustomLabel : Label
{
public static readonly BindableProperty MyTextProperty = BindableProperty.Create(nameof(MyText), typeof(string), typeof(CustomLabel), null, BindingMode.OneWay, propertyChanged: OnMyTextChanged);
private static void OnMyTextChanged(BindableObject bindable, object oldValue, object newValue)
{
var thisBindable = (CustomLabel)bindable;
if (thisBindable != null)
{
thisBindable.MyText = (string)newValue;
}
}
public string MyText
{
get => (string)GetValue(MyTextProperty);
set
{
SetValue(MyTextProperty, value);
Text = value;
}
}
}
Cause :
thisBindable.MyText = (string)newValue;
Because you set the value of MyText when its value changed . So it will never been invoked next time (in TwoWay the method will been invoked multi times).
Solution:
You should set the Text in OnMyTextChanged directly .
private static void OnMyTextChanged(BindableObject bindable, object oldValue, object newValue)
{
var thisBindable = (CustomLabel)bindable;
if (thisBindable != null)
{
thisBindable.Text = (string)newValue;
}
}
public string MyText
{
get => (string)GetValue(MyTextProperty);
set
{
SetValue(MyTextProperty, value);
//Text = value;
}
}
I have this template:
<?xml version="1.0" encoding="utf-8"?>
<Grid Padding="20,0" xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Japanese;assembly=Japanese"
x:Class="Japanese.Templates.DataGridTemplate"
x:Name="this" HeightRequest="49" Margin="0">
<Grid.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding TapCommand, Source={x:Reference this}}"
CommandParameter="1"
NumberOfTapsRequired="1" />
</Grid.GestureRecognizers>
<Label Grid.Column="0" Text="{Binding Test" />
</Grid>
Behind this I have:
public partial class DataGridTemplate : Grid
{
public DataGridTemplate()
{
InitializeComponent();
}
public static readonly BindableProperty TapCommandProperty =
BindableProperty.Create(
"Command",
typeof(ICommand),
typeof(DataGridTemplate),
null);
public ICommand TapCommand
{
get { return (ICommand)GetValue(TapCommandProperty); }
set { SetValue(TapCommandProperty, value); }
}
}
and I am trying to call the template like this in file: Settings.xaml.cs
<template:DataGridTemplate TapCommand="openCFSPage" />
hoping that it will call my method here in file: Settings.cs
void openCFSPage(object sender, EventArgs e)
{
Navigation.PushAsync(new CFSPage());
}
The code compiles but when I click on the grid it doesn't call the openCFSPage method.
1) Does anyone have an idea what might be wrong?
2) Also is there a way that I can add a parameter to the template and then have that parameter passed to my method in the CS back end code?
Note that I would like to avoid adding a view model if possible. The application is small and I'd like to just have the code I need in the CS code of the page that calls the template.
Please note that the simplest way to implement this would be through MVVM (i.e. a view-model), but if you want to side-step this option (as you mentioned in the question) then you can use one of the following options
Option1 : Wrap delegate into command object
If you look at it from the perspective of a XAML parser, you are technically trying to assign a delegate to a property of type ICommand. One way to avoid the type mismatch would be to wrap the delegate inside a command-property in the page's code-behind.
Code-behind [Settings.xaml.cs]
ICommand _openCFSPageCmd;
public ICommand OpenCFSPageCommand {
get {
return _openCFSPageCmd ?? (_openCFSPageCmd = new Command(OpenCFSPage));
}
}
void OpenCFSPage(object param)
{
Console.WriteLine($"Control was tapped with parameter: {param}");
}
XAML [Settings.xaml]
<!-- assuming that you have added x:Name="_parent" in root tag -->
<local:DataGridView TapCommand="{Binding OpenCFSPageCommand, Source={x:Reference _parent}}" />
Option2 : Custom markup-extension
Another option (a bit less mainstream) is to create a markup-extension that wraps the delegate into a command object.
[ContentProperty("Handler")]
public class ToCommandExtension : IMarkupExtension
{
public string Handler { get; set; }
public object Source { get; set; }
public object ProvideValue(IServiceProvider serviceProvider)
{
if (serviceProvider == null)
throw new ArgumentNullException(nameof(serviceProvider));
var lineInfo = (serviceProvider?.GetService(typeof(IXmlLineInfoProvider)) as IXmlLineInfoProvider)?.XmlLineInfo ?? new XmlLineInfo();
object rootObj = Source;
if (rootObj == null)
{
var rootProvider = serviceProvider.GetService<IRootObjectProvider>();
if (rootProvider != null)
rootObj = rootProvider.RootObject;
}
if(rootObj == null)
{
var valueProvider = serviceProvider.GetService<IProvideValueTarget>();
if (valueProvider == null)
throw new ArgumentException("serviceProvider does not provide an IProvideValueTarget");
//we assume valueProvider also implements IProvideParentValues
var propInfo = valueProvider.GetType()
.GetProperty("Xamarin.Forms.Xaml.IProvideParentValues.ParentObjects",
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if(propInfo == null)
throw new ArgumentException("valueProvider does not provide an ParentObjects");
var parentObjects = propInfo.GetValue(valueProvider) as IEnumerable<object>;
rootObj = parentObjects?.LastOrDefault();
}
if(rootObj != null)
{
var delegateInfo = rootObj.GetType().GetMethod(Handler,
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if(delegateInfo != null)
{
var handler = Delegate.CreateDelegate(typeof(Action<object>), rootObj, delegateInfo) as Action<object>;
return new Command((param) => handler(param));
}
}
throw new XamlParseException($"Can not find the delegate referenced by `{Handler}` on `{Source?.GetType()}`", lineInfo);
}
}
Sample usage
<local:DataGridView TapCommand="{local:ToCommand OpenCFSPage}" />
You have 2 options depending on the the use case :
FYI, there's no way to call another method directly from the view (its a bad design pattern to do so)
Using Event Aggregator :
Create interface
public interface IEventAggregator
{
TEventType GetEvent<TEventType>() where TEventType : EventBase, new();
}
All you have to do is call it from you TapCommand
_eventAggregator.GetEvent<ItemSelectedEvent>().Publish(_selectedItem);
Then in your Settings.cs you can Create a method that can receive the data
this.DataContext = new ListViewModel(ApplicationService.Instance.EventAggregator);
Inheritance and Polymorphism / Making openCFSPage a service :
Creating a interface / service that links both models
public interface IOpenCFSPage
{
Task OpenPage();
}
and a method :
public class OpenCFSPage : IOpenCFSPage
{
private INavigationService _navigationService;
public OpenCFSPage(INavigationService navigationService){
_navigationService = navigationService;
}
public async Task OpenPage()
{
await _navigationService.NavigateAsync(new CFSPage());
}
}
Settings.xaml:
<template:DataGridTemplate TapCommand="{Binding OpenCFSPage}" />
<!-- Uncomment below and corresponding parameter property code in DataGridTemplate.xaml.cs to pass parameter from Settings.xaml -->
<!--<template:DataGridTemplate TapCommand="{Binding OpenCFSPage}" CommandParameter="A" />-->
Settings.xaml.cs:
public Settings()
{
InitializeComponent();
OpenCFSPage = new Command(p => OpenCFSPageExecute(p));
BindingContext = this;
}
public ICommand OpenCFSPage { get; private set; }
void OpenCFSPageExecute(object p)
{
var s = p as string;
Debug.WriteLine($"OpenCFSPage:{s}:");
}
DataGridTemplate.xaml:
<?xml version="1.0" encoding="UTF-8"?>
<Grid xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Japanese;assembly=Japanese"
Padding="0,20"
HeightRequest="49" Margin="0"
x:Class="Japanese.DataGridTemplate">
<Grid.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding TapCommand}"
CommandParameter="1"
NumberOfTapsRequired="1" />
</Grid.GestureRecognizers>
<Label Grid.Column="0" Text="Test" />
</Grid>
DataGridTemplate.xaml.cs:
public partial class DataGridTemplate : Grid
{
public DataGridTemplate()
{
InitializeComponent();
}
public static readonly BindableProperty TapCommandProperty =
BindableProperty.Create(
nameof(TapCommand), typeof(ICommand), typeof(DataGridTemplate), null,
propertyChanged: OnCommandPropertyChanged);
public ICommand TapCommand
{
get { return (ICommand)GetValue(TapCommandProperty); }
set { SetValue(TapCommandProperty, value); }
}
//public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create(
// nameof(CommandParameter), typeof(string), typeof(DataGridTemplate), null);
//public string CommandParameter
//{
// get { return (string)GetValue(CommandParameterProperty); }
// set { SetValue(CommandParameterProperty, value); }
//}
static TapGestureRecognizer GetTapGestureRecognizer(DataGridTemplate view)
{
var enumerator = view.GestureRecognizers.GetEnumerator();
while (enumerator.MoveNext())
{
var item = enumerator.Current;
if (item is TapGestureRecognizer) return item as TapGestureRecognizer;
}
return null;
}
static void OnCommandPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is DataGridTemplate view)
{
var tapGestureRecognizer = GetTapGestureRecognizer(view);
if (tapGestureRecognizer != null)
{
tapGestureRecognizer.Command = (ICommand)view.GetValue(TapCommandProperty);
//tapGestureRecognizer.CommandParameter = (string)view.GetValue(CommandParameterProperty);
}
}
}
}
Check this code you help you. Here you have to pass a reference of list view and also you need to bind a command with BindingContext.
<ListView ItemsSource="{Binding Sites}" x:Name="lstSale">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Vertical">
<Label Text="{Binding FriendlyName}" />
<Button Text="{Binding Name}"
HorizontalOptions="Center"
VerticalOptions="Center"
Command="{Binding
Path=BindingContext.RoomClickCommand,
Source={x:Reference lstSale}}"
CommandParameter="{Binding .}" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>