What is the correct way to use RelativeSource in the outer tag? - xaml

I want to avoid repeating Source={RelativeSource AncestorType={x:Type vm:MainViewModel}} in the following.
<SwipeView ... xmlns:vm="clr-namespace:Todo.ViewModel">
<SwipeView.LeftItems>
<SwipeItems>
<SwipeItem Text="Delete"
Command="{Binding DeleteCommand,Source={RelativeSource AncestorType={x:Type vm:MainViewModel}}}" />
</SwipeItems>
</SwipeView.LeftItems>
<Grid Padding="0,5">
<Frame >
<Frame.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding TapCommand,Source={RelativeSource AncestorType={x:Type vm:MainViewModel}}}"/>
</Frame.GestureRecognizers>
</Frame>
</Grid>
</SwipeView>
I do the following but it does not work as expected.
<SwipeView ... xmlns:vm="clr-namespace:Todo.ViewModel"
BindingContext="{Binding Source={RelativeSource AncestorType={x:Type vm:MainViewModel}}}"
>
<SwipeView.LeftItems>
<SwipeItems>
<SwipeItem Text="Delete" Command="{Binding DeleteCommand}" />
</SwipeItems>
</SwipeView.LeftItems>
<Grid Padding="0,5">
<Frame >
<Frame.GestureRecognizers>
<TapGestureRecognizer Command="{Binding TapCommand}" />
</Frame.GestureRecognizers>
</Frame>
</Grid>
</SwipeView>
Repo
Use the following repo to avoid getting inconsistent results (among us) and to make sure we are talking in the same scope.
https://github.com/pstricks-fans/Todo
Here are the relevant parts:
MySwipeView:
public partial class MySwipeView : SwipeView
{
public MySwipeView()
{
InitializeComponent();
}
}
<SwipeView ...
x:Class="Todo.CustomControls.MySwipeView"
xmlns:vm="clr-namespace:Todo.ViewModel"
>
<SwipeView.LeftItems>
<SwipeItems>
<SwipeItem
Text="Delete"
Command="{Binding DeleteCommand,Source={RelativeSource AncestorType={x:Type vm:MainViewModel}}}"
CommandParameter="{Binding .}"/>
</SwipeItems>
</SwipeView.LeftItems>
<Grid Padding="0,5">
<Frame >
<Frame.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding TapCommand,Source={RelativeSource AncestorType={x:Type vm:MainViewModel}}}"
CommandParameter="{Binding .}"/>
</Frame.GestureRecognizers>
<Label Text="{Binding .}" FontSize="24"/>
</Frame>
</Grid>
</SwipeView>
MainPage:
public partial class MainPage : ContentPage
{
public MainPage(MainViewModel vm)
{
InitializeComponent();
BindingContext = vm;
}
}
<ContentPage ...
xmlns:local="clr-namespace:Todo.CustomControls"
xmlns:vm="using:Todo.ViewModel"
x:DataType="vm:MainViewModel"
>
<Grid ... >
<CollectionView ... >
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="{x:Type x:String}">
<local:MySwipeView />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Grid>
</ContentPage>
MainViewModel:
public partial class MainViewModel : ObservableObject
{
[RelayCommand]
void Delete(string s){}
[RelayCommand]
async Task Tap(string s){}
}

