I walked into an issue where I wanted to use a DataTemplateSelector for a single item. In this GitHub issue I came accross an answer that extends a contentview and allows for this to happen. I now use this with two Data Templates. The bindingcontext I pass into these works great. But as soon as I want to use another bindingcontext it just says it can't find it.
My DataTemplate:
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:Universal_ONE.ViewModels">
<DataTemplate x:Key="AirpointSettingsDataTemplate">
<StackLayout Orientation="Horizontal"
x:Name="StackName"
Spacing="10">
<Label Text="TestAirpoint" TextColor="Black" FontSize="30" FontFamily="Roboto"/>
<Label Text="{Binding U}" TextColor="Black" FontSize="30" FontFamily="Roboto">
<Label.BindingContext>
<viewmodels:AirpointSettingsViewModel/>
</Label.BindingContext>
</Label>
</StackLayout>
</DataTemplate>
</ResourceDictionary>
The viewmodel:
public class AirpointSettingsViewModel : INotifyPropertyChanged
{
public AirpointSettingsViewModel()
{
Debug.WriteLine("ViewModel Airpoint settings");
}
private string u = "sometext";
public string U
{
get => u;
set
{
u = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(U)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
When putting breakpoints in the viewmodel it does also not hit those breakpoints.
I also tested with the viewmodel as a bindingcontext of the stacklayout, but that doesn't do anything either.
The error:
[0:] Binding: 'U' property not found on 'Universal_ONE.ViewModels.AirpointSettingsViewModel', target property: 'Xamarin.Forms.Label.Text'
I have also tested with a totally unrelated viewmodel on a random property. It still won't find it then. Thus the problem is not that it can't find the viewmodel or that it is faulty.
Related
I have attempted to make a simple EventToCommand behaviour on a XAML switch using the Maui Community ToolKit which is not working as expected.
I have tried to bind the event to a command in my code behind which originally worked when originally binded to a button command but it does not work with the switch. VisualStudio is unable to debug and stops the program when attempting to display each Frame in the CollectionView but I believe the error is due to incorrect Binding on the EventToCommandBehavior Tag.
XAML Code
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:model="clr-namespace:LightApp.Models"
x:Class="LightApp.MainPage"
xmlns:viewmodel="clr-namespace:LightApp.ViewModel"
x:DataType="viewmodel:DeviceViewModel"
>
<Grid>
<CollectionView ItemSource={Binding Devices}>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="model:Device">
<Frame>
<StackLayout>
<Grid>
<Label .../>
<Image .../>
// THIS WORKS
<Button Text="Power"
Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodel:DeviceViewModel}}, Path=CommandToCallCommand}" >
</Button>
// THIS DOESN'T
<Switch>
<Switch.Behaviors>
<toolkit:EventToCommandBehavior
EventName="Toggled"
Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodel:DeviceViewModel}}, Path=CommandToCallCommand}" >
</toolkit:EventToCommandBehavior>
</Switch.Behaviors>
</Switch>
<Closing Tags....>
DeviceViewModel.cs
usings...
namespace LightApp.ViewModel
{
public partial class DeviceViewModel : BaseViewModel
{
public ObservableCollection<Device> Devices { get; } = new();
public DeviceViewModel(DeviceService deviceService)
{
Title = "Devices";
this.deviceService = deviceService;
}
[RelayCommand]
void CommandToCall()
{
// command here
}
}
}
I have a simple view model where one property contains a model, and another property contains a list of models.
I am able to bind the "Test" model's properties without an issue but I'm not able to get the XAML to recognize that "ListModel" contains a list with its own properties. I have looked at several examples for how to set up the view model and initialize the list correctly before binding it to the view, and while the XAML understands that "ListModel" is a property, I can't get it to recognize that it's a list, and thus it will not compile so that I can at least see if it isn't the intellisense that could be failing for whatever reason.
This is the view model in question with the list named "ListModel"
public class TestViewModel
{
public TestModel Test { get; } = new TestModel();
public List<TestListModel> ListModel { get; set; }
public TestViewModel()
{
Initialize();
}
public void Initialize()
{
ListModel = new List<TestListModel>();
ListModel.Add(new TestListModel
{
ListProp1 = "First",
ListProp2 = "Second",
ListProp3 = "Third"
});
}
}
This is the Model that is being put into a list. It seems like the view isn't seeing these properties.
public class TestListModel
{
public string ListProp1 { get; set; }
public string ListProp2 { get; set; }
public string ListProp3 { get; set; }
}
This is my XAML Currently.
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MauiApp1.MainPage"
xmlns:local="clr-namespace:ViewModels"
x:DataType="local:TestViewModel"
>
<ScrollView>
<VerticalStackLayout
Spacing="25"
Padding="30,0"
VerticalOptions="Center">
<!--This works-->
<Entry Text="{Binding Test.Property1}"/>
<Entry Text="{Binding Test.Property2}"/>
<Entry Text="{Binding Test.Property3}"/>
<!--This does not work-->
<ListView
ItemsSource="{Binding ListModel}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding ListProp1}"/>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</VerticalStackLayout>
</ScrollView>
</ContentPage>
For anyone stumbling in on this: Jason in the comments has answered the question. The fix was simply to remove x:DataType from the top of the XAML, though I did not remove the "xmlns:local" from it.
What I had was a View Model that had more than just one model in it, which seemed to upset the intellisense when removing x:DataType. Removing it originally prevented the application from compiling because it couldn't find the properties I had in the XAML. Once I cleaned and rebuilt the solution it compiled and worked without a hitch.
The view and the viewmodel are often connected through data bindings defined in XAML. The BindingContext for the view is usually an instance of the viewmodel.So I think you forget to connect these two elements with BindingContext.
Code behind View:
public partial class MainPage : ContentPage
{
TestViewModel tv = new TestViewModel();
public MainPage()
{
InitializeComponent();
BindingContext = tv;
}
}
Code in Xaml:
<ScrollView>
<VerticalStackLayout
Spacing="25"
Padding="30,0"
VerticalOptions="Center">
<!--This works-->
<Entry Text="{Binding Test.Property1}"/>
<Entry Text="{Binding Test.Property2}"/>
<Entry Text="{Binding Test.Property3}"/>
<!--This works too-->
<ListView
HasUnevenRows="True"
ItemsSource="{Binding ListModel}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<VerticalStackLayout>
<Label Text="{Binding ListProp1}"/>
<Label Text="{Binding ListProp2}"/>
<Label Text="{Binding ListProp3}"/>
</VerticalStackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</VerticalStackLayout>
</ScrollView>
Outcome:
Reference link.
I was able to fix the error while also keeping the compiled bindings in place by making the ItemSource bind to a List with a private backing field. It seems like the compiler is unable to resolve the list correctly without the private property being there. After i added listModel it compiled. So the issue seems to be that the setter is missing.
private List<TestListModel> listModel;
public List<TestListModel> ListModel { get => listModel; set => listModel = value; }
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.
As the title suggests, I'm trying to pass in a Person object to a custom control instead of passing in each property separately.
So this:
<controls:PersonControl
Person="{Binding Person}"
ControlTemplate="{StaticResource PersonControlTemplate}">
</controls:PersonControl>
instead of this (which works, based on this implementation)
<controls:PersonControl
Name="{Binding Person.Name}"
Age="{Binding Person.Age}"
ControlTemplate="{StaticResource PersonControlTemplate}">
</controls:PersonControl>
I've tried changing the bindable property signature on the PersonControl code behind but it's not working. I actually just get a blank screen.
So:
1 - Is this even possible (i know it's called a bindable property but does it take objects as well?
and
2 - If not what is the recommended approach?
The reason I want to do this is the person object may grow over time and I would rather just update the custom control instead of the consuming page AND it's view model.
Update:
Here's the PersonControl Code:
public partial class PersonControl : ContentView
{
public static readonly BindableProperty PersonProperty = BindableProperty.Create(
nameof(Person),
typeof(Person),
typeof(PersonControl),
string.Empty);
public string Name
{
get { return this.Person.Name; }
}
public Person Person
{
get { return (Person)GetValue(PersonProperty); }
set { SetValue(PersonProperty, value); }
}
public PersonControl()
{
InitializeComponent();
}
}
And here's the PersonControl xaml:
<ContentView.Content>
<StackLayout>
<Label Text="{TemplateBinding Person.Name, Mode=OneWay}"/>
</StackLayout>
</ContentView.Content>
and lastly the consuming page:
<ContentPage.Resources>
<ControlTemplate x:Key="PersonControlTemplate">
<controls:PersonControl></controls:PersonControl>
</ControlTemplate>
</ContentPage.Resources>
<ContentPage.Content>
<StackLayout Spacing="10" x:Name="layout">
<controls:PersonControl
Person="{Binding Person}"
ControlTemplate="{StaticResource PersonControlTemplate}"></controls:PersonControl>
</StackLayout>
</ContentPage.Content>
The person object is a property on the page's viewmodel as per mvvm pattern.
Thanks in advance for your help.
Update: Ive followed this tutorial and tried to replace the bindable string type with an object but still no joy
Yes you can, I used a Entry, type some text, then transfer text to the Label in ContentView. Here is running GIF.
First of all I add a property called PersonName like following code
public partial class PersonControl : ContentView
{
public PersonControl()
{
InitializeComponent();
}
public static BindableProperty PersonNameProperty = BindableProperty.Create(
propertyName: "PersonName",
returnType: typeof(string),
declaringType: typeof(PersonControl),
defaultValue: "",
defaultBindingMode: BindingMode.OneWay);
public string PersonName
{
get { return (string)GetValue(PersonNameProperty); }
set { SetValue(PersonNameProperty, value); }
}
}
Then, here is code about layout of ContentView. Please do not forget to add x:Name="ParentControl" in ContentView tab.
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Name="ParentControl"
x:Class="CustomDemoControl.PersonControl">
<ContentView.Content>
<StackLayout>
<Label Text="{Binding Source={x:Reference ParentControl}, Path=PersonName}"/>
</StackLayout>
</ContentView.Content>
</ContentView>
Then I use it in the other page.
<StackLayout>
<Entry x:Name="myEntry" Text="1"/>
<local:PersonControl
BindingContext="{x:Reference Name=myEntry}"
PersonName="{Binding Path=Text, StringFormat='Welcome Mr {0}'}"
></local:PersonControl>
</StackLayout>
So, it turns out the answer was to pass a default object in the bindable property signature (in the custom control's cs) like so:
public static readonly BindableProperty PersonProperty =
BindableProperty.Create(
nameof(Person),
typeof(Person),
typeof(PersonProperty ),
default(Person)); //this was the fix
Now, I missed this because I wasn't aware of errors in the control. It just wasn't showing up in the output window. Can anyone point me in the direction of how to comprehensively debug xamarin.forms apps properly? Or at least catch these kind of errors in future?
Thank you
I am having trouble binding to a property in Xamarin, and can't figure it out using Microsoft's documentation for some reason.
Say I have this viewmodel:
public class FooViewModel
{
public IEnumerable<string> Foos { get; set; }
public string SpecialFoo { get; set; }
}
And this in my view:
<StackLayout BindableLayout.ItemsSource="{Binding Foos}">
<BindableLayout.ItemTemplate>
<DataTemplate>
<Label Text="{Binding SpecialFoo}"/>
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
I am running into the problem that in the DataTemplate, I can't make the binding to a property inside FooViewModel. How do I make this binding to SpecialFoo?
That's easy all you need is a reference to your page and tell it that look for this in the VM and not in the model:
Give your current ContentPage a name :
<ContentPage
...
x:Name="currentPage"/>
Then your label would look something like:
<Label Text="{Binding BindingContext.SpecialFoo, Source={x:Reference currentPage}}"/>