Xamarin.Forms: How to set BindingContext inside XAML? - xaml

I have the following page:
<?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:pages="clr-namespace:XamFormsBle.Pages;assembly=XamFormsBle"
x:Name="ContentPageContainer"
x:Class="XamFormsBle.Pages.ConnectPage">
<!--This does not work-->
<ContentPage.BindingContext>
<pages:ConnectPage/>
</ContentPage.BindingContext>
<StackLayout Orientation="Vertical">
<Button Text="Refresh"
Clicked="RefreshDevicesList"/>
<ListView ItemsSource="{Binding DevicesList}"/>
</StackLayout>
</ContentPage>
And the code behind:
public partial class ConnectPage : ContentPage
{
public ObservableCollection<string> DevicesList { get; set; }
public ConnectPage()
{
InitializeComponent();
DevicesList = new ObservableCollection<string>();
//BindingContext = this;
}
public void RefreshDevicesList(object sender, EventArgs e)
{
DevicesList.Add("device");
}
}
What I am trying to achieve is to bind the ListView to the DevicesList. It works when I uncomment the BindingContext line in the constructor. I want to move that line into the .xaml itself. Researching the matter leads to the ContentPage.BindingContext block in the .xaml, but that crashes the program. There also seems to be the approach of setting the Source inside the binding of the ListView's ItemsSource, but I don't understand the syntax to work it in my case (I'm new to Xamarin.Forms and XAML in general). Is there a way to set the BindingContext inside .xaml?

You're using MVVM wrong. You're trying to set viewmodel to the view itself. Create a seperate class for the viewmodel, and it shouldn't be deriving from a ContentPage as you did.

Related

Can't bind property for ContentView control

I have a ContentView called HomePageOrientationViewLoader that I want to use in a ContentPage called HomePage. HomePageOrientationViewLoader will either load a ContentView called HomePageLandscape if the orientation is in Landscape or a ContentView called HomePagePortrait if the orientation is in Portrait.
I am doing this so that I can load a different layout for landscape vs portrait so I can optimize my layout.
My issue is that I use dependency injection to inject my ViewModel HomeViewModel. I inject this into the ContentPage HomePage and I am attempting to pass the HomeViewModel from the ContentPage HomePage's XAML into the markup for HomePageOrientationViewLoader.
Here is my HomePage.xaml.cs code behind for my ContentPage:
using ScoreKeepersBoard.ViewModels;
namespace ScoreKeepersBoard.Views;
public partial class HomePage : ContentPage
{
public HomePage(HomeViewModel homeViewModelInj)
{
HomeViewModel = homeViewModelInj;
BindingContext = homeViewModelInj;
InitializeComponent();
}
HomeViewModel HomeViewModel { get; set; }
protected override void OnNavigatedTo(NavigatedToEventArgs args)
{
((HomeViewModel)BindingContext).CreateInitialGameTypes();
base.OnNavigatedTo(args);
}
}
Here is my HomePage.xaml for the ContentPage:
<?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:controls="clr-namespace:ScoreKeepersBoard.ContentViews"
x:Class="ScoreKeepersBoard.Views.HomePage"
Title="HomePage">
<VerticalStackLayout>
<controls:HomePageOrientationViewLoader
BindingContext="{Binding HomeViewModel}"
>
</controls:HomePageOrientationViewLoader>
</VerticalStackLayout>
</ContentPage>
Here is my HomePageOrientationViewLoader.xaml.cs code behind for my ContentView:
using System.Reflection;
using ScoreKeepersBoard.ViewModels;
namespace ScoreKeepersBoard.ContentViews;
public partial class HomePageOrientationViewLoader : ContentView
{
public ContentView homePagePortraitContentView;
public ContentView homePageLandscapeContentView;
public HomeViewModel HomeViewModel { get; set; }
public HomePageOrientationViewLoader()
{
InitializeComponent();
try
{
//homeVM is always null
HomeViewModel homeVM = ((HomeViewModel)BindingContext);
string entryValue = homeVM.EntryValue;
homePagePortraitContentView = new HomePagePortrait(homeVM);
homePageLandscapeContentView = new HomePageLandscape(homeVM);
}
catch (Exception e)
{
string message = e.Message;
}
DeviceDisplay.Current.MainDisplayInfoChanged += Current_MainDisplayInfoChanged;
this.Content = DeviceDisplay.Current.MainDisplayInfo.Orientation == DisplayOrientation.Portrait ? homePagePortraitContentView : homePageLandscapeContentView;
}
private void Current_MainDisplayInfoChanged(object sender, DisplayInfoChangedEventArgs e)
{
if (e.DisplayInfo.Orientation == DisplayOrientation.Landscape)
{
this.Content = homePageLandscapeContentView;
}
else if (e.DisplayInfo.Orientation == DisplayOrientation.Portrait)
{
this.Content = homePagePortraitContentView;
}
else
{
this.Content = homePagePortraitContentView;
}
}
}
The code compiles and runs,
but the issue is that the BindingContext on HomePageOrientationViewLoader is always null. I would expect this to be set from the property defined in the ContentView's markup in HomePage ContentPage.
I also tried to set HomePageOrientationViewLoader's markup in ContentView's markup in HomePage ContentPage as just a normal Property defined on HomePageOrientationViewLoader as such:
<?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:controls="clr-namespace:ScoreKeepersBoard.ContentViews"
x:Class="ScoreKeepersBoard.Views.HomePage"
Title="HomePage">
<VerticalStackLayout>
<controls:HomePageOrientationViewLoader
HomeViewModel = "{Binding HomeViewModel}">
</controls:HomePageOrientationViewLoader>
</VerticalStackLayout>
</ContentPage>
But this won't even compile. I get the following error:
/Users/RemoteCommand/Projects/ScoreKeepersBoard/Views/HomePage.xaml(13,13): Error XFC0009: No property, BindableProperty, or event found for "HomeViewModel", or mismatching type between value and property. (XFC0009)
This is obviously not true since HomeViewModel is a Property on both HomePage ContentPage and HomePageOrientationViewLoader ContentView.
I need to be able to have HomeViewModel in HomePageOrientationViewLoader and then to pass this on to HomePageLandscape and HomePagePortrait ContentViews so they can share the same ViewModel when the user switches between Portrait and Landscape view but I am so far not able to get this working. I would appreciate any help.
UPDATE
Jason commented: " If you want the control to have the same BindingContext as the page, you don't need to do anything - that should automatically inherit."
I just tried to remove the binding property on the control markup as such:
<?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:controls="clr-namespace:ScoreKeepersBoard.ContentViews"
x:Class="ScoreKeepersBoard.Views.HomePage"
Title="HomePage">
<VerticalStackLayout>
<controls:HomePageOrientationViewLoader>
</controls:HomePageOrientationViewLoader>
</VerticalStackLayout>
</ContentPage>
And when my HomePageOrientationViewLoader page loads the constructor the HomeViewModel from HomePageOrientationViewLoader's BindingContext is still null:
UPDATE 2
Jason said that you should not assume the BindingContext is set in the constructor. He was right. When my HomePageOrientationViewLoader ContentView first loads the ContentView is not set but when I change the orientation and check to see what the BindingContext is as such:
private void Current_MainDisplayInfoChanged(object sender, DisplayInfoChangedEventArgs e)
{
HomeViewModel homeVM = ((HomeViewModel)BindingContext);
homePageLandscapeContentView.BindingContext = homeVM;
homePagePortraitContentView.BindingContext = homeVM;
...
}
homeVM which is set from the BindingContext of HomePageOrientationViewLoader is no longer null. I can then set the BindingContext of HomePageLandscape and HomePagePortrait and their ViewModels are active and bound. The only issue for me is that when the page initially loads HomePageLandscape and HomePagePortrait don't have their BindingContext set.
How can I get around this? Is there some event on ContentView that gets triggered when BindingContext is set so I could then override that event and bind HomePageLandscape and HomePagePortrait's Binding Context to the BindingContext of HomePageOrientationViewLoader?
ContentView has an OnBindingContextChanged method you can override

Xamarin Froms Entry Custom Control does not work

I'm trying to make a custom control to use it in multiple places.
the thing is it works fine with the label but when it comes to entry it won't even run and it gives me
No property, BindableProperty, or event found for "EntryText"
here is my custom control Xaml:
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
<ContentView.Content>
<StackLayout>
<Label x:Name="MyLabel" />
<Entry x:Name="MyEntry" />
</StackLayout>
</ContentView.Content>
</ContentView>
and its code behind
public partial class MyCustomControl : ContentView
{
public static readonly BindableProperty LabelTextProperty = BindableProperty.Create(
nameof(LabelText),
typeof(string),
typeof(MyCustomControl),
propertyChanged: LabelTextPropertyChanged);
private static void LabelTextPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (MyCustomControl)bindable;
control.MyLabel.Text = newValue?.ToString();
Debug.WriteLine("LabelTextPropertyChanged: " + control.MyEntry);
Debug.WriteLine("LabelTextPropertyChanged: new value" + newValue);
}
public string LabelText
{
get => base.GetValue(LabelTextProperty)?.ToString();
set
{
base.SetValue(LabelTextProperty, value);
}
}
public static BindableProperty EntryBindableProperty = BindableProperty.Create(
nameof(EntryText),
typeof(string),
typeof(MyCustomControl),
propertyChanged:EntryBindablePropertyChanged
);
private static void EntryBindablePropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (MyCustomControl)bindable;
Debug.WriteLine("EntryBindablePropertyChanged: " + control.MyEntry);
Debug.WriteLine("EntryBindablePropertyChanged: new value" + newValue);
}
public string EntryText
{
get => base.GetValue(EntryBindableProperty)?.ToString();
set
{
base.SetValue(EntryBindableProperty, value);
}
}
public MyCustomControl()
{
InitializeComponent();
}
}
and its Usage
<StackLayout>
<local:MyCustomControl
LabelText="{Binding BindingLabel}"
EntryText="{Binding BindingEntry}"/>
</StackLayout>
NOTE:
I tried to remove the
</ContentView.Content>
from my xaml because i've seen some example like that,
and also i've tried to set binding in the code behind constructor
public MyCustomControl()
{
InitializeComponent();
MyEntry.SetBinding(Entry.TextProperty, new Binding(nameof(EntryText) , source: this));
}
but neither did work for the entry .
so how can i resolve this and does it make any additional setting if i want to bind the value to a view model.
thanks in advance.
Update:
#Jessie Zhang -MSFT
thanks for your help I really appreciate it,
however I discovered a bug in MyCustomControl code => EntryTextProperty
is that i have to declare the defaultBindingMode to be able to get the data passed to a ViewModel Property.
so i changed the BindableProperty code to:
public static BindableProperty EntryTextProperty = BindableProperty.Create(
nameof(EntryText),
typeof(string),
typeof(MyCustomControl),
defaultBindingMode: BindingMode.OneWayToSource,
propertyChanged:EntryBindablePropertyChanged
);
You didn't bind the Entry.Text and EntryText property in MyCustomControl.xaml:
Please refer the following code:
<?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="CardViewDemo.Controls.MyCustomControl"
x:Name="this"
>
<ContentView.Content>
<StackLayout BindingContext="{x:Reference this}">
<Label x:Name="MyLabel" Text="{Binding LabelText}" />
<Entry x:Name="MyEntry" Text="{ Binding EntryText}" />
</StackLayout>
</ContentView.Content>
</ContentView>
And I used the following code to did a test, it works properly.
<local:MyCustomControl
LabelText="test1"
EntryText="test2"/>
Update:
From document Create a property,we know that:
The naming convention for bindable properties is that the bindable
property identifier must match the property name specified in the
Create method, with "Property" appended to it.
So, you can change your code EntryBindableProperty to EntryTextProperty, just as follows:
public static readonly BindableProperty EntryTextProperty = BindableProperty.Create(nameof(EntryText), typeof(string),typeof(MyCustomControl),string.Empty, BindingMode.TwoWay, null, EntryBindablePropertyChanged);
public string EntryText
{
get => base.GetValue(EntryTextProperty)?.ToString();
set
{
base.SetValue(EntryTextProperty, value);
}
}
And the xaml code is:
<?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="CardViewDemo.Controls.MyCustomControl"
x:Name="template"
>
<ContentView.Content>
<StackLayout >
<Label x:Name="MyLabel" Text="{Binding Source={x:Reference template},Path=LabelText}"/>
<Entry x:Name="MyEntry" Text="{Binding EntryText, Source={x:Reference template}}" >
</Entry>
</StackLayout>
</ContentView.Content>
</ContentView>