In your case, SwipeView is being used inside an ItemTemplate. You MUST NOT change its BindingContext; that has to be the associated Item.
Therefore, your original goal is NOT POSSIBLE; we can simplify the "Source" expression, but we cannot eliminate it.
Simplest I know is:
Command="{Binding VM.DeleteCommand, Source={x:Reference thePage}}"
Explanation: On "thePage", finds property "VM", which contains a property "DeleteCommand". Make the following changes to MainPage.
<ContentPage
...
x:Name="thePage"
x:Class="MainPage">
class MainPage : ContentPage
{
// You can change this name. Be sure to use same name in XAML above.
public property MainViewModel VM { get; set; }
public MainPage(MainViewModel vm)
{
InitializeComponent();
// Put this line BEFORE set BindingContext. Used by XAML.
VM = vm;
BindingContext = vm;
}

At first, the format of binding to an ancestor in the official document is something like {Binding Source={RelativeSource AncestorType={x:Type local:PeopleViewModel}}, Path=DeleteEmployeeCommand}
And then you can try to set the SwipeView's binding context in the construction method instead of the binding way you used.
I have done a sample to test, and the binding worked well:
The MySwipeView.cs :
public partial class MySwipeView : SwipeView
{
      public ICommand TestCommand { get; private set; }
      public MySwipeView()
      {
            InitializeComponent();
            TestCommand = new Command<string>(Test);
//BindingContext = this;
            BindingContext = new MyViewModel();
      }
      void Test(string print)
      {
            Debug.WriteLine("============"+print);
      }
}
The MySwipeView.xaml:
<SwipeView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MauiAppTest.MySwipeView"
>
<SwipeView.LeftItems>
<SwipeItems>
<SwipeItem
Text="Delete"
Command="{Binding DeleteCommand}"
CommandParameter="xxxxxxxxx"/>
</SwipeItems>
</SwipeView.LeftItems>
<Grid Padding="0,5">
<Frame >
<Frame.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding TapCommand}"
CommandParameter="xxxxxxxx"/>
</Frame.GestureRecognizers>
<Label Text="xxxxxxxx" FontSize="24"/>
</Frame>
</Grid>
</SwipeView>
The MainPage.xaml:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MauiApp21"
x:Class="MauiApp21.MainPage">
<HorizontalStackLayout>
<local:MySwipeView/>
</HorizontalStackLayout>
</ContentPage>
The MyViewModel.cs:
public partial class MyViewModel : ObservableObject
{
[RelayCommand]
void Delete(string value) { Debug.WriteLine("===========Delete"); }
[RelayCommand]
void Tap(string value) { Debug.WriteLine("===============Tap"); }
}
No matter the BindingContext is this or the MyViewModel, the command will run successfully.

Related

RelativeSource in MAUI control not bound

I'm going through simple example explained in video:
https://youtu.be/5Qga2pniN78?t=961
At 16. minute (timestamp in link above), he implements the Delete Command on SwipeItem.
In my local project, everything worked so far, but Delete Command is never triggered. I checked source generators, DeleteCommand exists.
My 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"
x:Class="MauiApp1.MainPage"
xmlns:viewmodel="clr-namespace:MauiApp1.ViewModel"
x:DataType="viewmodel:MainViewModel">
<Grid RowDefinitions="100, Auto, *"
ColumnDefinitions=".75*, .25*"
Padding="10"
RowSpacing="10"
ColumnSpacing="10">
<Image Grid.ColumnSpan="2" Source="tom.jpg"
BackgroundColor="Transparent"></Image>
<Entry Placeholder="Enter task" Grid.Row="1" Text="{Binding Text}"></Entry>
<Button Text="Add" Grid.Row="1" Grid.Column="1" Command="{Binding AddCommand}"></Button>
<CollectionView Grid.Row="2" Grid.ColumnSpan="2" ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="{x:Type x:String}">
<SwipeView>
<SwipeView.RightItems>
<SwipeItem Text="Delete" BackgroundColor="Red"
Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodel:MainViewModel}}, Path=DeleteCommand}"
CommandParameter="{Binding .}">
</SwipeItem>
</SwipeView.RightItems>
<Grid Padding="0,5">
<Frame>
<Label Text="{Binding .}" FontSize="24"></Label>
</Frame>
</Grid>
</SwipeView>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Grid>
</ContentPage>
View Model:
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace MauiApp1.ViewModel
{
public partial class MainViewModel : ObservableObject
{
public MainViewModel()
{
Items = new();
Items.Add("test");
}
[ObservableProperty]
private ObservableCollection<string> items;
[ObservableProperty]
private string text;
[RelayCommand]
private void Add()
{
if (string.IsNullOrWhiteSpace(text))
{
return;
}
Items.Add(Text);
Text = string.Empty;
}
[RelayCommand]
private void Delete(string s)
{
if (Items.Contains(s))
{
Items.Remove(s);
}
}
}
}
Why is DeleteCommand not triggering?
try this
Command="{Binding BindingContext.DeleteCommand,
Source={x:Reference myPage}}"
where myPage is your page's name
<ContentPage x:Name="myPage" ...
Resolved,
I forgot to add <SwipeItems> element after <SwipeView.RightItems>.
AncestorType={x:Type viewmodel:MainViewModel}
Your viewmodel is not part of the visual tree, so you can't bind to it with relative source anyway.
You can use your CollectionView's Binding Context and then the specific property you need:
Command="{Binding BindingContext.DeleteCommand, Source={RelativeSource AncestorType={x:Type CollectionView}}}

