How can I bind a property to a view model in MAUI? - c#

I'm trying to bind a property to a view model.
I get the following error:
Error XFC0009 No property, BindableProperty, or event found for "ViewModel", or mismatching type between value and property.
public abstract class BaseTestView : ContentView
{
public BaseVm ViewModel
{
get => (BaseVm)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, BindingContext = value);
}
public static BindableProperty ViewModelProperty { get; set; } = BindableProperty.Create(nameof(ViewModel), typeof(BaseVm), typeof(BaseTestView));
}
<v:BaseTestView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:MyProject.ViewModels"
xmlns:v="clr-namespace:MyProject.Views"
x:Class="MyProject.Views.ChildTestView"
x:DataType="vm:ChildTestVm">
<v:BaseTestView.Content>
<StackLayout>
<Label Text="{Binding Foo}" />
</StackLayout>
</v:BaseTestView.Content>
</v:BaseTestView>
public partial class ChildTestView : BaseTestView
{
public ChildTestView() : base()
{
InitializeComponent();
}
}
public class ChildTestVm : BaseVm
{
public string Foo { get; set; }
public ChildTestVm()
{
Title = "Test";
Foo = "some stuff";
}
}
public class HomeVm : BaseVm
{
public ChildTestVm Tested { get; set; }
}
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:MyProject.ViewModels"
xmlns:v="clr-namespace:MyProject.Views"
x:Class="MyProject.Pages.HomePage"
x:DataType="HomeVm">
<ContentPage.Content>
<StackLayout>
<v:ChildTestView ViewModel="{Binding Tested}" />
<!-- ^ Error here /-->
</StackLayout>
</ContentPage.Content>
</ContentPage>
public partial class HomePage : ContentPage
{
}
Any idea of what this error means and how to fix it?

I tried some experiments, but failed to figure out why it gave that complaint - every variation I tried also gave that error.
Instead, do it this way:
First, set the BindingContext of ChildTestView:
<v:ChildTestView BindingContext="{Binding Tested}" />
That data-binds ChildTestView to the ChildTestVm from Tested.
If you also need access to the Vm for code behind, do it this way:
ChildTestView.xaml.cs:
private ChildTestVm ViewModel => (ChildTestVm)BindingContext;
Now in methods of ChildTestView, you can use ViewModel.Foo.
NOTE: If you dynamically change Tested:
If you have code anywhere that does Tested = ... AFTER HomePage is loaded and visible, then getting that to work requires Tested setter to do OnPropertyChanged(); (or other MVVM data binding mechanism). This is necessary to inform XAML of changes.

Related

How to provide ViewModel that has constructor with parameters to XAML page BindingContext

Provide BindingContext of the ViewModel to XAML page have benefits of IntelliSense. However, this syntax only works if the ViewModel has an empty contractor eg: MainPageViewModel()
<ContentPage.BindingContext>
<viewModels:MainPageViewModel />
</ContentPage.BindingContext>
If MainPageViewModel doesn't have empty constructor but only have parameters constructor eg: MainPageViewModel(param1, param2), then the above XAML syntax will throw compile error. How would I do this in XAML?
You could have a default constructor for your view model that would just serve IntelliSense purposes and then use the view model constructor with parameters in the page's constructor. That would give you the IntelliSense while being able to call the view models parameterized constructor. Something like this:
View model
public class SomeViewModel
{
public string SomeValue { get; set; }
//intellisense only constructor
public SomeViewModel(){
SomeValue = "test";
}
//real constructor
public SomeViewModel(string someValue){
SomeValue = someValue;
}
}
Page XAML
<Window.BindingContext>
<local:SomeViewModel />
</Window.BindingContext>
Page constructor
public SomePage()
{
this.BindingContext = new SomeViewModel("real value");
}
After looking at WeeklyXamarin.Mobile Repo. I found that this can be implemented nicely with the following:
Create a PageBase as follow:
public class PageBase : ContentPage { }
public class PageBase<TViewModel> : PageBase
{
public TViewModel ViewModel { get; set; }
public PageBase()
{
BindingContext = ViewModel = Container.Instance.ServiceProvider.GetRequiredService<TViewModel>();
}
}
Then the ViewModel in XAML can be passed in with x:TypeArguments like:
<views:PageBase
x:Class="WeeklyXamarin.Mobile.Views.AboutPage"
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"
xmlns:viewmodels="clr-namespace:WeeklyXamarin.Core.ViewModels;assembly=WeeklyXamarin.Core"
xmlns:views="clr-namespace:WeeklyXamarin.Mobile.Views"
xmlns:vm="clr-namespace:WeeklyXamarin.Core.ViewModels;assembly=WeeklyXamarin.Core"
Title="{Binding Title}"
x:TypeArguments="viewmodels:AboutViewModel"
mc:Ignorable="d">
AboutViewModel has a parameter in the contractor and work nicely with the app
public AboutViewModel(INavigationService navigation, IDataStore dataStore) : base(navigation)
Credit: Mr Kym Phillpotts for his code at WeeklyXamarin repo
I think you could not do that in xaml. There is no such syntax in Xamarin.forms. The default usage is the constructor with no parameters. Make a simple example for your reference.
ViewModel:
public class viewModels
{
public string text1 { get; set; }
public viewModels()
{
text1 = "text1";
}
}
BindingContext in xaml:
<StackLayout>
<Label
HorizontalOptions="CenterAndExpand"
Text="{Binding text1}"
VerticalOptions="CenterAndExpand">
<Label.BindingContext>
<local:viewModels />
</Label.BindingContext>
</Label>
</StackLayout>