Why is my simple data binding not working?

For the life of me I can't understand why this simple data binding isn't working at all.
First I tried it with the combo box (picker) items, and even though the string list "categories_names" gets populated with values in the code behind, they don't show up in the view.
Then I tried a simple text on a label, which didn't work either.
The XAML file:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="budget.Pages.CreateItemBase"
BindingContext="budget.Pages.CreateItemBase">
<ContentPage.Content>
<StackLayout>
<Label
Text="{Binding Thing}"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand"
/>
<Picker
x:Name ="IB_Category"
ItemsSource="{Binding categories_names}"
Title="Select a Category"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand"
/>
</StackLayout>
</ContentPage.Content>
</ContentPage>
The code behind:
public partial class CreateItemBase : ContentPage
{
private List<Category> Categories;
private string thing;
public string Thing
{
get { return thing; }
set { thing = value; }
}
public IList<string> categories_names
{
get { return Categories.Select(x => x.Name).ToList(); }
set { Categories = new List<Category>(); }
}
public CreateItemBase ()
{
InitializeComponent ();
var z = BindingContext; // To check if the BindingContext is right during debug
Thing = "whatever";
SQLiteConnection DBCon = new SQLiteConnection(App.DBLocation);
DBCon.CreateTable<Category>(); // Creates the table if it doesn't exist yet
Categories = DBCon.Table<Category>().ToList();
DBCon.Close();
}
}
Any idea on what might be wrong? Much appreciated.