In a ListView how to send the clicked object back to the command in the view model - Xamarin Forms

Given the following ListView, I'd like to have a command that would send the clicked object, in this case the Address, back to a command in the view model - SelectNewAddress or DeleteAddress.
<StackLayout VerticalOptions="FillAndExpand" Padding="10,15,10,15">
<Label Text="Addresses" FontSize="22" HorizontalTextAlignment="Center" FontAttributes="Bold" Padding="0,0,0,7" TextColor="#404040" />
<StackLayout VerticalOptions="FillAndExpand">
<flv:FlowListView FlowColumnCount="1"
HeightRequest="200"
SeparatorVisibility="None"
HasUnevenRows="True"
FlowItemsSource="{Binding AllAddresses}">
<flv:FlowListView.FlowColumnTemplate>
<DataTemplate x:DataType="popups:AddressItem">
<Grid ColumnDefinitions="*,35" Padding="0,0,0,15" x:Name="Item">
<Grid Grid.Column="0">
<Grid.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding SelectNewAddress}" />
</Grid.GestureRecognizers>
<Label Text="{Binding MainAddress}"
LineBreakMode="TailTruncation"
HorizontalTextAlignment="Start"
VerticalTextAlignment="Center"
FontSize="18"
TextColor="{StaticResource CommonBlack}"/>
</Grid>
<Grid Grid.Column="1" IsVisible="{Binding IsSelected}" >
<Grid.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding SelectNewAddress}"/>
</Grid.GestureRecognizers>
<StackLayout Padding="10,0,0,0">
<flex:FlexButton Icon="check.png"
WidthRequest="25"
HeightRequest="25"
CornerRadius="18"
BackgroundColor="{StaticResource Primary}"
ForegroundColor="{StaticResource CommonWhite}"
HighlightBackgroundColor="{StaticResource PrimaryDark}"
HighlightForegroundColor="{StaticResource CommonWhite}"/>
</StackLayout>
</Grid>
<Grid Grid.Column="1" IsVisible="{Binding IsSelected, Converter={StaticResource invertBoolConverter}}">
<Grid.GestureRecognizers>
<TapGestureRecognizer Command="{Binding DeleteAddress} />
</Grid.GestureRecognizers>
<StackLayout Padding="10,0,0,0">
<flex:FlexButton Icon="deleteCard.png"
WidthRequest="25"
HeightRequest="25"
CornerRadius="18"
BackgroundColor="{StaticResource WooopDarkGray}"
ForegroundColor="{StaticResource CommonWhite}"
HighlightBackgroundColor="{StaticResource PrimaryDark}"
HighlightForegroundColor="{StaticResource CommonWhite}"/>
</StackLayout>
</Grid>
</Grid>
</DataTemplate>
</flv:FlowListView.FlowColumnTemplate>
</flv:FlowListView>
</StackLayout>
The commands in the view model are the following:
...
public ICommand SelectNewAddress { get; set; }
public ICommand DeleteAddress { get; set; }
...
public AddressSelectionViewModel()
{
DeleteAddress = new Command(DeleteAddressCommand);
SelectNewAddress = new Command(SelectNewAddressCommand);
}
...
private void SelectNewAddressCommand(object obj)
{
try
{
var item = (AddressItem)obj;
AddressHelper.UpdateAddress(item.DeliveryAddressLocation);
UpdateAddresses();
}
catch (Exception ex)
{
// TODO
}
}
private void DeleteAddressCommand(object obj)
{
try
{
var item = (AddressItem)obj;
AddressHelper.RemoveAddress(item.DeliveryAddressLocation);
UpdateAddresses();
}
catch (Exception ex)
{
// TODO
}
}
I want the object obj passed to SelectNewAddressCommand and DeleteAddressCommand to be the address clicked on the ListView
First make sure you have included your view model as DataType and view as Class inside the ContentPage:
xmlns:pages="clr-namespace:your.namespace.ViewModels"
x:DataType="pages:AddressSelectionViewModel"
x:Class="your.namespace.Views.AddressSelectionPage"
<ContentPage xmlns="..."
xmlns:x="..."
xmlns:flv="..."
xmlns:popups="..."
xmlns:flex="..."
xmlns:views="..."
xmlns:xct="..."
xmlns:pages="clr-namespace:your.namespace.ViewModels"
x:DataType="pages:AddressSelectionViewModel"
x:Class="your.namespace.Views.AddressSelectionPage"
Shell.FlyoutItemIsVisible="..."
Shell.NavBarIsVisible="..."
Shell.TabBarIsVisible="...">
Inside the top Grid element add property x:Name="Item" ("Item" is only used as an example, you can name it anything):
<flv:FlowListView FlowColumnCount="1"
HeightRequest="200"
SeparatorVisibility="None"
HasUnevenRows="True"
FlowItemsSource="{Binding AllAddresses}">
<flv:FlowListView.FlowColumnTemplate>
<DataTemplate x:DataType="popups:AddressItem">
<Grid ColumnDefinitions="*,35" Padding="0,0,0,15" x:Name="Item"> <!-- Here -->
Then we change the Command and CommandParameter of the TapGestureRecognizer to the following:
<TapGestureRecognizer
Command="{Binding Path=SelectNewAddress, Source={RelativeSource AncestorType={x:Type pages:AddressSelectionViewModel}}}"
CommandParameter="{Binding Source={x:Reference Item}, Path=BindingContext}" />
<TapGestureRecognizer
Command="{Binding Path=DeleteAddress, Source={RelativeSource AncestorType={x:Type pages:AddressSelectionViewModel}}}"
CommandParameter="{Binding Source={x:Reference Item}, Path=BindingContext}" />
In the Command we specify the function as Path, then we clarify that the source of this function is inside the view model through AncestoryType. When inside a list view we cannot reference properties outside the object being iterated. Hence, we need to specify the desired source.
So now we are referencing the actual function. But we aren't sending the object obj as a parameter yet.
In the CommandParameter we have to pass the currently bound object with Path and Source. Note that in Source we are referencing has the name Item we defined as the x:Name of the Grid earlier.
Make sure the page has the viewmodel as its BindingContext. (If you are doing mvvm, you've already done this.)
Give <flv:FlowListView a name:
<flv:FlowListView x:Name="theListView" ... >
The item needs to refer to the command in the page's viewmodel. BindingContext propagates down through the hierarchy, so this is easily done relative to the listview name:
<flv:FlowListView.FlowColumnTemplate>
<DataTemplate>
...
<TapGestureRecognizer
Command="{Binding BindingContext.SelectNewAddress, Source={x:Reference theListView}}" ...
The item's BindingContext is the item model, so that is easily passed as a parameter:
<TapGestureRecognizer
Command=... CommandParameter="{Binding .}" />
NOTE: Differences between this and Wizard's answer:
{Binding .} is all that is needed to refer to the item itself.
Instead of using RelativeSource, which requires specifying a Type, my personal preference is to name a view, then refer to that name. I find this easier to read and to remember how to do.
I left out all details that are not relevant to the question. The above steps are sufficient. (x:DataType commands are good for performance, so I am in no way suggesting not to do them. But that is a separate topic, IMHO.)

Scrolling when there are two CollectionViews in ContentPage

I have two CollectionViews in my ContentPage inside a StackLayout, one above the other. Each binds to a separate ItemsSource. Above each one I have a Label. At this point each one take up 50% of the screen and scrolls separately.
I would like everything to scroll as though it were one long list.
So I surrounded everything with a ScrollView. But then, depending on where you put your finger, the scroll may scroll the entire page (which is what I want) or just the current CollectionView.
It seems like there is no way to cancel the scroll capability of the CollectionView. Is that true? and if not, How should I set up my ContentPage ?
In the below example both CollectionViews have the same model and binding but in reality they will be different.
Here is the xaml:
<RefreshView
x:DataType="local:AllRestaurantsViewModel"
Command="{Binding LoadItemsCommand}"
IsRefreshing="{Binding IsBusy, Mode=TwoWay}">
<ScrollView>
<StackLayout>
<Label
FontSize="Large"
HorizontalOptions="Center"
Text="Suggested Restaurants" />
<CollectionView
x:Name="ItemsListView"
ItemsSource="{Binding SuggestedRestsComments}"
SelectionMode="None">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout Padding="10" x:DataType="model:SuggestedRestsComment">
<Label
FontSize="16"
LineBreakMode="NoWrap"
Style="{DynamicResource ListItemTextStyle}"
Text="{Binding restaurantName}" />
<Label
FontSize="13"
LineBreakMode="NoWrap"
Style="{DynamicResource ListItemDetailTextStyle}"
Text="{Binding CityName}" />
<StackLayout.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding Source={RelativeSource AncestorType={x:Type local:ItemsViewModel}}, Path=ItemTapped}"
CommandParameter="{Binding .}"
NumberOfTapsRequired="1" />
</StackLayout.GestureRecognizers>
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<Label
FontSize="Large"
HorizontalOptions="Center"
Text="Existing Restaurants" />
<CollectionView
x:Name="ItemsListView2"
ItemsSource="{Binding SuggestedRestsComments}"
SelectionMode="None">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout Padding="10" x:DataType="model:SuggestedRestsComment">
<Label
FontSize="16"
LineBreakMode="NoWrap"
Style="{DynamicResource ListItemTextStyle}"
Text="{Binding restaurantName}" />
<Label
FontSize="13"
LineBreakMode="NoWrap"
Style="{DynamicResource ListItemDetailTextStyle}"
Text="{Binding CityName}" />
<StackLayout.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding Source={RelativeSource AncestorType={x:Type local:ItemsViewModel}}, Path=ItemTapped}"
CommandParameter="{Binding .}"
NumberOfTapsRequired="1" />
</StackLayout.GestureRecognizers>
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</StackLayout>
</ScrollView>
</RefreshView>
You could have a try with Custom CollectionViewRenderer to achieve that in each platform.
For example, send a mesage in Forms:
void OnButtonClicked(object sender, EventArgs e)
{
MessagingCenter.Send<object>(this, "StopScrollinng");
}
Then in iOS CustomCollectionViewRenderer class stop scrolling:
public class CustomCollectionViewRenderer: CollectionViewRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<GroupableItemsView> e)
{
base.OnElementChanged(e);
MessagingCenter.Subscribe<object>(this, "StopScrollinng", (sender) =>
{
// Do something whenever the "StopScrollinng" message is received
if (Control != null)
{
NSArray s = Control.ValueForKey(new NSString("_subviewCache")) as NSMutableArray;
UICollectionView c = s.GetItem<UICollectionView>(0);
c.SetContentOffset(c.ContentOffset, true);
}
});
}
}
And in Android CustomCollectionViewRenderer class stop scrolling:
public class CustomCollectionViewRenderer: CollectionViewRenderer
{
public CustomCollectionViewRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<ItemsView> elementChangedEvent)
{
base.OnElementChanged(elementChangedEvent);
MessagingCenter.Subscribe<object>(this, "StopScrollinng", (sender) =>
{
// Do something whenever the "StopScrollinng" message is received
this.DispatchTouchEvent(MotionEvent.Obtain(SystemClock.UptimeMillis(), SystemClock.UptimeMillis(), MotionEventActions.Cancel, 0, 0, 0));
});
}
}
How about this?
In your scrollview set InputTransparent="True" this allows the input to go through to the layer underneath.
<ScrollView InputTransparent="True">
Then leave some white space (background) on the right side of the collection views.
Now when someone swipes in the white space, the entire page scrolls. And when someone swipes inside the collection view, the collection view scrolls.
taken from https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/collectionview/populate-data
the idea is to not use two separate collectionviews and merge them into one by choosing a datatemplate at runtime
xmlns:controls="clr-namespace:<your namespace>.Controls"
<ContentPage.Resources>
<DataTemplate x:Key="DataTemplate1">
...
</DataTemplate>
<DataTemplate x:Key="DataTemplate2">
...
</DataTemplate>
<controls:DataTemplateSelector1 x:Key="DataTemplateSelector1"
Template1="{StaticResource DataTemplate1}"
Template2="{StaticResource DataTemplate2}" />
</ContentPage.Resources>
namespace <your namespace>.Controls
{
public class DataTemplateSelector1: DataTemplateSelector
{
public DataTemplate Template1 { get; set; }
public DataTemplate Template2 { get; set; }
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
{
//here you return which template you want to use based on the properties of "item" . e.g you can do item is SomeClass ? Template1 : Template2
}
}
}
<ScrollView>
<CollectionView x:Name="collection" ItemTemplate="{StaticResource DataTemplateSelector1}"></CollectionView>
</ScrollView>

