MAUI: ListView Binding With Custom ViewCell - c#

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();
}

Related

.NET MAUI binding ItemSelected event of ListView to ViewModel

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
}
// ...
}

New elements added to ObservableCollection from external Page are not persisted

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("..");
}

Xamarin.Forms Image Source binding to string (MVVM)

I am intending to bind the source for an image from a URL I am loading in my ViewModel
In the xaml file, it works just fine if I use:
<Image Source="https://example.com/image.jpg"/>
However, it won't work if I use
ViewModel
public string Image = {get; set;}
Image = "https://example.com/image.jpg";
XAML - I am setting the BindingContext to the ViewModel
<Image Source="{Binding Image}"/>
Any ideas?
Thanks
The code below should work. I set the binding in Xaml.
Xaml:
<ContentPage.BindingContext>
<local:ViewModel></local:ViewModel>
</ContentPage.BindingContext>
<ContentPage.Content>
<StackLayout>
<Image Source="{Binding Image}"></Image>
</StackLayout>
</ContentPage.Content>
ViewModel:
public class ViewModel
{
public string Image { get; set; }
public ViewModel()
{
Image = "https://aka.ms/campus.jpg"; //"https://example.com/image.jpg"
}
}
Or you could set the binding in code behind:
Xaml:
<ContentPage.Content>
<StackLayout>
<Image Source="{Binding Image}"></Image>
</StackLayout>
</ContentPage.Content>
Code behind:
public partial class Page2 : ContentPage
{
public Page2()
{
InitializeComponent();
this.BindingContext = new ViewModel();
}
}
public class ViewModel
{
public string Image { get; set; }
public ViewModel()
{
Image = "https://aka.ms/campus.jpg"; //"https://example.com/image.jpg"
}
}

Why won't my Xamarin.Forms ListView Data Binding reflect in other Views?

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.

Xamarin Forms User Control Binding inside ListView

Scenario - There is a ListView binded to a ObservableCollection of string. Listview has one label and one UserControl (containing nothing but a label). Both are binded to the same collection.
Also, there is a button which generate some random data for the collection.
Problem is when I run the app and click on Generate Data button the label gets updated but not the UserControl.
Below is the sample code.
MainPage.xaml
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:TestSample"
xmlns:controls="clr-namespace:TestSample.Controls"
x:Class="TestSample.MainPage">
<StackLayout>
<Button Text="Generate Data" Clicked="Button_Clicked"/>
<ListView Grid.Row="1" HorizontalOptions="Center" ItemsSource="{Binding Collection}" SeparatorVisibility="None">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Horizontal" HorizontalOptions="Center">
<Label Text="{Binding}"/>
<Label Text=" - "/>
<controls:MagicBox Text="{Binding}"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
MainPage.xaml.cs
public partial class MainPage : ContentPage
{
public ObservableCollection<string> Collection { get; set; }
public MainPage()
{
InitializeComponent();
Collection = new ObservableCollection<string>
{
"XX",
"XX",
"XX"
};
this.BindingContext = this;
}
public void Button_Clicked(object sender, EventArgs e)
{
var rand = new Random();
for (int i = 0; i < 3; i++)
{
Collection[i] = rand.Next(10, 100).ToString();
}
}
}
UserControl
<ContentView.Content>
<Grid>
<Label Text="{Binding Text}" />
</Grid>
public partial class MagicBox : ContentView
{
public static readonly BindableProperty TextProperty =
BindableProperty.Create("Text", typeof(string), typeof(MagicBox), "XX");
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public MagicBox ()
{
InitializeComponent ();
this.BindingContext = this;
}
}
I also tried with ObservableCollection of a POCO class instead of string after implementing INotifyPropertyChanged, didn't worked.
If I bind the MagicBox Text to a string directly it works but not if I bind it to some property.
doing
this.BindingContext = this;
in MagicBox.xaml.cs forces the BindingContext to the current object. It also means that the BindingContext from the parent is no longer inherited.
in order to make it work, change your code behind to
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class MagicBox : ContentView
{
public static readonly BindableProperty TextProperty =
BindableProperty.Create("Text", typeof(string), typeof(MagicBox), default(string));
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public MagicBox ()
{
InitializeComponent ();
}
}
and your xaml to
<?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="TestSample.Controls.MagicBox"
x:Name="box">
<ContentView.Content>
<Grid>
<Label Text="{Binding Text, Source={x:Reference box}}" />
</Grid>
</ContentView.Content>
</ContentView>
I tested it. it works.
I think the problem is the line "this.BindingContext = this;" in your custom control.
You should Bind like this:
Text="{Binding Path=BindingContext, Source={x:Reference ListViewName}}"
Make sure add x:Name to your Listview. No tested, but hope it help you.
First, update your custom control:
Change your "Text" dependency property definition => Set the binding mode to "OneWay" and add propertyChanged event handler like this:
public partial class MagicBox : ContentView
{
public static readonly BindableProperty TextProperty =
BindableProperty.Create("Text", typeof(TextVM), typeof(MagicBox), "XX", BindingMode.OneWay, null, new BindableProperty.BindingPropertyChangedDelegate(TextPropertyChanged));
public TextVM Text
{
get { return (TextVM)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
Then add the 'Text' propertyChanged method into your custom control like this:
private static void TextPropertyChanged(BindableObject sender, object oldValue, object newValue )
{
Label updatedLabel = sender as Label;
if(updatedLabel == null) return;
updatedLabel.Text = (newValue as TextVM)?.MyText;
}
Make an Observable object that embed the text property, in order to throw the 'PropertyChanged' event:
public class TextVM : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _myText;
public string MyText
{
get => _myText;
set
{
_myText = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("MyText"));
}
}
}
Then in your XAML, update the text binding:
<controls:MagicBox Text="{Binding MyText}"/>
Don't forget to update your collection type and the random number generation process...
It should be good !

Categories

Resources