Why does not Xamarin bind property?

I want to save data to application on window closing or application crashes.
When user writes to entry the data gets storen in property, but for some reason the binding does not work.
I followed a course on Udemy for this. I think it has something to do with referencing to different place in PCL.
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace TheIVInventory.ViewModels
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class AddItemPage : ContentPage
{
public AddItemPage()
{
InitializeComponent();
BindingContext = Application.Current;
}
private void Button_Clicked(object sender, EventArgs e) //Item added click.
{
}
}
}
Xaml :
<ContentPage
BackgroundColor="#104850"
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:Class="TheIVInventory.ViewModels.AddItemPage">
<StackLayout VerticalOptions="Center" x:Name="formLayout" Margin="20">
<Entry PlaceholderColor="White" Keyboard="Chat" Margin="40" Placeholder="Item Name" TextColor="White" Text="{Binding ItemName}"></Entry>
<Entry PlaceholderColor="White" Keyboard="Numeric" Margin="40" Placeholder="Item Price MIN (€)" TextColor="White"></Entry>
<Entry PlaceholderColor="White" Keyboard="Numeric" Margin="40" Placeholder="Item Price MAX (€)" TextColor="White"></Entry>
<Editor PlaceholderColor="White" Margin="40" VerticalOptions="FillAndExpand" Keyboard="Chat" Placeholder="Item Description" TextColor="White"></Editor>
<Button Text="Save" BackgroundColor="#80EEFF" Margin="10" Clicked="Button_Clicked" ></Button>
<Image Source="konjakki.png" Scale="0.15" AnchorY="0" BackgroundColor="#104850" ></Image>
</StackLayout>
</ContentPage>
using System;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace TheIVInventory
{
public partial class App : Application
{
// Setting the item add members.
private const string itemNameKey = "Name";
private const string itemMinPrice = "0";
public App()
{
InitializeComponent();
MainPage = new NavigationPage(new MainPage())
{
BarBackgroundColor = Color.FromHex("#104850"),
BarTextColor = Color.White
}; // Making the navigation possible.
}
protected override void OnStart()
{
// Handle when your app starts
}
protected override void OnSleep()
{
// Handle when your app sleeps
}
protected override void OnResume()
{
// Handle when your app resumes
}
// Making the add item properties.
public string ItemName
{
get
{
if (Properties.ContainsKey(itemNameKey))
return Properties[itemNameKey].ToString();
return "";
}
set
{
Properties[ItemName] = value;
}
}
public string ItemMinPrice
{
get
{
if (Properties.ContainsKey(itemMinPrice))
return Properties[itemMinPrice].ToString();
return "";
}
set
{
Properties[itemMinPrice] = value;
}
}
}
}
The main issue is that you don't appear to have set a BindingContext for your XAML to refer to.
It also looks to me as if you are trying to implement MVVM structure, but have not entirely understood it.
The points that immediately draw my eye are:
Your view code is within a ViewModel namespace rather than View
(which will prove confusing later on).
You have code that should be within a ViewModel class (which will be your Views BindingContext) in your main App code.
I would suggest that you create a Views namespace and move your AddItemPage code to it;
Create an AddItemViewModel and use it to implement your ItemName and ItemMinPrice properties.
Instead of using a button_clicked event in the code behind, bind the Button Command to an ICommand property type in your ViewModel. Then have your ViewModel instantiate the ICommand to use an internal method to run your save code.
If you want to achieve the MVVM in xamarin forms. Your binding way is wrong, you could create a model, put the ItemMinPrice and ItemName in this model like following code.Achieve the INotifyPropertyChanged interface, when data was changed.
class MyModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
string itemMinPrice;
public string ItemMinPrice
{
set
{
if (itemMinPrice != value)
{
itemMinPrice = value;
OnPropertyChanged("ItemMinPrice");
}
}
get
{
return itemMinPrice;
}
}
string itemName;
public string ItemName
{
set
{
if (itemName != value)
{
itemName = value;
OnPropertyChanged("ItemName");
}
}
get
{
return itemName;
}
}
}
Then you can change the bindingContext, like this code BindingContext = new MyModel();
I add a break point in the MyModel, give a itemName in the Entry you can see it was executed like following GIF.
Note: If you want to achieve that model data changed could display the view. You should change Mode to TwoWay like following code.
<Entry PlaceholderColor="White" Keyboard="Chat" Margin="40"
Placeholder="Item Name" TextColor="Black" Text="{Binding ItemName,
Mode=TwoWay}"></Entry>
Here is offical artical about MVVM, you can refer to it.
https://learn.microsoft.com/en-us/xamarin/xamarin-forms/xaml/xaml-basics/data-bindings-to-mvvm

