Xamarin Forms : Add view Model To ContentView in collectioveView Issue - xaml

I'm adding a ContentView in a CollectionView ItemTemplate
<CollectionView ItemsSource="{Binding Questions}">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout >
<row:QuestionView Question="{Binding .}" /> <--- Content View
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
My Issue is that I am trying to add another View Model into each Question View but the Binding Context is set as the question object and not the Question View Model and I cant change it
public static readonly BindableProperty QuestionProperty =
BindableProperty.Create(propertyName: nameof(Question)
, returnType: typeof(AuditQuestion)
, declaringType: typeof(AuditCrewDetailsListComponent)
, defaultBindingMode: BindingMode.TwoWay
, propertyChanged: QuestionChanged);
private static void QuestionChanged(BindableObject bindable, object oldValue, object newValue)
{
(bindable as QuestionView).BindingContext = ViewModelLocator.QuestionVM; <-- tried this but did not change the BindingContext , bindingContext is set to the Question
if(newValue != null)
{
((QuestionVM)(bindable as QuestionView)
.BindingContext).Question = (Question)newValue; <-- Trying to add the question to the view model(throws Error)
}
}
public QuestionView()
{
InitializeComponent();
this.BindingContext = ViewModelLocator.QuestionVM;
}
public class QuestionVM: BaseViewModel
{
public Question question;
public QuestionVM() : base()
{
}
public Question Question
{
get
{
return question;
}
set
{
SetProperty(ref question, value);
}
}
}
So my goal is to pass the question from the BindableProperty into the
question view model.
I'm new to Xamarin forms so I'm not even sure if this is the best way to do this

Related

Stacklayout backgroundColor binding with MVVM

