I am trying to open another view after tapping on an item in the list view.
I have tried adding a TapGestureRegonizer and even adding ViewCell with grids etc. None of these seem to work. I have added a tap gesture to a label and that seemed to work but the same does not work for list view items. This seems like a simple problem for something like list view, but there doesnt seem to be a built in functionality for this.
The Xaml:
<ListView x:Name="dataList"
ItemsSource="{Binding routeLabels}"
HasUnevenRows="True"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="3">
</ListView>
The code behind:
var listviewgesture = new TapGestureRecognizer();
listviewgesture.SetBinding(TapGestureRecognizer.CommandProperty,"LoadRoutePage");
dataList.GestureRecognizers.Add(listviewgesture);
The view model:
public ICommand LoadRoutePage { get; protected set; }
public DriverDashboardViewModel(INavigation navigation,MessagDatabase database)
{
this._database = database;
this.Navigation = navigation;
this.LoadNotifications = new Command(async () => await OpenNotificationsPage());
this.LoadRoutePage = new Command(async () => await OpenRoutePage());
}
public async Task OpenRoutePage()
{
await Navigation.PushAsync(new RoutePageView());
}
Just to be clear the LoadNotifications method does work in opening a page but LoadRoutePage does not. So I know there is some level of communication between the view and viewmodel.
You should not be adding a TapGestureRecognizer to a ListView. Every cell already has events that handle tapping on them and a GestureRecognizer would probably only confuse the ListView regarding what the tap should be doing. There are a few ways to go about this.
1. SelectedItem binding
Bind a SelectedItem property to the ListView and handle your method calls in the setter of that property.
<ListView x:Name="dataList" ItemsSource="{Binding routeLabels}"
HasUnevenRows="True" Grid.Row="1" Grid.Column="0"
Grid.ColumnSpan="3" SelectedItem="{Binding SelectedItem}">
</ListView>
And in your viewmodel:
string _selectedItem;
public string SelectedItem {
get {return _selectedItem; }
set
{
_selectedItem = value;
// Additional code
}
}
2. Use the built in events ItemSelected or ItemTapped
A ListView has some events you can hook up named ItemSelected and ItemTapped. These can be caught in code-behind and can handle what you're trying to achieve.
<ListView x:Name="dataList" ItemsSource="{Binding routeLabels}"
HasUnevenRows="True" Grid.Row="1" Grid.Column="0"
Grid.ColumnSpan="3" ItemSelected="Handle_ItemSelected" ItemTapped="Handle_ItemTapped">
</ListView>
3. Use event to command binding with behaviors
Since you use viewmodels you ideally don't want these events since they're handled on the UI side. There are NuGet packages out there that can translate an event to a Command that you can handle in your viewmodel. Take a look at Corcav.Behaviors for example.
4. Create a behavior of your own
I have one I use regularly which looks like this:
public class ListViewSelectedItemBehavior : Behavior<ListView>
{
public static readonly BindableProperty CommandProperty = BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(ListViewSelectedItemBehavior));
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
public ListView AssociatedObject { get; private set; }
protected override void OnAttachedTo(ListView bindable)
{
base.OnAttachedTo(bindable);
AssociatedObject = bindable;
bindable.BindingContextChanged += OnBindingContextChanged;
bindable.ItemSelected += OnListViewItemSelected;
}
protected override void OnDetachingFrom(ListView bindable)
{
base.OnDetachingFrom(bindable);
bindable.BindingContextChanged -= OnBindingContextChanged;
bindable.ItemSelected -= OnListViewItemSelected;
AssociatedObject = null;
}
private void OnBindingContextChanged(object sender, EventArgs e)
{
OnBindingContextChanged();
}
private void OnListViewItemSelected(object sender, SelectedItemChangedEventArgs e)
{
if (Command == null)
return;
if (Command.CanExecute(e.SelectedItem))
Command.Execute(e.SelectedItem);
}
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
BindingContext = AssociatedObject.BindingContext;
}
}
To add this to your ListView you simply add a behavior to it:
<ListView x:Name="dataList" ItemsSource="{Binding routeLabels}"
HasUnevenRows="True" Grid.Row="1" Grid.Column="0"
Grid.ColumnSpan="3">
<ListView.Behaviors>
<behaviors:ListViewSelectedItemBehavior Command="{Binding ItemSelectedCommand}" />
</ListView.Behaviors>
</ListView>
In this case ItemSelectedCommand is a Command object in your ViewModel.
Not sure if I understand you correctly but you are trying to get an event going when someone taps on anelement of a listview?
If so you don't need a recognizer you simply have to add ItemTapped in your XAML:
<ListView x:Name="dataList"
ItemsSource="{Binding routeLabels}"
HasUnevenRows="True"
Grid.Row="1"
Grid.Column="0"
ItemTapped="Name of event"
Grid.ColumnSpan="3">
</ListView>
This will generate an event for you ( just do double tab when creating the ItemTapped ) and here you can place your code
You're binding a command instead of an event to the "Tapped" event. Try something like this:
code behind:
var listviewgesture = new TapGestureRecognizer();
listviewgesture.Tapped += Handle_listViewItemTapped;
dataList.GestureRecognizers.Add(listviewgesture);
ViewModel:
private void Handle_listViewItemTapped(object sender, EventArgs e)
{
viewModel.OpenRoutePage();
}
Related
Is it possible to create a ListView with ViewCells that will contain two Buttons and Label, first button will be "+", second "-" and a label will be a counter that will show how much "+" button has been tapped.
Then I want to be able to get from my listview an item that is binded to this viewcell and information about how much this item has been selected.
For now I created a StackLayout filled with Views thats "mocks" a Viewcells. This solution is so bad for many items because I have to create lots of Views (it takes few seconds).
So I would like to solve the problem using a ListView but I have no idea how to achive this. Or maybe you have a better solution than using a listview?
this should be trivial. First, create a data structure to hold your data
public class MyData : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private double _count;
public double Count
{
get
{ return _count; }
set
{
_count = value;
NotifyPropertyChanged();
}
}
List<MyData> data { get; set; }
you will need to initialize it with as many rows as your want to display in your list. The create a template with a Label and Buttons that are bound to your Count property
<ListView x:Name="listView" >
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout>
<Label Text="{Binding Count}" />
<Button Clicked="Increment" CommandParameter="{Binding .}" Text="+" />
<Button Clicked="Decrement" CommandParameter="{Binding .}" Text="-" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
in your code-behind
protected void Decrement(object sender, EventArgs args) {
var b = (Button)sender;
var data = (MyData)b.CommandParameter;
data.Count--;
}
protected void Increment(object sender, EventArgs args) {
var b = (Button)sender;
var data = (MyData)b.CommandParameter;
data.Count++;
}
finally, use binding or direct assignment to set the List's ItemsSourcee
listView.ItemsSource = data;
I'm trying to do some basic UI and binding in UWP to get my head wrapped around it but I'm having trouble accessing a button within a listview item.
I have a Button where on clicking it, it creates a new object which is added to my ObservableCollection which then ends up adding a new item in my ListView
XMAL
<ListView Grid.Row="1" ItemsSource="{x:Bind counts}" x:Name="buttonsView" Margin="5,0" Background="White" Foreground="#FF5059AB">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:Counter">
<StackPanel Orientation="Vertical">
<TextBlock Text="{x:Bind CountValue, Mode=TwoWay}" FontWeight="Black"/>
<Button Click="Increment_Click" Content="Increment"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
C#
public class Counter
{
private int count;
public Counter()
{
count = 0;
}
public int CountValue
{
get
{
return count;
}
set
{
count = value;
}
}
}
public sealed partial class MainPage : Page
{
ObservableCollection<Counter> counts = new ObservableCollection<Counter>();
public MainPage()
{
this.InitializeComponent();
}
private void Increment_Click(object sender, RoutedEventArgs e)
{
// ?
}
private void AddCounter_Click(object sender, RoutedEventArgs e)
{
counts.Add(new Counter());
}
}
That works.
But when I click on the button within the StackPanel, the Increment_Click method is called but I'm not sure what to do at that point. I would like to access the Counter object by getting the index of the ListView item and using that to index into the ObservableCollection.
How do I figure out what the index is of the ListView item that was added?
Instead of an event you should use Command and CommandParameter. You would then bind the Command to a command implemented in Counter and CommandParameter to the item (like {Binding}).
However, you can achieve your goal with Click event as well using DataContext:
private void Increment_Click(object sender, RoutedEventArgs e)
{
var button = (Button)sender;
var counter = (Counter)button.DataContext;
//...
}
Basically DataContext is by default inherited from the parent unless you specify otherwise, so in this case DataContext is the current list item.
my question here is how to know which button is clicked. My buttons are bound to property of type ObservableCollection which contains objects of type Item and I need to use that object in my ViewModel when a button is clicked. Any ideas how to know which button is clicked? I had few ideas, like sending multiple Command Parameters (1.SelectedItems from ListBox 2.The Object from the button) or bind the object from the button to another property of type Item in the ViewModel after the button is clicked in order to use it. Any ideas will be apreciated.
I have this DataTemplate for buttons
<DataTemplate x:Key="ButtonTemplate">
<WrapPanel>
<Button x:Name="OrderButton"
FontSize="10"
Height="80" Width="80"
Content="{Binding Name}"
Command="{Binding OrderCommand,
Source={StaticResource OrderViewModel}}"
CommandParameter="{Binding ElementName=ListBoxUserControl, Path=SelectedItems}">
</Button>
</WrapPanel>
</DataTemplate>
My ViewModel
public class OrderViewModel : ObservableCollection<Order>, INotifyPropertyChanged
{
public CreateOrderCommand CreateOrderCommand { get; set; }
public ObservableCollection<Item> Data { get; set; }
public OrderViewModel()
{
this.CreateOrderCommand = new CreateOrderCommand(this);
DataObservableCollection data= new DataObservableCollection();
Data = data;
}
}
And I populate my buttons like this
<WrapPanel x:Name="OrderButtons">
<ItemsControl ItemTemplate="{StaticResource ButtonTemplate}"
ItemsSource="{Binding Data, Source={StaticResource OrderViewModel}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal">
</WrapPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</WrapPanel>
Simply change the Button.CommandParameter binding to CommandParamter="{Binding}" if you want the data context of the button (i.e. the item from your items source) as the command parameter or,
CommandParameter="{Binding RelativeSource={RelativeSource Self}}" if you want the actual button that was clicked.
First send the Button DataContext using the CommandParameter. To send the SelectedItem of your Listbox you can use
<Listbox SelectedItem="{Binding SelectedItem}"/>
in your Listbox and make a SelectedItem property in your ViewModel.
private YourItemObject mySelectedItem;
public YourItemObject SelectedItem
{
get { return mySelectedItem; }
set
{
value = mySelectedItem
}
Now you can use the SelectedItem in your ViewModel when the Button gets clicket. If you have multiple selections it gets a little bit more tricky ;).
private ButtonClicked(Parameter object)
{
SelectedItem.UsingIt();
if(object is YourButtonDataContext){
YourButtonDataContext.UsingIt();
}
}
Update with MultiSelection:
With Multiselection you have to do your own Listbox.
public class CustomListBox : ListBox
{
public CustomListBox()
{
this.SelectionChanged += CustomListBox_SelectionChanged;
}
void CustomListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
this.SelectedItemsList = this.SelectedItems;
}
#region SelectedItemsList
public IList SelectedItemsList
{
get { return (IList)GetValue(SelectedItemsListProperty); }
set { SetValue(SelectedItemsListProperty, value); }
}
public static readonly DependencyProperty SelectedItemsListProperty =
DependencyProperty.Register("SelectedItemsList", typeof(IList), typeof(CustomListBox), new PropertyMetadata(null));
#endregion
}
In the ViewModel you have to have a property with the SelectedItems.
private IList mySelectedData = new List<SelectedDataObject>();
public IList SelectedData
{
get { return mySelectedData ; }
set
{
if (mySelectedData != value)
{
mySelectedData = value;
RaisePropertyChanged(() => SelectedData);
}
}
}
The XAML Looks like this:
<local:CustomListBox ItemsSource="{Binding YourList}" SelectionMode="Extended" SelectedItemsList="{Binding SelectedData, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
...
</local:CustomListBox>
Source for Multiselection in DataGrid is: https://stackoverflow.com/a/22908694/3330227
I am trying to create an infinite scroll in xamarin.forms. I think that the problem is, that my items are not binded to my ListView. Do I have to bind my datatemplate to listview. My datatemplate contains imagecell with text,detail and ImageSource.
While I was debugging, my listview.ItemAppearing += (sender, e) => was never called. So I am assuming that here is my problem.
I am using Http client with json response. Bellow is my code:
public partial class MainPage : ContentPage
{
readonly IList<Article> books = new ObservableCollection<Article>();
readonly BookManager manager = new BookManager();
bool isLoading;
public MainPage()
{
books = new ObservableCollection<Article>();
var listview = new ListView();
listview.ItemsSource = books;
listview.ItemAppearing += (sender, e) =>
{
if (isLoading || books.Count == 0)
return;
//hit bottom!
if (e.Item == books[books.Count - 1])
{
LoadItems();
}
};
LoadItems();
BindingContext = books;
InitializeComponent();
}
my LoadItems method:
public async void LoadItems()
{
//simulator delayed load
var bookCollection = await manager.GetAll();
foreach (Article book in bookCollection.articles)
{
books.Add(book);
}
}
and my xamlpage
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="BookClient.MainPage"
Title="Library Books">
<ContentPage.ToolbarItems>
<ToolbarItem Text="Add New" Icon="ic_action_new.png" Clicked="OnAddNewBook" />
<ToolbarItem Text="Refresh" Icon="ic_autorenew.png" Clicked="OnRefresh" />
</ContentPage.ToolbarItems>
<ListView
ItemsSource="{Binding}"
ItemTapped="OnEditBook">
<ListView.ItemTemplate>
<DataTemplate>
<ImageCell
Text="{Binding id, StringFormat='ID= {0}'}" Detail="{Binding content}" ImageSource="{Binding images.image_intro}">
<ImageCell.ContextActions>
<MenuItem Clicked="OnDeleteBook"
CommandParameter="{Binding}"
Text="Delete" IsDestructive="True" />
</ImageCell.ContextActions>
</ImageCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage>
Looks like you have a couple problems, though I have not messed with infinite scrolling before. To answer your question, you do not need to bind your DataTemplate, it looks good exactly how you have it.
In your XAML you specify ItemsSource="{Binding}" but then in your code-behind you set listview.ItemsSource = books; which cancels out your original binding. I would suggest commenting out that line in your code-behind and leave in the XAML line.
You are not awaiting LoadItems();. You should move LoadItems(); into OnAppearing() so that it can be awaited.
Move the books property into your ViewModel and then have you ViewModel inherit from INotifyPropertyChanged and set your ViewModel as the BindingContext. This would then make your ListView.ItemSource change to ItemsSource="{Binding Books}". So your ViewModel will become
public class BooksViewModel : INotifyPropertyChanged {
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
private ObservableCollection<Article> _books;
public ObservableCollection<Article> Books {
get { return _books ?? (_books = new ObservableCollection<Article>()); }
set {
if(_books != value) {
_books = value;
OnPropertyChanged();
}
}
}
public async void LoadItems()
{
//simulator delayed load
var bookCollection = await manager.GetAll();
foreach (Article book in bookCollection.articles)
{
books.Add(book);
}
}
protected virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = null) {
System.ComponentModel.PropertyChangedEventHandler handler = PropertyChanged;
if(handler != null) { handler(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); }
}
}
Doing the above allows Books to notify the UI that it has changed so the UI will update. You also should be specifying Books as ObservableCollection<> instead of IList<>
A suggestion, I would attach a method to listview.ItemAppearing in ContentPage.OnAppearing() and then remove the event handler in ContentPage.OnDisappearing(). This will prevent memory leaks and should be done for any event handling that it makes sense to do it to. This will require you to put your ItemAppearing lambda code into its own method
Let me know if it is still not working for you after this.
Edit: Forgot about the ViewModel's PropertyChanged event. See ViewModel code again, at the top.
The event binding of Caliburn.Micro seems not to work with the ListPickerFlyout of Windows Phone 8.1. I want to bind the event ItemsPicked of the Flyout to the corresponding method of my ViewModel.
<ListView
x:Name="Links"
toolkitex:ListViewExtensions.BindableSelection="{Binding Selection}"
cm:Message.Attach="[Event ItemClick] = [Click($link)]">
<FlyoutBase.AttachedFlyout>
<ListPickerFlyout
SelectionMode="Single"
Placement="Full"
ItemsSource="{Binding Lists}"
SelectedItem="{Binding SelectedList, Mode=TwoWay}"
ctrls:FlyoutEx.Parent="{Binding ElementName=Links}"
ctrls:FlyoutEx.IsOpen="{Binding IsListSelectionOpen, Mode=TwoWay}"
cm:Message.Attach="[Event ItemsPicked] = [ItemsPicked($this, $eventArgs)]">
</ListPickerFlyout>
</FlyoutBase.AttachedFlyout>
</ListView>
When the event will be raised, I get the following exception: No target found for method ItemsPicked.
System.Exception: No target found for method ItemsPicked.
at Caliburn.Micro.ActionMessage.Invoke(Object eventArgs)
at Caliburn.Micro.TriggerAction`1.Execute(Object sender, Object parameter)
at Microsoft.Xaml.Interactivity.Interaction.ExecuteActions(Object sender,
ActionCollection actions, Object parameter)
at Microsoft.Xaml.Interactions.Core.EventTriggerBehavior.OnEvent(Object
sender, Object eventArgs)
I also tried it without event and method method parameters but it does not work either.
The problem occured becouse ListPickerFlyout doesn't have DataContext (or has wrong). I don't see a way to set the DataContext, but you can determine selected item by pinning to setter of your SelectedList property from ViewModel. For example if your SelectedList is type of string:
private string _selectedLists;
public string SelectedLists
{
get { return _selectedLists; }
set
{
_selectedLists = value;
ItemsPicked(value); // <----------------
NotifyOfPropertyChange(() => SelectedLists);
}
}
private void ItemsPicked(string selectedValue)
{
//Handle selection
}