How to bind ListView to a collection and refresh it real time

I am binding my ListView to my Realm database, but it doesn't refresh it when I add a new item into it. I am adding new items using another page.
I am using here as reference: https://help.syncfusion.com/xamarin/sflistview/mvvm#binding-itemssource
My Model:
public class Category : RealmObject
{
[PrimaryKey]
public string CategoryID { get; set; } = Guid.NewGuid().ToString();
public string CategoryTitle { get; set; }
public string CategoryDetails { get; set; }
[Backlink(nameof(Note.CategoryOfNote))]
public IQueryable<Note> Notes { get; }
public string CategoryIcon { get; set; }
public bool IsExpanded { get; set; }
}
My XAML file containing ListView
<ContentPage.BindingContext>
<vm:MainViewModel />
</ContentPage.BindingContext>
<ContentPage.ToolbarItems>
<ToolbarItem Text="NEWCAT"
Clicked="NewCat_Clicked"/>
</ContentPage.ToolbarItems>
<ContentPage.Content>
<ListView x:Name="categoryList"
ItemsSource="{Binding Categories}"
ItemTapped="ListView_ItemTapped"
HasUnevenRows="True">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Vertical"
HorizontalOptions="FillAndExpand"
Padding="10"
Spacing="10">
<Label Text="{Binding Path=CategoryTitle}"
FontSize="Medium"/>
<StackLayout IsVisible="{Binding IsExpanded}"
Orientation="Horizontal"
HorizontalOptions="CenterAndExpand">
<Button Text="Notes"
Clicked="NotesButton_Clicked" />
<Button Text="Edit"
Clicked="EditButton_Clicked"/>
<Button Text="Delete"
Clicked="DeleteButton_Clicked"/>
</StackLayout>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage.Content>
And my ViewModel
(DBServices.GetCategories is a static method that returns a collection of categories from Realm DB. And BaseViewModel implements INotifyPropertyChanged)
class MainViewModel : BaseViewModel
{
private Category _oldCategory;
public MainViewModel()
{
RefreshCategories();
}
private void RefreshCategories()
{
Categories = new ObservableCollection<Category>(DBServices.GetCategories());
}
private ObservableCollection<Category> _Categories;
public ObservableCollection<Category> Categories
{
get
{
return _Categories;
}
set
{
_Categories = value;
OnPropertyChanged("Categories");
}
}
}
This is OnPropertyChanged method in BaseViewModel class
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
When I click to the toolbar button, it pushes a new form page to create a new category. Then it just adds that category to the Realm DB. Afterwards it pops itself. But I see no change in my ListView.
EDIT: I can see new items in ListView when I restarted the app. But I want it to be listed as I add them.
You could use the Xamarin Forms MessagingCenter to tell the ListView it needs to update.
On the page where the new category is added to the Realm DB, at some point after the category is added but before the page is popped, you would send a message to tell the ListView page to update. This may look something like:
MessagingCenter.Send(this, "Update listview");
In your MainViewModel you'll need to subscribe to the message and then act upon it. This could be something like:
MessagingCenter.Subscribe<AddCategoryPage>(this, "Update listview", (sender) =>
{
RefreshCategories();
});
Two items to note when using the MessagingCenter.
The message passed must be identical between the sender and subscriber. In this case the message is "Update listview". Because it must be identical, you would probably want to define the message in separate class so it can be referenced by both the sender and subscriber.
When subscribing to a message you have to make sure that you unsubscribe. A popular pattern is to subscribe when the page appears and unsubscribe when the page disappears. A different pattern may work better for you though.
On a side note, you shouldn't need the full property definition for Categories. The ObservableCollection will handle the notification when items are added or removed. Just set Categories = new ObservableCollection<Category>(); once and then only add or remove items. You don't need to new it up on every DB call.
To test the XAML and ViewModel only, try replacing DBServices.GetCategories() with the following in the ViewModel:
public static IEnumerable<Category> TestCategories
{
get
{
yield return new Category { CategoryTitle = "title 1" };
yield return new Category { CategoryTitle = "title 2" };
}
}
...
public MainViewModel()
{
Categories = new ObservableCollection<Category>(TestCategories);
}
DBServices.GetCategories() should also be tested for expected output, not shown here.