I'm attempting to get my head around MVVM with XamarinForms and I'm slightly confused with regards to proper partitioning of functionality:
I have a main page, MainPage.xaml, which includes a stacklayout:
<StackLayout x:Name="MainPageStackLayout">
...
</StackLayout>
Within this stacklayout I have Picker which is bound as follows:
<Picker Title="Select a background colour"
TitleColor="Black"
TextColor="Black"
ItemsSource="{Binding MyColours}"
ItemDisplayBinding="{Binding Name}"
SelectedItem="{Binding selectedBackGroundColour}" SelectedIndexChanged="BackGroundColourPicker_SelectedIndexChanged"/>
Following the article from microsoft (https://learn.microsoft.com/en-us/samples/xamarin/xamarin-forms-samples/userinterface-monkeyapppicker/):
I have a "View" which basically defines the layout of my page.
A "ViewModel" which holds an IList "MyColours" and a variable "SelectedBackGroundColour".
A "Model" which defines the MyColour class. A MyColour has a string name and a Xamarin.Forms.Color (from a hex value, both populated on start up).
This all works fine. I can start up the app and the Picker populates with the colours I add to "MyColours". If I change the index then my SelectedBackGroundColour also updates, has the correct name and a different RGB value.
However, I'm lost as to where I would tie in the updating of the actual background colour of the MainPageStackLayout. The View (MainPage.xaml.cs) picks up the "BackGroundColourPicker_SelectedIndexChanged" event but what is the standard practice for reading from the view model (where SelectedBackGround colour is actual defined ?)
I have a feeling I can bind Background colour in the MainPageStackLayout xaml view so I wont have to catch the selected index change event.
Thanks all.
According to your description, I guess that you want to change MainPage StackLayout BackGround color by Picker value, am I right?
If yes, please follow the steps below.
Firstly, please confirm that you implement INotifyPropertyChanged interface to notify SelectedBackGroundColour changed.
Then there are full code, please take a look:
<StackLayout x:Name="MainPageStacklayout" BackgroundColor="{Binding selectedBackGroundColour.color}">
<Picker
x:Name="picker1"
Title="Select a background colour"
ItemDisplayBinding="{Binding name}"
ItemsSource="{Binding MyColours}"
SelectedItem="{Binding selectedBackGroundColour}"
TextColor="Black"
TitleColor="Black" />
</StackLayout>
public partial class Page5 : ContentPage, INotifyPropertyChanged
{
public ObservableCollection<MyColour> MyColours { get; set; }
private MyColour _selectedBackGroundColour;
public MyColour selectedBackGroundColour
{
get { return _selectedBackGroundColour; }
set
{
_selectedBackGroundColour = value;
RaisePropertyChanged("selectedBackGroundColour");
}
}
public Page5()
{
InitializeComponent();
MyColours = new ObservableCollection<MyColour>()
{
new MyColour(){name="red",color=Color.Red},
new MyColour(){name="gray",color=Color.Gray},
new MyColour(){name="BlueViolet",color=Color.BlueViolet}
};
selectedBackGroundColour = MyColours[0];
this.BindingContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public class MyColour
{
public string name { get; set; }
public Color color { get; set; }
}
The screenshot:

Add multiple bindingcontexts to xaml file

I try to realize my first MVVM-Project.
First I created model called "person.cs".
Then I created a modelview "AddPerson.cs", which should dynamically creates the data, which is stored in person.cs.
In my view (completely created with xaml) I have a button which should call a method "CreatePerson()" from my "AddPerson.cs". I like to bind the method.
Additionally I have created a label which should be bound to the class "person.cs" for example to the public string "Name".
How can I set the BindingContext of the Button to the "AddPerson.cs"-class and the BindingContext of the Label to the "person.cs"-class?
Yes this is possible.
Most of the Elements inherit BindablObject. Each BindableObjaect has a BindingContext Property.
See: https://learn.microsoft.com/en-us/xamarin/xamarin-forms/xaml/xaml-basics/data-binding-basics
MainViewModel
The Viewmodel for your entire page, that holds every sub-viewmodel.
public class MainViewModel
{
public AddPersonViewModel AddPersonViewModel { get; }
public PersonViewModel PersonViewModel { get; }
public MainViewModel()
{
// the passed action is just a fake action to simulate adding a person
AddPersonViewModel = new AddPersonViewModel(value => PersonViewModel.Name = value);
PersonViewModel = new PersonViewModel();
}
}
AddPersonViewModel
Contains your add logic.
public class AddPersonViewModel : INotifyPropertyChanged
{
public AddPersonViewModel(Action<string> onAction)
{
AddPerson = new Command(() =>
{
onAction(NewName); // call your update logic
NewName = ""; // reset name
});
}
public Command AddPerson { get; }
private string _name;
public string NewName
{
get => _name;
set
{
_name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NewName)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
PersonViewModel
Contains your "new" Person.
public class PersonViewModel : INotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set
{
_name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
MainPage
Create and set your MainViewModel.
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
BindingContext = new MainViewModel();
}
}
MainPage.xaml
Here we bind the BindingContext of Entry and Button to the AddPersonViewModel property of our ContentPage's BindingContext which is the MainViewModel. And then we bind the Text of the Label and the Command of the Button to NewName and AddPerson properties of the local BindingContext, which is AddPersonViewModel
Same for Label.
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:App5"
x:Class="App5.MainPage">
<StackLayout>
<Entry BindingContext="{Binding AddPersonViewModel}" Text="{Binding NewName}"
HorizontalOptions="FillAndExpand" />
<Button BindingContext="{Binding AddPersonViewModel}" Text="Click me!" Command="{Binding AddPerson}"
HorizontalOptions="Center" />
<Label Text="Added Person:" FontAttributes="Bold"
HorizontalOptions="Center"/>
<Label BindingContext="{Binding PersonViewModel}" Text="{Binding Name}"
HorizontalOptions="Center"/>
</StackLayout>
</ContentPage>
The example is very hacky, but I think you get the point. The key is the already mentioned property BindingContext
You are missing some essential concepts which result in your requests being strange.
You don't data bind to the class definition, but to the instance of the class. As one ViewModel is a class it may contain instances of other classes that you data bind to, and everything except that is in 99% of cases a wrong thing to do and your example is not one of those 1% of cases.
So basically your ViewModel should be something like:
public class PersonViewModel
{
public Person Person {get; set}
public ICommand AddPersonCommand {get; set}
}
Your BindingContext is then an instance of PersonViewModel and then on Label you bind to Person while on the button you would bind to AddPersonCommand.

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 (XAML): Different layouts depending on a condition

Is there a way to choose what layout initialize depending on one condition? I have a Grid for football stats but if myViewModel.Sport == Sports.Basketball I'd like to load a completely different layout.
I tried something like this with Datatrigger in each View but it seems a mess for me:
<Label Text="{Binding Goals}"
Grid.Row="1" Grid.Column="0">
<Label.Triggers>
<DataTrigger TargetType="Label"
Binding="{Binding Sport}"
Value="1">
<Setter Property="Text"
Value="{Binding Points}"/>
</DataTrigger>
</Label.Triggers>
</Label>
I show "goals" but if the Sports enum value is 1 (Sports.Basketball) I change to "points". I want to do this with lots of Labels and even Images so I need a proper way to do it.
Could someone help me? I need to load a different Grid depending on the Sport Property of my ViewModel.
Another thing you could do is place each separate sport into it's own view, add all the views to your page and set their IsVisible property depending on which sport you want to show.
An example would look like this in pseudo-code:
<Page>
<Grid>
<BasketballView IsVisible="{Binding IsBasketball}">
<SoccerView IsVisible="{Binding IsSoccer}">
<FootballView IsVisible="{Binding IsFootball}">
</Grid>
</Page>
Then set the appropriate boolean values from the ViewModel.
To use DataTemplateSelector to solve this, as mentioned by #StephaneDelcroix, you'll want a custom class that has ItemsSource and ItemTemplate properties.
I haven't thought through / tested how DataTemplateSelector would be used with this; anyone is welcome to add that to this answer.
using System.Collections;
using Xamarin.Forms;
namespace YourNamespace
{
// From https://forums.xamarin.com/discussion/19874/listview-inside-stacklayout-a-height-problem/p2, #maxx313.
public class TemplatedStack : StackLayout
{
public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create("ItemsSource", typeof(IList), typeof(TemplatedStack), propertyChanged: OnItemsSourceChanged);
public IList ItemsSource
{
get { return (IList)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
private static void OnItemsSourceChanged(BindableObject pObj, object pOldVal, object pNewVal)
{
var layout = pObj as TemplatedStack;
if (layout != null && layout.ItemTemplate != null)
{
layout.BuildLayout();
layout.ForceLayout();
}
}
public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create("ItemTemplate", typeof(DataTemplate), typeof(TemplatedStack), propertyChanged: OnItemTemplateChanged);
public DataTemplate ItemTemplate
{
get { return (DataTemplate)GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
}
private static void OnItemTemplateChanged(BindableObject pObj, object pOldVal, object pNewVal)
{
var layout = pObj as TemplatedStack;
if (layout != null && layout.ItemsSource != null)
layout.BuildLayout();
}
private void BuildLayout()
{
Children.Clear();
foreach (var item in ItemsSource)
{
var view = (View)ItemTemplate.CreateContent();
view.BindingContext = item;
Children.Add(view);
}
}
protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
{
return base.OnMeasure(widthConstraint, heightConstraint);
}
}
}
In your XAML, do
<yourXmlns:TemplatedStack .../>
where yourXmlns must be an xmlns declaration at top of your XAML.
Usage of ItemsSource and ItemTemplate properties is similar to how you would bind an items collection and template to a ListView.
(The reason NOT to use a ListView here, is that ListView may interfere with touch events, and adds extra layout cost.)
Bind to this a collection containing a single item.
E.g. for this question, that item would be the specific sport being viewed.

Curious difference in behavior between compiled and regular binding

I'm trying to create a MenuFlyout with ToggleMenuFlyoutItems where one and only one toggle is checked at any given moment. The toggles corresponds to ToggleViewModels, binding the IsChecked property of the toggle to an IsSelected property of the ToggleViewModel. Because I want to uncheck the previously checked toggle whenever a new toggle is checked I relay the setting of the IsSelected property to the MainViewModel that holds the collection of ToggleViewModels.
Button with flyout defined in MainPage.xaml
<Button Content="Asdf">
<Button.Flyout>
<MenuFlyout>
<ToggleMenuFlyoutItem
Text="{x:Bind _viewModel.ToggleCollection[0].Name}"
IsChecked="{x:Bind _viewModel.ToggleCollection[0].IsSelected, Mode=TwoWay}" />
<ToggleMenuFlyoutItem
Text="{x:Bind _viewModel.ToggleCollection[1].Name}"
IsChecked="{x:Bind _viewModel.ToggleCollection[1].IsSelected, Mode=TwoWay}" />
<ToggleMenuFlyoutItem
Text="{x:Bind _viewModel.ToggleCollection[2].Name}"
IsChecked="{x:Bind _viewModel.ToggleCollection[2].IsSelected, Mode=TwoWay}" />
</MenuFlyout>
</Button.Flyout>
</Button>
MainPageViewModel:
public class MainViewModel : BindableBase
{
public MainViewModel()
{
ToggleCollection = new ObservableCollection<ToggleViewModel>();
var selectToggleAction = new Action<ToggleViewModel>(param => SetToggleSelection(param));
for (int i = 0; i < 3; i++)
{
ToggleCollection.Add(new ToggleViewModel($"Item {i}", selectToggleAction));
}
}
public ObservableCollection<ToggleViewModel> ToggleCollection { get; private set; }
private void SetToggleSelection(ToggleViewModel toggle)
{
var selectedToggle = ToggleCollection.SingleOrDefault(t => t.IsSelected);
if (selectedToggle != toggle)
{
selectedToggle?.SetSelection(false);
toggle.SetSelection(true);
}
}
}
ToggleViewModel:
public class ToggleViewModel : BindableBase
{
private Action<ToggleViewModel> _selectToggleAction;
private bool _isSelected;
public ToggleViewModel(string name, Action<ToggleViewModel> action)
{
Name = name;
_selectToggleAction = action;
}
public string Name { get; set; }
public bool IsSelected
{
get { return _isSelected; }
set
{
if (_isSelected != value)
{
_selectToggleAction(this);
base.OnPropertyChanged();
}
}
}
public void SetSelection(bool selected)
{
_isSelected = selected;
base.OnPropertyChanged("IsSelected");
}
}
Now all the code above works very well. The problem occurs when I try to use regular bindings instead of compiled ones:
<ToggleMenuFlyoutItem
Text="{Binding ToggleCollection[0].Name}"
IsChecked="{Binding ToggleCollection[0].IsSelected, Mode=TwoWay}" />
Binding the properties like this I'm suddenly able to uncheck the currently checked toggle so that none is selected. This is due to the getter of the IsSelected property not being called when I raise the OnPropertyChanged in the setter of the IsSelected property (the reason for using regular bindings is that I want to create the toggles dynamically in code behind, but to illustrate the problem XAML works just as well).
Can anyone explain to me why the {x:Bind} in this case works but not the {Binding}?