Scrolling when there are two CollectionViews in ContentPage - xaml

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>

Related

Workaround for ScrollView not scrolling inside a StackLayout in Maui

I have a page that presents a list of items in a CollectionView within a ScrollView. I want the user to be able to add a new item to this list by hitting an "add" button or tapping an image at the top of the page. I first tried this using the hierarchy of views shown below. The code below is an abbreviated version of the real thing. I discovered that putting a ScrollView within a VerticalStackLayout breaks the scrolling in Maui! Here is the reported bug.
I tried deleting the VerticalStackLayout that precedes the ScrollView and the scrolling still doesn't work.
<ContentPage.Content>
<VerticalStackLayout>
<VerticalStackLayout HorizontalOptions="Center">
<Image Source="add.png">
<ImageGestureRecongnizers>
<TapGestureRecognizer... Code to add new item to MyCollection...]/>
</ImageGestureRecognizers>
</Image>
</VerticalStackLayout>
<ScrollView>
<CollectionView ItemsSource="{Binding MyCollection}">
<CollectionView.ItemTemplate>
<DataTemplate>
[Layout for displaying items in MyCollection...]
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ScrollView>
<VerticalStackLayout
</ContentPage.Content>
I'd greatly appreciate suggestions on a workaround to allow the viewing of the scrollable list and adding an item to the list by tapping an object (button or image) that's always visible on the page regardless of how the list is scrolled.
Actually for the Xaml, this will work...
<?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="MauiAppTest01.MainPage">
<Grid RowDefinitions="30,*">
<CollectionView ItemsSource="{Binding MyCollection}"
Grid.Row="1">
<CollectionView.ItemTemplate>
<DataTemplate>
<VerticalStackLayout >
<Label Text="{Binding Name}" FontSize="Large" />
</VerticalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<Image Source="dotnet_bot.png" HeightRequest="100" WidthRequest="100"
Margin="0,0,50,20">
<Image.GestureRecognizers>
<TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped"/>
</Image.GestureRecognizers>
</Image>
</Grid>
Adjust your RowDefinition(30) for the image!
Sorry if my code is not neat, as I'm on mobile.
I ended up creating a "fancy" floating button on the bottom right of the page by:
Creating a one-row Grid
Putting both the CollectionView and Image in the one row
Defining the Image after the CollectionView so that the Image sits on top of the CollectionView
I also got rid of the ScrollView per Jason's suggestion.
<ContentPage.Content>
<Grid
<CollectionView ItemsSource="{Binding MyCollection}">
<CollectionView.ItemTemplate>
<DataTemplate>
[Layout for displaying items in MyCollection...]
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<Image Source="add.png" HeightRequest="40" HorizontalOptions="End"
VerticalOptions="End" Margin="0,0,50,20">
<ImageGestureRecongnizers>
<TapGestureRecognizer... Code to add new item to MyCollection...]/>
</ImageGestureRecognizers>
</Image>
</Grid>
</ContentPage.Content>
If you want to allow the viewing of the scrollable list and add items to the list by tapping an image, you can just wrap them with a Grid.
Here's the code snippet below for your reference:
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="MauiAppTest01.MainPage">
<Grid>
<CollectionView ItemsSource="{Binding MyCollection}">
<CollectionView.ItemTemplate>
<DataTemplate>
<VerticalStackLayout >
<Label Text="{Binding Name}" FontSize="Large" />
</VerticalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<Image Source="dotnet_bot.png" HeightRequest="100" WidthRequest="100" HorizontalOptions="End" VerticalOptions="End" Margin="0,0,50,20">
<Image.GestureRecognizers>
<TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped"/>
</Image.GestureRecognizers>
</Image>
</Grid>
</ContentPage>
Code-behind:
public partial class MainPage : ContentPage
{
public ObservableCollection<MyModel> MyCollection { get; set; }
public MainPage()
    {
            InitializeComponent();
MyCollection = new ObservableCollection<MyModel>
{
new MyModel{ Name="1"},
new MyModel{ Name="2"},
};
BindingContext = this;
}
private void TapGestureRecognizer_Tapped(object sender, EventArgs e)
{
for (int i = 0; i < 10; i++) {
MyCollection.Add(new MyModel { Name = i+"" });
}
}
}
Model:
public class MyModel
{
public string Name { get; set; }
}

Get (and bind to) current displaywidth of element in Maui