How bind a command in DataTemplate in Resource Dictionary?

I'm trying to make a better solution architecture, for that I've separated many parts of code in differents files. Because my application use a lot of DataTemplates, I push them in different ResourceDictionary.xaml files.
Problem :
I have a view Agenda.xaml, with the viewModel AgendaViewModel. This view have a ListView which call's datatemplate in external ResourceDictionary file. But if I want put a Binding Command in the dataTemplate, the command is never executed because (I guess) the resource Dictionary where is my DataTemplate not reference ViewModel.
What can I do ?
I've already tried some weird Binding code like
<TapGestureRecognizer Command="{Binding BindingContext.OpenActiviteCommand, Source={x:Reference agendaPage}}" CommandParameter="{Binding .}"/>
Where "agendaPage" is the x:Name of Agenda.xaml.
All I found on Google was about WPF and Binding property not available on Xamarin Forms (RelativeSource, ElementName etc...)
I know I can put dataTemplate in my Agenda.xaml view, but I really want keep it in an external file. I want avoid view files with 1500 lines....
This is my Agenda.xaml 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"
x:Class="Corim.Portable.CorimTouch.ViewForms.Agenda.AgendaViewDetail"
xmlns:converters="clr-namespace:Corim.Portable.CorimTouch.Converters"
Title="Agenda"
x:Name="agendaPage">
<ContentPage.Content>
<Grid HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" BackgroundColor="{StaticResource LightGrayCorim}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Liste itv,pointage,activite -->
<ListView
x:Name="listAgenda"
Grid.Row="1"
SeparatorVisibility="None"
HasUnevenRows="True"
SelectionMode="None"
CachingStrategy="RecycleElement"
ItemsSource="{Binding AgendaList}"
ItemTemplate="{StaticResource agendaTemplateSelector}"
BackgroundColor="{StaticResource LightGrayCorim}">
</ListView>
</Grid>
</ContentPage.Content>
</ContentPage>
And this is one part of Datatemplate in AgendaTemplates.xaml
<DataTemplate x:Key="agenda-adresse-intervention">
<ViewCell>
<Frame Margin="10,5,10,0" HasShadow="False" Padding="0" CornerRadius="10" IsClippedToBounds="True">
<controls:CustomTappedStackLayout
BackgroundColor="White"
TappedBackgroundColor="{StaticResource RollOver}"
HorizontalOptions="FillAndExpand"
Orientation="Horizontal"
Padding="10">
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Command="{Binding Path=BindingContext.OpenParcCommand, Source={x:Reference agendaPage}}" CommandParameter="{Binding .}" NumberOfTapsRequired="1"/>
</StackLayout.GestureRecognizers>
<Image
Source="localisation_adresse"
WidthRequest="30"
HeightRequest="30"
Aspect="AspectFit"
HorizontalOptions="Start"
Margin="10"
VerticalOptions="StartAndExpand"/>
<StackLayout
HorizontalOptions="FillAndExpand"
Orientation="Vertical">
<Label
Text="{Binding Client}"
IsVisible="{Binding Client, Converter={StaticResource StringEmptyBooleanConverter}}"
FontFamily="{StaticResource SemiBoldFont}"
FontSize="{StaticResource MediumTextSize}"
TextColor="Black"/>
<Label
Text="{Binding Title}"
IsVisible="{Binding Title, Converter={StaticResource StringEmptyBooleanConverter}}"
FontFamily="{StaticResource RegularFont}"
FontSize="{StaticResource DefaultTextSize}"
TextColor="Gray"/>
</StackLayout>
</controls:CustomTappedStackLayout>
</Frame>
</ViewCell>
</DataTemplate>
But if I want put a Binding Command in the dataTemplate, the command
is never executed because (I guess) the resource Dictionary where is
my DataTemplate not reference ViewModel.
You guess wrong: it's totally fine to do what you are doing and should work transparently. The binding is resolved at runtime your data template does not know anything about the object that will be bound.
1st: drop the BindingContext.OpenActiviteCommand nonsense :) Just bind to OpenActiviteCommand, the only question is:
2nd: Where is your OpenActiviteCommand ?
The data context of your AgendaTemplates is the item in your AgendaList.
If the type of the AgendaList is an ObservableCollection<AgendaViewModel>, and your AgendaViewModel has a OpenParcCommand then it should be fine:
public class AgendaViewModel
{
public AgendaViewModel(ICommand openParcCommand)
{
OpenParcCommand = openParcCommand;
}
public ICommand OpenParcCommand { get; }
}
and in your AgendaPageViewModel:
public class AgendaPageViewModel
{
public ObservableCollection<AgendaViewModel> AgendaList { get; }
}
Thanks to #Roubachof
The soluce was replace my ListView of InterventionModel by ListView of AgendaDataViewModel.
AgendaViewModel is a new class which contains all the commands I need, and an InterventionModel.
this is AgendaDataViewModel :
public class AgendaDataViewModel : HybridContentViewModel
{
private InterventionModel _model;
public InterventionModel Model
{
get => _model;
set { _model = value; }
}
public ICommand OpenActiviteCommand { get; private set; }
public AgendaDataViewModel()
{
this.OpenActiviteCommand = new Command<InterventionModel>(this.OpenActivite);
}
/// <summary>
/// Ouvre le formulaire d'édition de l'activité
/// </summary>
/// <param name="model"></param>
private void OpenActivite(InterventionModel model)
{
//TODO amener sur le formulaire d'activité
}
}
my AgendaTemplate.xaml
<!--Template pour l'affichage du parc-->
<DataTemplate x:Key="agenda-adresse-intervention">
<ViewCell>
<Frame Margin="10,5,10,0" HasShadow="False" Padding="0" CornerRadius="10" IsClippedToBounds="True">
<controls:CustomTappedStackLayout
BackgroundColor="White"
TappedBackgroundColor="{StaticResource RollOver}"
HorizontalOptions="FillAndExpand"
Orientation="Horizontal"
Padding="10">
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Command="{Binding OpenParcCommand}" CommandParameter="{Binding Model}" NumberOfTapsRequired="1"/>
</StackLayout.GestureRecognizers>
<Image
Source="localisation_adresse"
WidthRequest="30"
HeightRequest="30"
Aspect="AspectFit"
HorizontalOptions="Start"
Margin="10"
VerticalOptions="StartAndExpand"/>
<StackLayout
HorizontalOptions="FillAndExpand"
Orientation="Vertical">
<Label
Text="{Binding Model.Client}"
IsVisible="{Binding Model.Client, Converter={StaticResource StringEmptyBooleanConverter}}"
FontFamily="{StaticResource SemiBoldFont}"
FontSize="{StaticResource MediumTextSize}"
TextColor="Black"/>
<Label
Text="{Binding Model.Title}"
IsVisible="{Binding Model.Title, Converter={StaticResource StringEmptyBooleanConverter}}"
FontFamily="{StaticResource RegularFont}"
FontSize="{StaticResource DefaultTextSize}"
TextColor="Gray"/>
</StackLayout>
</controls:CustomTappedStackLayout>
</Frame>
</ViewCell>
</DataTemplate>
As you can see, the values binding is made by this line :
{Binding Model.Client}
where Client is the name of Binded property. And to Bind a Command, you don't need Model, and just bind like this :
Command={Binding CommandName}
Hope it helps someone in the future !