Databinding of Listview in custom contentview

I am trying to create a contentview which has a listview xamarinforms.
I want to set a property of the contentview that is then used to bind the data to the listview.
Unfortunately I am not able to populate it.
I broke down the example to get a poc. The desired result should be a contentpage with all the names.
Any helb appreciated. Thx in advance!
The example consists of:
Contentpage:
Adds the contentview.
It also has a bindingcontext to a viewmodel - CompanyVM.
Sets Property PersonList of contentview to PersonVM.Personlist. (Unsure if correct)
Contentview.XAML:
XAML of contentview
Bindings for listview (unsure if correct)
Contentview.cs
XAML code-behind
Property Settings for contentview
CompanyVM:
Viewmodel used
Company & Person & Mockup
Simple classed to work with
Example
ContentMainPage
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:cv="clr-namespace:ContentViewExample.XAML"
xmlns:vm="clr-namespace:ContentViewExample.ViewModel"
xmlns:local="clr-namespace:ContentViewExample"
x:Class="ContentViewExample.MainPage"
x:Name="mainpage">
<ContentPage.BindingContext>
<vm:CompanyVM/>
</ContentPage.BindingContext>
<StackLayout>
<cv:personlistCV Company="{x:Reference mainpage }"/>
<!--Is this correct?-->
</StackLayout>
</ContentPage>
```
Contentview.XAML
<?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="ContentViewExample.XAML.personlistCV"
x:Name="cvPersons">
<ContentView.Content>
<StackLayout>
<ListView x:Name="lstPerson"
ItemsSource="{Binding Company.Persons}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Horizontal">
<Label Text="{Binding Path=Name}"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentView.Content>
</ContentView>
Contentview.cs
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace ContentViewExample.XAML
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class personlistCV : ContentView
{
public personlistCV ()
{
InitializeComponent ();
}
//CompanyProperty
public static readonly BindableProperty CompanyProperty =
BindableProperty.Create(
"Company",
typeof(CompanyVM),
typeof(personlistCV),
null);
public personlistCV Company
{
set { SetValue(CompanyProperty, value); }
get { return (personlistCV)GetValue(CompanyProperty); }
}
}
}
CompanyVM
namespace ContentViewExample.ViewModel
{
public class CompanyVM: ViewModelBase
{
ObservableCollection<Person> persons;
string companyname;
public CompanyVM()
{
companyname = "Test Company";
persons = new ObservableCollection<Person>();
foreach (Person item in MockData.GetPeople())
persons.Add(item);
}
public string Company
{
set { SetProperty(ref companyname, value); }
get { return companyname; }
}
public ObservableCollection<Person> Persons
{
set { SetProperty(ref persons, value); }
get { return persons; }
}
}
}
Company & Person
public class Company
{
public string Name { get; set; }
}
public class Person
{
public string Name{get;set;}
}
public static class MockData
{
public static List<Person> GetPeople()
{
List<Person> tmp = new List<Person>
{
new Person
{
Name="Ted"
},
new Person
{
Name="Jennifer"
},
new Person
{
Name="Andy"
},
new Person
{
Name="Oscar"
}
};
return tmp;
}
}
You have tried to bind personlistCV.Company the following way
<StackLayout>
<cv:personlistCV Company="{x:Reference mainpage }"/>
<!--Is this correct?-->
</StackLayout>
I see several issues here:
Bindings are set with the XAML extension Binding
The Company is set to the mainpage, which is of Type MainPage.
It should rather be set to mainpage.BindingContext (this is a CompanyCV object)
Furthermore the personlistCV.Company is of type personlistCV, which does not really make sense. The field should be of type CompanyVM, since we'd like to bind the viewmodel (and personlistCV does not even have a Persons (bindable) property).
Having said that, the following code should work:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:cv="clr-namespace:ContentViewExample.XAML"
xmlns:vm="clr-namespace:ContentViewExample.ViewModel"
xmlns:local="clr-namespace:ContentViewExample"
x:Class="ContentViewExample.MainPage"
x:Name="mainpage">
<ContentPage.BindingContext>
<vm:CompanyVM/>
</ContentPage.BindingContext>
<StackLayout>
<cv:personlistCV Company="{Binding Path=BindingContext, Source={x:Reference mainpage}}"/> <!-- Bind the element to `mainpage.BindingContext` -->
</StackLayout>
</ContentPage>
Maybe
<cv:personlistCV Company="{Binding .}"/>
could work, too, since . usually binds to the BindingContext of the view and the BindingContext of the page is passed down to the views (unless another BindingContext is set for the views explicitly).
And for the companyCV
public partial class personlistCV : ContentView
{
public personlistCV ()
{
InitializeComponent ();
}
//CompanyProperty
public static readonly BindableProperty CompanyProperty =
BindableProperty.Create(
"Company",
typeof(CompanyVM),
typeof(personlistCV),
null);
public CompanyVM Company
{
set { SetValue(CompanyProperty, value); }
get { return (personlistCV)GetValue(CompanyProperty); }
}
}

Xamarin Forms - Changes to ObservableCollection are not updating the View

I have a ViewModel that, when a Command is executed, gets items from a service and adds those items to an ObservableCollection.
The ObservableCollection is bound to a ListView.
My problem is that adding items to the ObservableCollection is not automatically updating the View - the ListView stays empty. If I call OnPropertyChanged(nameof(ItemList)) then the ListView is updated - but shouldn't the ObservableCollection be taking care of raising this?
I am using the latest Xamarin.Forms (2.3.4.247). This issue only affects Android - if I run on iOS it works correctly.
The ViewModel that implements INotifyPropertyChanged:
public class ListViewModel : ViewModelBase
{
private readonly IService m_Service;
public ListViewModel(IService service)
{
m_Service = service;
ItemList = new ObservableCollection<Item>();
RefreshCommand = new Command(async () => await RefreshData());
}
public ICommand RefreshCommand {get;set;}
public ObservableCollection<Item> ItemList { get; private set; }
private async Task RefreshData()
{
ItemList.Clear();
var newItems = await m_Service.GetItems();
if(newItems != null) {
foreach(var n in newItems)
{
ItemList.Add(new ItemViewModel() { Title = n.Title });
}
}
}
The Item ViewModel (that also implements INotifyPropertyChanged):
public class ItemViewModel : ViewModelBase
{
private string m_JobTitle;
public ItemViewModel()
{
}
public string Title
{
get { return m_Title; }
set
{
m_Title = value;
OnPropertyChanged(nameof(Title));
}
}
}
The 'List' View:
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:ui="clr-namespace:Test.Mobile.UI;assembly=Test.Mobile.UI"
x:Class="Test.Mobile.UI.MyListView">
<ContentView.Content>
<StackLayout Orientation="Vertical">
<ListView ItemsSource="{Binding ItemList}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ui:ItemView></ui:ItemView>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentView.Content>
</ContentView>
The 'Item' View:
<?xml version="1.0" encoding="UTF-8"?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:enums="clr-namespace:Test.Mobile.Models.Enums;assembly=Test.Mobile.Models"
x:Class="Test.Mobile.UI.JobItemView">
<ContentView.Content>
<Label Text={Binding Title}/>
</ContentView.Content>
</ContentView>
What version of Xamarin Forms are you using?
Update your project to the latest version
If you still having the issue, after adding the all elements try this:
ItemList = new ObservableCollection(newItems);
If that does not work, maybe could be an issue in your ViewModelBase.
For Handle PropertyChanged I use this nice package
https://github.com/Fody/PropertyChanged