In my xaml setup I have a Label, which should wrap its text. It is contained within a border, and I want the Label width to be the width of the border. I've tried to do this with the following binding on the label:
WidthRequest="{Binding Source={RelativeSource Mode=FindAncestor, AncestorType={x:Type Border}}, Path=Width}"
This has not worked though, and i suspect it hasn't because the Width that gets returned is NaN (I've written out the width of the border during runtime and it was NaN). But this is confusing to me, as I have a binding on the Border to the width of the Flexlayout which is working. Is there anything I'm missing and is there any better way to do this?
xaml:
<ScrollView Grid.Row="1">
<FlexLayout BindableLayout.ItemsSource="{Binding DisplayedUserActions}" JustifyContent="Start" Wrap="Wrap" Direction="Row">
<BindableLayout.ItemTemplate>
<DataTemplate>
<Border FlexLayout.AlignSelf="Stretch" FlexLayout.Basis="{Binding Source={RelativeSource FindAncestor, AncestorType{x:Type ScrollView}}, Path=Width, Converter={StaticResource WidthToFlexlayoutBasisConverter}}">
<Grid RowDefinitions="*,*">
-- more content --
<Label Grid.Row="1" Text="{Binding Title}" LineBreakMode="WordWrap" HorizontalTextAlignment="Center" VerticalOptions="Center" HorizontalOptions="Center" TextColor="Black" Margin="5" WidthRequest="{Binding Source={RelativeSource Mode=FindAncestor, AncestorType={x:Type Border}}, Path=Width}"/>
</Grid>
</Border>
</DataTemplate>
</BindableLayout.ItemTemplate>
</FlexLayout>
</ScrollView>
You can use data binding to achieve the same width for Label and Border. Bind the Border and Label's WidthRequest properties to the TestWidth property in the same binding context. like this:
Xaml:
<StackLayout>
<StackLayout.BindingContext>
<local:MyViewModel/>
</StackLayout.BindingContext>
<Border BackgroundColor="Pink" WidthRequest="{Binding Testwidth}">
<Label x:Name="lable" Text="test1111111111111111" WidthRequest="{Binding Testwidth}"/>
</Border>
</StackLayout>
ViewModel:
public class MyViewModel : INotifyPropertyChanged
{
public double testwidth;
public event PropertyChangedEventHandler PropertyChanged;
public double Testwidth
{
get { return testwidth; }
set
{
if (testwidth != value)
{
testwidth = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Testwidth"));
}
}
}
public MyViewModel() { testwidth= 100; }
}

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.)

Is there a way to limit the boundaries of a drag event to the size of the screen?

I'm making a weather application and on one of the pages, users can save cities to quickly look up the weather for that city. I also want them to be able to delete a city in the list by pressing and holding and then dragging it to the delete button on the bottom of the screen.
The problem is that the label for the city name is the width of the entire screen and when you press and hold a city, you automatically grab the center of this label. Because of this, when you press the city name to much to the left of the label, the name disappears outside of the screen.
I can think of 2 possible solutions; make the screen have some kind of boundaries so nothing can disappear or make the label the width of the text within. But I can't find any of these solutions online. Does someone know how to do this or maybe has another solution?
This is the code for the xaml file.
<StackLayout>
<CollectionView x:Name="CitiesListView"
ItemsSource="{Binding Cities}"
SelectionMode="None">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout Padding="10" x:DataType="model:City">
<Label Text="{Binding name}"
LineBreakMode="NoWrap"
Style="{DynamicResource ListCityTextStyle}"
FontSize="16" />
<StackLayout.GestureRecognizers>
<DragGestureRecognizer DragStartingCommand="{Binding Path=DragStartingCommand, Source={RelativeSource AncestorType={x:Type local:CitiesViewModel}}}" DragStartingCommandParameter="{Binding .}"/>
<TapGestureRecognizer
NumberOfTapsRequired="1"
Command="{Binding Source={RelativeSource AncestorType={x:Type local:CitiesViewModel}}, Path=CityTapped}"
CommandParameter="{Binding .}">
</TapGestureRecognizer>
</StackLayout.GestureRecognizers>
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<StackLayout HorizontalOptions="Center" VerticalOptions="FillAndExpand" Padding="5,10,5,10">
<StackLayout.GestureRecognizers>
<DropGestureRecognizer DropCommand="{Binding DropOverCommand}"/>
</StackLayout.GestureRecognizers>
<Image HeightRequest="40"
WidthRequest="40"
Source="trash_icon.png"
HorizontalOptions="EndAndExpand"/>
</StackLayout>
</StackLayout>
And this is the code for the drag and drop commands
public Command DragStartingCommand => new Command<City>((param) =>
{
var dur = TimeSpan.FromSeconds(0.1);
Vibration.Vibrate(dur);
_dragCity = param;
});
public Command DropOverCommand => new Command(() =>
{
if (Cities.Contains(_dragCity))
{
Cities.Remove(_dragCity);
removeCity(_dragCity);
}
});
public async void removeCity(City city)
{
await DataStore.DeleteCityAsync(city.id);
}
private City _dragCity;
Here is also a link to a video maybe better understand what I am talking about. Video
You could try to set the DragGestureRecognizer for the Label instead of StackLayout. A simple example for your reference. The text of Label would on the point which you pressed.
Xaml:
private async void OnDropGestureRecognizerDragOver(object sender, DragEventArgs e)
{
e.AcceptedOperation = DataPackageOperation.Copy;
}
private async void OnDropGestureRecognizerDrop(object sender, DropEventArgs e)
{
await DisplayAlert("Correct", "Congratulations!", "OK");
//await DisplayAlert("Incorrect", "Better luck next time.", "OK");
}
Code behind:
<StackLayout Margin="20">
<Label Text="Answer the following question by dragging your answer to the Entry." />
<Label Text="What's 2+2?" />
<Grid ColumnDefinitions="0.5*, 0.5*"
Margin="0,20,0,0">
<Label Text="3"
HorizontalOptions="Center">
<Label.GestureRecognizers>
<DragGestureRecognizer />
</Label.GestureRecognizers>
</Label>
<Label Grid.Column="1"
Text="4"
HorizontalOptions="Center">
<Label.GestureRecognizers>
<DragGestureRecognizer />
</Label.GestureRecognizers>
</Label>
</Grid>
<Entry Margin="0,20,0,0"
Placeholder="Drag your answer here">
<Entry.GestureRecognizers>
<DropGestureRecognizer DragOver="OnDropGestureRecognizerDragOver"
Drop="OnDropGestureRecognizerDrop" />
</Entry.GestureRecognizers>
</Entry>
</StackLayout>