Data Binding using Mvvm Helpers

I'm trying to bind some data using MvvmHelpers by creating an object and binding that object.
https://github.com/jamesmontemagno/mvvm-helpers
In my model I have several data to bind so I just made a quick template of what I'm working on.
If I move what is inside NameModel to NameViewModel it does work however I'm trying separate my data.
Model:
public class NameModel : BaseViewModel
{
string name;
public string Name
{
get { return name; }
set { SetProperty(ref name, value); }
}
}
View Model:
public class NameViewModel : BaseViewModel
{
NameModel nameModel;
public NameViewModel()
{
nameModel = new NameModel { name="Jon Doe" };
}
}
Page.xaml.cs
public partial class NamePage : ContentPage
{
public NamePage()
{
InitializeComponent();
BindingContext = new NameViewModel();
}
}
Page.xaml:
<ContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="NameProj.NamePage">
<StackLayout
Orientation="Vertical" >
<Label
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
BackgroundColor="Transparent"
Text={Binding Name}/>
</StackLayout>
</ContentPage>
Have you tried changing it to nameModel.Name?
<Label
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
BackgroundColor="Transparent"
Text="{Binding nameModel.Name}"/>
Hope it helps!
NameModel nameModel;
I think should be public
public NameModel nameModel {get; set;}
and Yes, I think #mindOfAi is correct

ListView ItemsSource binding not displaying items

I am trying to create a bound ListView in Xamarin. Here's the C# code:
public partial class LearnPage : ContentPage
{
public class TodoItem
{
public string DisplayName { get; set; }
}
//ObservableCollection<TodoItem> Items = new ObservableCollection<TodoItem>();
private IEnumerable<TodoItem> _items;
public IEnumerable<TodoItem> Items
{
get { return _items; }
set
{
if (Equals(_items, value))
return;
_items = value;
OnPropertyChanged();
}
}
public LearnPage ()
{
InitializeComponent();
BindingContext = this;
Items = new TodoItem[]{
new TodoItem{ DisplayName = "Milk cartons are recyclable" }
};
//Items.Add(new TodoItem { DisplayName = "Milk cartons are recyclable" });
}
}
You can also see some commented out code with an ObervableCollection, which I have also tried with.
And here's the 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" x:Class="learn.LearnPage">
<ContentPage.Content>
<StackLayout VerticalOptions="FillAndExpand" Padding="0,10,0,10">
<ListView ItemsSource="{Binding Items}" RowHeight="40" x:Name="sarasas">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding DisplayName}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage.Content>
When I build the app, an empty list is displayed. I'm really new to Xamarin and I think I'm just missing something obvious. Any help appreciated!
I am not sure if ContentPage uses [CallerMemberName] for the OnPropertyChanged() method. So first thing I would try is to write OnPropertyChanged("Items") instead.
Either way, if I were you I would separate concerns and move the Items into a ViewModel class, which implements INotifyPropertyChanged itself. This will help later on if you want to test, add more code such as commands, inject dependencies etc., where you will keep ViewModel code separate from View code.
So you could start with:
public abstract class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged ([CallerMemberName]string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs (propertyName));
}
}
Then create your ViewModel:
public class LearnViewModel : BaseViewModel
{
public ObservableCollection<TodoItem> Items { get; } = new ObservableCollection<TodoItem>();
private ICommand _addTodoItem;
public ICommand AddTodoItem =>
_addTodoItem = _addTodoItem ?? new Command(DoAddTodoItem);
private int _count;
private void DoAddTodoItem()
{
var item = new TodoItem { DisplayName = $"Item {++_count}" };
// NotifyCollectionChanged must happen on UI thread.
Device.BeginInvokeOnMainThread (() => {
Items.Add(item);
});
}
}
Then you can keep your View code thin like:
public partial class LearnPage : ContentPage
{
public LearnPage ()
{
InitializeComponent();
BindingContext = new LearnViewModel();
}
}
Normally you would invoke a command to populate the Items in your ViewModel, this could be by fetching data from the Internet, or loading local data from a database etc.
In this sample you can just add a constructor to the LearnViewModel and call DoAddTodoItem a couple of times:
public class LearnViewModel : BaseViewModel
{
public LearnViewModel()
{
DoAddTodoItem();
DoAddTodoItem();
}
...
This should show you something like this when you launch the app:

Categories

Resources