How can i capture the event of a tapped item (located in a template) from the base class?

I have a base grid
<Grid Grid.Row="1" Grid.Column="1" x:Name="GridName">
<StackLayout Orientation="Vertical">
<art:GridOptionsView ItemsSource="{Binding Items}" >
<art:GridOptionsView.ItemTemplate>
<DataTemplate>
<uikit:DashboardItemTemplate />
</DataTemplate>
</art:GridOptionsView.ItemTemplate>
</art:GridOptionsView>
</StackLayout>
</Grid>
which uses the following DashboardItemTemplate
<?xml version="1.0" encoding="UTF-8"?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
BackgroundColor="White">
<ContentView.Content>
<Grid Padding="0">
<StackLayout VerticalOptions="Center" HorizontalOptions="Center" Orientation="Vertical" Spacing="10">
<Grid>
<Label Text="" Style="{StaticResource FontIcon}" HorizontalTextAlignment="Center" Opacity="1" FontSize="130" TextColor="{Binding BackgroundColor}" VerticalOptions="Center" HorizontalOptions="Center" IsVisible="{Binding Source={x:Reference Root}, Path=ShowiconColoredCircleBackground}" />
<Label Text="{Binding Icon}" Style="{StaticResource FontIcon}" Opacity="1" TextColor="White" VerticalOptions="Center" HorizontalOptions="Center" />
</Grid>
<Label Text="{Binding Name}" TextColor="{Binding Source={x:Reference Root}, Path=TextColor}" FontSize="14" HorizontalTextAlignment="Center">
</Label>
</StackLayout>
</Grid>
</ContentView.Content>
<ContentView.GestureRecognizers>
<TapGestureRecognizer Tapped="OnWidgetTapped" />
</ContentView.GestureRecognizers>
</ContentView>
How can i capture the "OnWidgetTapped" event on my base xaml class?
I do this usually with a custom bindable property ParentBindingContext in my template:
public class MyTemplate : ContentPage
{
public static BindableProperty ParentBindingContextProperty = BindableProperty.Create(nameof(ParentBindingContext),
typeof(object), typeof(BasePageTemplate));
public object ParentBindingContext
{
get { return GetValue(ParentBindingContextProperty); }
set { SetValue(ParentBindingContextProperty, value); }
}
}
And then in your page (which contains the template) just set the ParentBindingContext:
<DataTemplate>
<template:MyTemplate ParentBindingContext="{Binding BindingContext, Source={x:Reference Name=MyPageName}}" />
</DataTemplate>
With that you can access the full BindingContext of your page in your template. The following example of a command shows how the template can bind to a command MyCommand, which is in the BindingContext of the page:
Command="{Binding ParentBindingContext.MyCommand, Source={x:Reference Name=MyTemplatePageName}}"
But this presupposes that your page has a BindingContext behind (like a ViewModel). This ViewModel then contains the "global" commands for the whole page. These commands (or just methods) can then be accessed by the template, because they know about the BindingContext of the page.
I changed an answer from flow description to the code. The idea is to create ItemTemplate programatically and pass to its constructor the page with list (or grid). Define a function ItemTemplateTapped and call it from template.
EventOnGridPage
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="ButtonRendererDemo.EventOnGridPage">
<ListView x:Name="listView" >
</ListView>
</ContentPage>
EventOnGridPage code behind
public partial class EventOnGridPage : ContentPage
{
public EventOnGridPage()
{
InitializeComponent();
listView.ItemsSource = new List<Contact>
{
new Contact { Name = "Kirti",Status = "True"},
new Contact { Name = "Nilesh",Status = "False"}
};
listView.ItemTemplate = new DataTemplate(loadTemplate);
}
private object loadTemplate()
{
return new ViewCell() { View = new EventOnGridTemplate(this) };
}
public void ItemTemplateTapped(string name)
{
DisplayAlert("ItemTemplateTapped", name, "OK");
}
}
EventOnGridTemplate 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="ButtonRendererDemo.EventOnGridTemplate"
BackgroundColor="Green">
<Label Text="{Binding Name}" x:Name="myLabel"></Label>
</ContentView>
EventOnGridTemplate code behind
public partial class EventOnGridTemplate
{
EventOnGridPage parent;
public EventOnGridTemplate(EventOnGridPage parent)
{
this.parent = parent;
InitializeComponent();
var tapGestureRecognizer = new TapGestureRecognizer();
tapGestureRecognizer.Tapped += TapGestureRecognizer_Tapped;
myLabel.GestureRecognizers.Add(tapGestureRecognizer);
}
private void TapGestureRecognizer_Tapped(object sender, EventArgs e)
{
parent.ItemTemplateTapped(myLabel.Text);
}
}
If you already defined the tap gesture binding in the XAML code, you don't need to add the TapGestureRecognizer, simply sign your method to an event listener method:
Your XAML:
<ContentView.GestureRecognizers>
<TapGestureRecognizer Tapped="OnWidgetTapped" />
</ContentView.GestureRecognizers>
On C# code behind:
public void OnWidgetTapped(object sender, EventArgs args)
{
// do stuff here
}
You just need to implement your OnWidgetTapped method:
void OnWidgetTapped(object sender, System.EventArgs e)
{
// Do stuff here
}
Another solution. If you implemented OnWidgetTapped as was suggested you can use sender.Parent.Parent... till you get to the object you want - grid or page. Cast it to for example EventOnGridPage and then call the function of that object.