I am trying to bind the ItemSelected of a ListView to a View Model, but am experiencing some issues (due to my own misunderstands around how it all works).
I have view:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:Local="clr-namespace:FireLearn.ViewModels"
x:Class="FireLearn.MainPage"
Title="Categories">
<ContentPage.BindingContext>
<Local:CategoryViewModel/>
</ContentPage.BindingContext>
<NavigationPage.TitleView>
<Label Text="Home"/>
</NavigationPage.TitleView>
<ListView
ItemsSource="{Binding Categories}"
HasUnevenRows="True"
IsPullToRefreshEnabled="True"
IsRefreshing="{Binding ListRefreshing, Mode=OneWay}"
RefreshCommand="{Binding RefreshCommand}"
ItemSelected="{Binding OnItemTappedChanged}"
SelectionMode="Single"
SelectedItem="{Binding SelectedCategory}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<HorizontalStackLayout
Padding="8"
VerticalOptions="Fill"
HorizontalOptions="Fill">
<Image Source="cafs_bubbles.png"
HeightRequest="64"
MaximumWidthRequest="64"
HorizontalOptions="CenterAndExpand"
VerticalOptions="CenterAndExpand"/>
<VerticalStackLayout
Padding="8"
VerticalOptions="FillAndExpand"
HorizontalOptions="FillAndExpand">
<Label Text="{Binding FormattedName}"
SemanticProperties.HeadingLevel="Level1"
FontSize="Title"
HorizontalOptions="Start"/>
<Label Text="{Binding ItemCount}"
FontSize="Subtitle"/>
<Label Text="{Binding Description}"
HorizontalOptions="Center"
LineBreakMode="WordWrap"
FontSize="Caption"
VerticalOptions="CenterAndExpand"
MaxLines="0"/>
</VerticalStackLayout>
</HorizontalStackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage>
This is linked to a view model:
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FireLearn.Models;
namespace FireLearn.ViewModels
{
public partial class CategoryViewModel : ObservableObject
{
public ObservableCollection<CategoryModel> categories = new ObservableCollection<CategoryModel>();
public ObservableCollection<CategoryModel> Categories
{
get => categories;
set => SetProperty(ref categories, value);
}
public bool listRefreshing = false;
public bool ListRefreshing
{
get => listRefreshing;
set => SetProperty(ref listRefreshing, value);
}
public CategoryModel selectedCategory = new CategoryModel();
public CategoryModel SelectedCategory
{
get => selectedCategory;
set
{
SetProperty(ref selectedCategory, value);
// Tap(value);
}
}
public RelayCommand RefreshCommand { get; set; }
//public RelayCommand TapCellCommand { get; set; }
public CategoryViewModel()
{
loadFromSource();
RefreshCommand = new RelayCommand(async () =>
{
Debug.WriteLine($"STARTED::{ListRefreshing}");
if (!ListRefreshing)
{
ListRefreshing = true;
try
{
await loadFromSource();
}
finally
{
ListRefreshing = false;
Debug.WriteLine($"DONE::{ListRefreshing}");
}
}
});
}
public async Task loadFromSource()
{
HttpClient httpClient = new()
{
Timeout = new TimeSpan(0, 0, 10)
};
Uri uri = new Uri("https://somewebsite.co.uk/wp-json/wp/v2/categories");
HttpResponseMessage msg = await httpClient.GetAsync(uri);
if (msg.IsSuccessStatusCode)
{
var result = CategoryModel.FromJson(await msg.Content.ReadAsStringAsync());
Categories = new ObservableCollection<CategoryModel>(result);
}
Debug.WriteLine("List Refreshed");
}
public void OnItemTappedChanged(System.Object sender, Microsoft.Maui.Controls.SelectedItemChangedEventArgs e)
{
var x = new ShellNavigationState();
Shell.Current.GoToAsync(nameof(NewPage1),
new Dictionary<string, object>
{
{
nameof(NewPage1),
SelectedCategory
}
});
}
}
}
I get compiler error "No property, BindableProperty, or event found for "ItemSelected", or mismatching type between value and property" and am really unsure of how to resolve. If I let XAML create a new event for me, it adds it in MainPage.Xaml.Cs rather than the VM
ItemSelected expects an event handler which usually only exists in the View's code behind. Since the ViewModel shouldn't know anything about the View, it's better not to mix concepts. You have a couple of options to get around this without breaking the MVVM pattern.
Option 1: Use Event Handler and invoke method of ViewModel
First, set up the code behind with the ViewModel by passing it in via the constructor and also add the event handler, e.g.:
public partial class MainPage : ContentPage
{
private CategoryViewModel _viewModel;
public MainPage(CategoryViewModel viewModel)
{
_viewModel = viewModel;
}
public void OnItemSelectedChanged(object sender, SelectedItemChangedEventArgs e)
{
//call a method from the ViewModel, e.g.
_viewModel.DoSomething(e.SelectedItem);
}
//...
}
Then use the event handler from within the XAML:
<ListView
ItemsSource="{Binding Categories}"
HasUnevenRows="True"
IsPullToRefreshEnabled="True"
IsRefreshing="{Binding ListRefreshing, Mode=OneWay}"
RefreshCommand="{Binding RefreshCommand}"
ItemSelected="OnItemSelectedChanged"
SelectionMode="Single"
SelectedItem="{Binding SelectedCategory}">
<!-- skipping irrelevant stuff -->
</ListView>
Mind that this does not use bindings.
In your CategoryViewModel you could then define a method that takes in the selected item as an argument:
public partial class CategoryViewModel : ObservableObject
{
//...
public void DoSomething(object item)
{
//do something with the item, e.g. cast it to Category
}
}
Option 2: Use EventToCommandBehavior
Instead of handling the invocation of a ViewModel method from your code behind, you could also use the EventToCommandBehavior from the MAUI Community Toolkit:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:Local="clr-namespace:FireLearn.ViewModels"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
x:Class="FireLearn.MainPage"
Title="Categories">
<ContentPage.Resources>
<ResourceDictionary>
<toolkit:SelectedItemEventArgsConverter x:Key="SelectedItemEventArgsConverter" />
</ResourceDictionary>
</ContentPage.Resources>
<ListView
ItemsSource="{Binding Categories}"
HasUnevenRows="True"
IsPullToRefreshEnabled="True"
IsRefreshing="{Binding ListRefreshing, Mode=OneWay}"
RefreshCommand="{Binding RefreshCommand}"
SelectionMode="Single"
SelectedItem="{Binding SelectedCategory}">
<ListView.Behaviors>
<toolkit:EventToCommandBehavior
EventName="ItemSelected"
Command="{Binding ItemSelectedCommand}"
EventArgsConverter="{StaticResource SelectedItemEventArgsConverter}" />
</ListView.Behaviors>
<!-- skipping irrelevant stuff -->
</ListView>
</ContentPage>
Then, in your ViewModel, you can define the ItemSelectedCommand:
public partial class CategoryViewModel : ObservableObject
{
[RelayCommand]
private void ItemSelected(object item)
{
//do something with the item, e.g. cast it to Category
}
// ...
}
This is the preferred way to do it. Option 1 is just another possiblity, but the EventToCommandBehavior is the better choice.
Note that this is an example using MVVM Source Generators (since you're already using the MVVM Community Toolkit). The full Command would normally be implemented like this:
public partial class CategoryViewModel : ObservableObject
{
private IRelayCommand<object> _itemSelectedCommand;
public IRelayCommand<object> ItemSelectedCommand => _itemSelectedCommand ?? (_itemSelectedCommand = new RelayCommand<object>(ItemSelected));
private void ItemSelected(object item)
{
//do something with the item, e.g. cast it to Category
}
// ...
}
Related
I am trying to create a ListView that supports the pull to refresh feature of ListView.
My XAML is:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:Local="clr-namespace:FireLearn.ViewModels"
x:Class="FireLearn.MainPage"
Title="Categories">
<ContentPage.BindingContext>
<Local:CategoryViewModel/>
</ContentPage.BindingContext>
<ListView
ItemsSource="{Binding Categories}"
ItemSelected="ListView_ItemSelected"
HasUnevenRows="True"
IsPullToRefreshEnabled="True"
IsRefreshing="{Binding ListRefreshing}"
RefreshCommand="{Binding RefreshCommand}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<HorizontalStackLayout
Padding="8">
<Image Source="cafs_bubbles.png"
HeightRequest="64"/>
<VerticalStackLayout
Padding="8">
<Label Text="{Binding FormattedName}"
SemanticProperties.HeadingLevel="Level1"
FontSize="Title"
HorizontalOptions="Start"/>
<Label Text="{Binding ItemCount}"
FontSize="Subtitle"/>
<Label Text="{Binding Description}"
HorizontalOptions="Center"
LineBreakMode="WordWrap"
FontSize="Caption"
VerticalOptions="CenterAndExpand"/>
</VerticalStackLayout>
</HorizontalStackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage>
and the code behind is:
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using CommunityToolkit.Mvvm.ComponentModel;
using FireLearn.Models;
namespace FireLearn.ViewModels
{
class CategoryViewModel : ObservableObject
{
public ObservableCollection<CategoryModel> categories = new ObservableCollection<CategoryModel>();
public ObservableCollection<CategoryModel> Categories
{
get => categories;
set => SetProperty(ref categories, value);
}
public bool ListRefreshing = false;
public ICommand RefreshCommand;
public CategoryViewModel()
{
loadFromSource();
RefreshCommand = new Command(async () =>
{
if (!ListRefreshing)
{
ListRefreshing = true;
try
{
await loadFromSource();
}
finally
{
ListRefreshing = false;
}
}
});
}
public async Task loadFromSource()
{
HttpClient httpClient = new()
{
Timeout = new TimeSpan(0,0,10)
};
Uri uri = new Uri("https://somewodpresssite/wp-json/wp/v2/categories");
HttpResponseMessage msg = await httpClient.GetAsync(uri);
if (msg.IsSuccessStatusCode)
{
var result = CategoryModel.FromJson(await msg.Content.ReadAsStringAsync());
Categories = new ObservableCollection<CategoryModel>(result);
}
Debug.WriteLine("List Refreshed");
}
}
}
The binding of the list works great and items show as expected (based on loadFromSource() being called in the constructor but the ICommand doesnt get called when I pull to refresh and I get the following error around the IsRefreshing binding:
[0:] Microsoft.Maui.Controls.Xaml.Diagnostics.BindingDiagnostics:
Warning: 'ListRefreshing' property not found on
'FireLearn.ViewModels.CategoryViewModel', target property:
'Microsoft.Maui.Controls.ListView.IsRefreshing'
I have tried on Android and iOs with the same result and am at a loss as to what I've missed here.
You can only bind to public properties
public bool ListRefreshing { get; set; }
I use FreshMvvm to develop and run MAUI project on Windows.
But I have some binding issues with ListView and my custom template.
The following is my code:
Model:
public class BaseModel
{
public string Code{ get; set; }
}
public class NameModel: BaseModel
{
public string Name{ get; set; }
}
ViewModel:
public class MainPageModel : FreshBasePageModel
{
private readonly IApiService _apiService;
private List<NameModel> _nameModelList;
public List<NameModel> NameModelList
{
get => _nameModelList;
private set
{
_nameModelList= value;
RaisePropertyChanged(nameof(NameModelList));
}
}
public MainPageModel(IApiService apiService)
{
_apiService = apiService;
}
protected override void ViewIsAppearing(object sender, EventArgs e)
{
base.ViewIsAppearing(sender, e);
Task.Run(() => GetNameData());
}
private async Task GetNameData()
{
var result = await _apiService.GetNameData();
NameModelList= result.GetRange(1, 10);
}
}
I create a list and use an api service to get a name model list data.
If api service gets the data, NameModelList will be updated.
NameModelList is the property which will be bind on Listview.ItemsSource
MainPage.xmal:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MyNamespace.ViewCells.CustomListViewCell"
x:Class="MyNamespace.Pages.MainPage"
BackgroundColor="{DynamicResource SecondaryColor}">
<Grid RowSpacing="25"
RowDefinitions="Auto"
VerticalOptions="FillAndExpand"
HorizontalOptions="FillAndExpand">
<ListView
x:Name="MyListView"
ItemsSource="{Binding NameModelList}"
Grid.Row="0"
WidthRequest="800"
HeightRequest="800"
BackgroundColor="Gray"
VerticalOptions="FillAndExpand"
HorizontalOptions="FillAndExpand">
<ListView.ItemTemplate>
<DataTemplate>
<local:MyCustomViewCell/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</ContentPage>
Custom ViewCell (.xml):
<ViewCell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyNamespace.ViewCells.CustomListViewCell.MyCustomViewCell">
<Grid RowSpacing="100" WidthRequest="100" HeightRequest="100">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100*" />
</Grid.ColumnDefinitions>
<StackLayout
GridLayout.Row="0"
GridLayout.Column="0">
<Label
Text="{Binding Code}"
FontSize="30"/>
<Label
Text="{Binding Name}"
FontSize="30"/>
</StackLayout>
</Grid>
</ViewCell>
Custom ViewCell (.cs)
public partial class MyCustomViewCell: ViewCell
{
public static readonly BindableProperty CodeProperty =
BindableProperty.Create("Code", typeof(string), typeof(MyCustomViewCell), "");
public string Code
{
get { return (string)GetValue(CodeProperty); }
set { SetValue(CodeProperty, value); }
}
public static readonly BindableProperty NameProperty =
BindableProperty.Create("Name", typeof(string), typeof(MyCustomViewCell), "");
public string Name
{
get { return (string)GetValue(NameProperty); }
set { SetValue(NameProperty, value); }
}
}
I define a custom ViewCell files and put this ViewCell in the Listview of MainPage.
Now my question is my Listview can't show data successfully.
I'm sure that NameModelList has value and its count is more than 1.
But I can see nothing.
The output log has no error, and the breakpoints in MyCustomViewCell.cs are never triggered.
So I think I have some binding issues, but I can't find it out.
To get to the bottom of this I took your code and put it in a project so I could have a little play with it. You can find the repo here. Not to be rude here or anything, but might be a good idea for a next question to do that yourself, that will help speed things up :)
Anyway, the problem is much more subtle. Because you're using XAML for your layout, you'll have to call InitializeComponent in the constructor. So adding this to your MyCustomViewCell made it work:
public MyCustomViewCell()
{
InitializeComponent();
}
I have two pages. On the first I want to see every elements from ObservableCollection and the second I want to add new element to ObservableCollection. When I give breakpoint I see that the ObservableCollection has new element, but when I back to the first page new element dosen't exist. What should I do?
There is code
public ListViewFlascardViewModel()
{
GoToAddFlashcard = new Command(goToAddFlashcard);
Fiszka = new ObservableCollection<Flashcard>();
Fiszka.Add(new Flashcard {Name = "hello" });
}
public Command GoToAddFlashcard { get; }
Flashcard flashcard = new Flashcard();
async void goToAddFlashcard()
{
await Shell.Current.GoToAsync(nameof(View.NoweFiszki));;
}
public ObservableCollection<Flashcard> Fiszka { get; set; }
}
And there is second page:
class NoweFiszkiViewModel
{
public Command SaveFlashcard { get; }
public NoweFiszkiViewModel()
{
SaveFlashcard = new Command(save);
}
ListViewFlascardViewModel list = new ListViewFlascardViewModel();
private async void save()
{
list.Fiszka.Add(new Flashcard { Name = "Bye" });
await Shell.Current.GoToAsync("..");
}
}
I try a lot of things, but nothing help. I am new in C# and I will be appreciated for every help.
I add whole code.
There is view
ListViewFlashcard
<ContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:MVVM.ViewModel"
xmlns:model="clr-namespace:MVVM.Model"
x:Class="MVVM.View.ListViewFlashcard"
x:DataType="viewmodels:ListViewFlascardViewModel"
>
<ContentPage.BindingContext>
<viewmodels:ListViewFlascardViewModel/>
</ContentPage.BindingContext>
<StackLayout>
<Button
Command="{Binding GoToAddFlashcard}"
Text="Click Me"/>
<ListView
ItemsSource="{Binding Fiszka}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="model:Flashcard">
<TextCell Text="{Binding Name}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
NoweFiszki
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:MVVM.ViewModel"
x:Class="MVVM.View.NoweFiszki">
<ContentPage.BindingContext>
<viewmodels:NoweFiszkiViewModel/>
</ContentPage.BindingContext>
<ContentPage.Content>
<StackLayout>
<Entry Text="{Binding Text, Mode=TwoWay}"/>
<Button Text="Save"
Command="{Binding SaveFlashcard}"/>
</StackLayout>
</ContentPage.Content>
And Model
Flashcard
public class Flashcard
{
public string Name { get; set; }
}
And AppShell
<Shell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MVVM.AppShell"
xmlns:local="clr-namespace:MVVM.View"
>
<FlyoutItem Title="Add Flashcard" Icon="icon_about.png">
<ShellContent ContentTemplate="{DataTemplate local:ListViewFlashcard}"/>
</FlyoutItem>
public partial class AppShell : Xamarin.Forms.Shell
{
public AppShell()
{
InitializeComponent();
Routing.RegisterRoute(nameof(NoweFiszki), typeof(NoweFiszki));
}
}
It is everything what I wrote.
A couple of things have to be corrected on your code:
In NoweFiszkiViewModel you are creating a new instance of ListViewFlascardViewModel every time, and adding elements on that new instance, which does not affect the instance to which ListViewFlashcard is actually bound.
To fix this, you will have to create a public static ListViewFlascardViewModel in ListViewFlashcard.xaml.cs and set the binding context to it, as follows
ListViewFlashcard.xaml.cs
public static ListViewFlascardViewModel vm { get; set; }
public ListViewFlashcard()
{
InitializeComponent();
vm = new ListViewFlascardViewModel();
}
protected override void OnAppearing()
{
base.OnAppearing();
BindingContext = vm;
}
then correct ListViewFlashcard.xaml as follows
ListViewFlashcard.xaml
<!--<ContentPage.BindingContext>
<viewmodels:ListViewFlascardViewModel/>
</ContentPage.BindingContext>-->
<StackLayout>
<Button
Command="{Binding GoToAddFlashcard}"
Text="Click Me"/>
<ListView ItemsSource="{Binding Fiszka}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="viewmodels:Flashcard">
<TextCell Text="{Binding Name}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
Finally you have to correct NoweFiszkiViewModel.cs as follows, adding the new elements to the public static view model:
NoweFiszkiViewModel.cs
public Command SaveFlashcard { get; }
public NoweFiszkiViewModel()
{
SaveFlashcard = new Command(save);
}
//ListViewFlascardViewModel list = new ListViewFlascardViewModel();
private async void save()
{
ListViewFlashcard.vm.Fiszka.Add(new Flashcard { Name = "Bye" });
await Shell.Current.GoToAsync("..");
}
My MainPage.xaml page is bound to ClientsViewModel.cs. This page has a ListView bound to an ObservableCollection property.
The NewClient.xaml page and entry fields are also bound to the ClientsViewModel.cs.
When I save a new client using the NewClient.xaml form and navigate back to MainPage.xaml (using the navigation back arrow) I expect to see the newly added client in the MainPage.xaml ListView however I do not see this change.
How come the ListView in MainPage.xaml isn't showing the newly updated record? Where am I going wrong?
It may be worthwhile mentioning that my actual project will be using SQLite, so the ObseravbleCollection will eventually be obtaining records directly from an SQLite database, so any help or advice around this would be greatly appreciated also.
Refer below code, or clone from my GitHub repository https://github.com/minlopalis/XamarinForms-ListView-DataBinding.git
(Model) Client.cs
public class Client
{
public int Id { get; set; }
public string Name { get; set; }
public string Phone { get; set; }
}
(ViewModel) BaseViewModel.cs
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
(View Model) ClientViewModel.cs
public class ClientViewModel : BaseViewModel
{
private ObservableCollection<Client> clients;
public ObservableCollection<Client> Clients
{
get { return clients; }
set
{
clients = value;
OnPropertyChanged();
}
}
public Command SaveClientCommand { get; }
public ClientViewModel()
{
this.Clients = new ObservableCollection<Client>();
SaveClientCommand = new Command(()=> {
Client client = new Client()
{
Name = Name,
Phone = Phone
};
Clients.Add(client);
OnPropertyChanged(nameof(Clients));
});
}
private int id;
public int Id
{
get { return id; }
set
{
id = value;
OnPropertyChanged();
}
}
private string name;
public string Name
{
get { return name; }
set
{
name = value;
OnPropertyChanged();
}
}
private string phone;
public string Phone
{
get { return phone; }
set
{
phone = value;
OnPropertyChanged();
}
}
}
(View) MainPage.xaml
<?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:viewModels="clr-namespace:DataBinding.ViewModels"
x:Class="DataBinding.MainPage">
<ContentPage.BindingContext>
<viewModels:ClientViewModel/>
</ContentPage.BindingContext>
<StackLayout>
<Label Text="Client List"></Label>
<ListView ItemsSource="{Binding Clients}">
<ListView.ItemTemplate>
<DataTemplate>
<StackLayout>
<Label Text="{Binding Name}"/>
<Label Text="{Binding Phone}"/>
</StackLayout>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Button Text="Add Client"
Clicked="AddClientButton_Clicked"/>
</StackLayout>
</ContentPage>
(View) NewClient.xaml
<?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:viewModels="clr-namespace:DataBinding.ViewModels"
x:Class="DataBinding.Views.NewClient">
<ContentPage.BindingContext>
<viewModels:ClientViewModel/>
</ContentPage.BindingContext>
<ContentPage.Content>
<StackLayout>
<Label Text="Add New Client" />
<Label Text="Name"/>
<Entry Text="{Binding Name}"/>
<Label Text="Phone"/>
<Entry Text="{Binding Phone}"/>
<Button Text="Save"
Command="{Binding SaveClientCommand}"/>
<!-- Added ListView -->
<ListView ItemsSource="{Binding Clients}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout>
<Label Text="{Binding Name}"/>
<Label Text="{Binding Phone}"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage.Content>
</ContentPage>
I've downloaded your code from the repo and I think there is one big flaw in it causing this. You're setting your BindingContext in XAML on both pages. If you set a breakpoint in the constructor of the ClientViewModel, you will notice it gets called twice: once when the app boots, once when you click "Add Client".
This means you are looking at two separate instances of this class so your Client is in the wrong instance. You want to make sure that you are looking at the same view model.
Even more so, you might even want to make the separation of concerns even better by creating an extra, i.e.: CreateClientViewModel which is only responsible for creating the client and returning that object to the ClientViewModel which then in its turn adds that to the collection.
Hope this helps!
According to your description, you want to pass data when navigate between pages, I suggest you can use MessagingCenter.
MainPage:
<StackLayout>
<Label Text="Client List" />
<ListView ItemsSource="{Binding Clients}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout>
<Label Text="{Binding Name}" />
<Label Text="{Binding Phone}" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Button Command="{Binding SaveClientCommand}" Text="Add Client" />
</StackLayout>
public partial class Page9 : ContentPage
{
private ClientViewModel _clientmodel;
public ClientViewModel clientmodel
{
get { return _clientmodel; }
set
{
_clientmodel = value;
}
}
public Page9()
{
InitializeComponent();
this.BindingContext = new ClientViewModel(this.Navigation);
}
}
public class ClientViewModel
{
public ObservableCollection<Client> Clients { get; set; }
public Command SaveClientCommand { get; }
private INavigation _navigation;
public ClientViewModel(INavigation navitation)
{
Clients = new ObservableCollection<Client>();
Clients.Add(new Client() { Name = "client1", Phone = "123" });
_navigation = navitation;
SaveClientCommand = new Command(async() => {
await _navigation.PushAsync(new NewClient());
});
MessagingCenter.Subscribe<string, string[]>("test", "Add", (sender, values) =>
{
Client client = new Client() { Name=values[0],Phone=values[1]};
Clients.Add(client);
});
}
}
NewClient.xaml:
public partial class NewClient : ContentPage
{
public NewClient()
{
InitializeComponent();
}
private void Button_Clicked(object sender, EventArgs e)
{
string name = entry1.Text;
string phone = entry2.Text;
string[] values = { name,phone};
MessagingCenter.Send<string, string[]>("test", "Add", values);
Navigation.PopAsync();
}
}
By the way, you don't need to call PropertyChanged for ObservableCollection, because ObservableCollection Class Represents a dynamic data collection that provides notifications when items get added, removed, or when the whole list is refreshed.
Thanks for everyone's help, I have solved my issue.
There were two problems with my code.
1. Two ViewModel Instances
As pointed out by Gerald Versluis I had two instances of my ViewModel. I fixed this issue by creating an instance of my view model in Application.Resources in my App.xaml page.
<?xml version="1.0" encoding="utf-8" ?>
<Application xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="DataBinding.App"
xmlns:ClientViewModel="clr-namespace:DataBinding.ViewModels">
<Application.Resources>
<ClientViewModel:ClientViewModel x:Key="ClientViewModel" />
</Application.Resources>
</Application>
And binding each page to the Static Resource (as below)
<?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:ViewModels="clr-namespace:DataBinding.ViewModels"
x:Class="DataBinding.Views.NewClient">
<ContentPage.BindingContext>
<StaticResource Key="ClientViewModel"/>
</ContentPage.BindingContext>
<ContentPage.Content>
<StackLayout>
<Label Text="Add New Client" />
<Label Text="Name"/>
<Entry Text="{Binding Name}"/>
<Label Text="Phone"/>
<Entry Text="{Binding Phone}"/>
<Button Text="Save"
Command="{Binding SaveClientCommand}"/>
<!-- Added ListView -->
<ListView ItemsSource="{Binding ClientList}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout>
<Label Text="{Binding Name}"/>
<Label Text="{Binding Phone}"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage.Content>
</ContentPage>
Thanks Gerald Versluis for your help. Check out his YouTube channel here.
2. Missing ViewCell
My MainPage.xaml was missing a ViewCell in the ListView. This was a simple typing oversight but was throwing a "'Specified cast is not valid" error. Big thanks to Alexander Fauland for his reply to this thread which helped me solve my missing ViewCell problem.
I'm trying to handle something similar (from UI perspective), to:
in order to invoke two different business logics for:
tapping at ViewCell element itself (inside ListView) - in example navigate to different page
tapping at Label element (Clickable Label), which is inside given ViewCell element - in example delete given object or smth else
I would like to have whole "tapping" logic inside page ViewModel.
Based on Xamarin forum proposes, I'm able to invoke some logic of "tapping" my delete action from cell, however directly inside my data model - which in my PoV is not good solution, as I would like to manipulate my List collection (so the most preferable way, would be to have this logic at page ViewModel).
What I have right now:
My page View XAML code looks like:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="App.Views.TestView">
<ContentPage.Content>
<StackLayout HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
<ListView HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" ItemsSource="{Binding MyItemsCollection}" SelectedItem="{Binding SelectedItem}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Horizontal">
<!-- Name Label -->
<Label Text="{Binding Name}" VerticalOptions="CenterAndExpand" HorizontalOptions="StartAndExpand" />
<!-- Delete "Icon" -->
<Label Text="Clickable Label" VerticalOptions="CenterAndExpand" HorizontalOptions="EndAndExpand">
<Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding OnClickableLabel}" CommandParameter="{Binding .}" />
</Label.GestureRecognizers>
</Label>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage.Content>
</ContentPage>
My page View C# code looks like (not specific code there, except binding **BindingContext* to page ViewModel):
public partial class TestView : ContentPage
{
public TestView()
{
InitializeComponent();
BindingContext = ServiceLocator.Current.GetInstance<TestViewModel>();
}
}
My page ViewModel C# code looks like:
public class TestViewModel : ViewModelBase
{
public TestViewModel()
{
MyItemsCollection = GetMyItemsCollection();
}
private List<MyItem> GetMyItemsCollection()
{
return new List<MyItem>
{
new MyItem
{
ID = 1L,
Name = "Item 1 Name"
},
new MyItem
{
ID = 2L,
Name = "Item 2 Name"
},
new MyItem
{
ID = 3L,
Name = "Item 3 Name"
}
};
}
private List<MyItem> _myItemsCollection { get; set; }
public List<MyItem> MyItemsCollection
{
get
{
return _myItemsCollection;
}
set
{
_myItemsCollection = value;
RaisePropertyChanged();
}
}
private MyItem _SelectedItem { get; set; }
public MyItem SelectedItem
{
get
{
return _SelectedItem;
}
set
{
if (_SelectedItem != value)
{
_SelectedItem = value;
RaisePropertyChanged();
Debug.WriteLine("SelectedItem: " + _SelectedItem.Name);
}
}
}
private RelayCommand<object> _OnClickableLabel;
public RelayCommand<object> OnClickableLabel
{
get { return _OnClickableLabel ?? (_OnClickableLabel = new RelayCommand<object>((currentObject) => Test(currentObject))); }
}
private void Test(object currentObject)
{
Debug.WriteLine("This should work... but it's not working :(");
}
}
My data model code looks like:
public class MyItem
{
public long ID { get; set; }
public string Name { get; set; }
private RelayCommand<object> _OnClickableLabel;
public RelayCommand<object> OnClickableLabel
{
get { return _OnClickableLabel ?? (_OnClickableLabel = new RelayCommand<object>((currentObject) => Test(currentObject))); }
}
private void Test(object currentObject)
{
Debug.WriteLine("This works... but it's not good idea, to have it here...");
}
}
Any idea what needs to be changed, in order to invoke OnClickableLabel directly inside my page ViewModel ?
I know, that it's something wrong at:
<TapGestureRecognizer Command="{Binding OnClickableLabel}" CommandParameter="{Binding .}" />
but don't know what :/.
Help! Thanks a lot.
Ok, I found solution, by extending XAML code:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="App.Views.TestView" x:Name="Page">
<ContentPage.Content>
<StackLayout HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
<ListView HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" ItemsSource="{Binding MyItemsCollection}" SelectedItem="{Binding SelectedItem}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Horizontal">
<!-- Name Label -->
<Label Text="{Binding Name}" VerticalOptions="CenterAndExpand" HorizontalOptions="StartAndExpand" />
<!-- Delete "Icon" -->
<Label Text="Clickable Label" VerticalOptions="CenterAndExpand" HorizontalOptions="EndAndExpand">
<Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding Path=BindingContext.OnClickableLabel, Source={x:Reference Page}}" CommandParameter="{Binding .}" />
</Label.GestureRecognizers>
</Label>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage.Content>
</ContentPage>
After that, I got OnClickableLabel command invoked inside my page ViewModel, as expected :).
If someone know "better" solution (better from XAML code point of view), I would like to see it ;).
Thanks a lot everyone!
continuing with what #Namek said i would suggest to get the object of list view item first and then call the command or viewmodel method.
for more you can refer my blog post about interactive Listview at https://adityadeshpandeadi.wordpress.com/2018/07/15/the-more-interactive-listview/
feel free to drop by. :)