.NET Maui Binding a contentview to parent ViewModel MVVM - xamarin.forms

I have a xaml page that contains two instances of the same content view. The content view have a datepicker which should update a value in the parent view model ( each content view should update a different variable in the view model). I tried to do the bindiable property but it's not working. I set the BindingMode to TwoWay but that's not working.
The issue is that the binding is not working from the contentview to the parent viewmodel through the bindiable property. Any input is much appreciated.
Below is my code:
MainPage.xaml
<?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"
BackgroundColor="{DynamicResource PageBackgroundColor}"
xmlns:picker="clr-namespace:TestSync.View"
xmlns:viewmodel="clr-namespace:TestSync.ViewModel"
x:DataType="viewmodel:TimeTrackerViewModel"
x:Class="TestSync.MainPage">
<VerticalStackLayout>
<Label Text="{Binding SelectedDate}"/>
<Label Text="{Binding SelectedDate1}"/>
<picker:DateTimePickerContentView CardTitle="First DatePicker" CardDate="{Binding SelectedDate,Mode=TwoWay}" />
<picker:DateTimePickerContentView CardTitle="Second DatePicker" CardDate="{Binding SelectedDate1,Mode=TwoWay}" />
</VerticalStackLayout>
</ContentPage>
TimeTrackerViewModel.cs
namespace TestSync.ViewModel
{
public partial class TimeTrackerViewModel :ObservableObject
{
[ObservableProperty]
public DateTime selectedDate;
[ObservableProperty]
public DateTime selectedDate1;
}
}
DateTimePickerContentView.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodel="clr-namespace:TestSync.View"
x:DataType="viewmodel:DateTimePickerContentView"
x:Class="TestSync.View.DateTimePickerContentView"
>
<VerticalStackLayout>
<Label Text="{Binding CardTitle}"/>
<DatePicker x:Name="myDate" Date="{Binding CardDate}" />
</VerticalStackLayout>
</ContentView>
and DateTimePickerContetntView.xaml.cs
namespace TestSync.View;
public partial class DateTimePickerContentView : ContentView
{
public static readonly BindableProperty CardTitleProperty = BindableProperty.Create(nameof(CardTitle), typeof(string), typeof(DateTimePickerContentView), string.Empty);
public string CardTitle
{
get => (string)GetValue(DateTimePickerContentView.CardTitleProperty);
set => SetValue(DateTimePickerContentView.CardTitleProperty, value);
}
public static readonly BindableProperty CardDateProperty = BindableProperty.Create(nameof(CardDate), typeof(DateTime), typeof(DateTimePickerContentView), defaultValue:DateTime.Parse("12/15/1992"),defaultBindingMode:BindingMode.TwoWay,propertyChanged:test);
private static void test(BindableObject bindable, object oldValue, object newValue)
{
var mytest= bindable as DateTimePickerContentView;
mytest.myDate.Date = (DateTime)newValue;
}
public DateTime CardDate
{
get => (DateTime)GetValue(DateTimePickerContentView.CardDateProperty);
set => SetValue(DateTimePickerContentView.CardDateProperty, value);
}
public DateTimePickerContentView()
{
InitializeComponent();
BindingContext = this;
}
}

I give you a workaround here.
For DateTimePickerContentView.xaml, define the BindingContext
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
...
x:Name="this">
<VerticalStackLayout BindingContext="{x:Reference this}">
<Label Text="{Binding CardTitle}"/>
<DatePicker x:Name="myDate" Date="{Binding CardDate}" />
</VerticalStackLayout>
</ContentView>
So for DateTimePickerContentView.cs, just delete this line
...
public DateTimePickerContentView()
{
InitializeComponent();
//BindingContext = this;
}
For data binding in a ContentView, you could refer to this official doc: Define the UI.
And if you want to set a default value, you should set it in TimeTrackerViewModel, because TimeTrackerViewModel's constructor execute after custom control set the default value. Then it will be replaced such as 1/1/1900 .
public TimeTrackerViewModel()
{
SelectedDate = DateTime.Parse("12/15/1992");
SelectedDate1 = DateTime.Parse("12/15/1992");
}
Hope it works for you.

Related

Could not bind to nested BindableProperty

I have created bindable property called Text in TargetClass.cs. That Text property is nested bindable property.
TargetClass.cs :
public class TargetClass : BindableObject
{
public static readonly BindableProperty TextProperty =
BindableProperty.Create("Text", typeof(string), typeof(TargetClass), "Default", BindingMode.TwoWay, null,
OnTextChanged);
private static void OnTextChanged(BindableObject bindable, object oldValue, object newValue)
{
}
public string Text
{
get
{
return (string)GetValue(TextProperty);
}
set
{
SetValue(TextProperty, value);
}
}
}
Then I have created MyView.cs
public class MyView : ContentView
{
private TargetClass target;
Label label;
public TargetClass Target
{
get
{
return target;
}
set
{
target = value;
label.Text = target.Text;
}
}
public MyView()
{
label = new Label();
label.FontSize = 50;
Content = label;
}
}
ViewModel.cs :
public class ViewModel : INotifyPropertyChanged
{
private string m_text = "New Value";
public string TextValue
{
get { return m_text; }
set
{
m_text = value;
OnPropertyChanged("TextValue");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
MainPage.xaml :
<ContentPage 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"
xmlns:local="clr-namespace:BindingDemo"
x:Class="BindingDemo.MainPage">
<ContentPage.BindingContext>
<local:ViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources>
<ResourceDictionary>
<local:TargetClass x:Key="target" Text="{Binding TextValue}"/>
</ResourceDictionary>
</ContentPage.Resources>
<StackLayout>
<local:MyView Target="{StaticResource target}"/>
</StackLayout>
It is working when I give some string value to Text property like,
<ContentPage.BindingContext>
<local:ViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources>
<ResourceDictionary>
<local:TargetClass x:Key="target" Text="Hello World"/>
</ResourceDictionary>
</ContentPage.Resources>
<StackLayout>
<local:MyView Target="{StaticResource target}"/>
</StackLayout>
in xaml.
But it doesnt work in MVVM binding When I bind the Text property like,
<ContentPage.BindingContext>
<local:ViewModel />
</ContentPage.BindingContext>
<ContentPage.Resources>
<ResourceDictionary>
<local:TargetClass x:Key="target" Text="{Binding TextValue}"/>
</ResourceDictionary>
</ContentPage.Resources>
<StackLayout>
<local:MyView Target="{StaticResource target}"/>
</StackLayout>
There is no problem with MVVM binding(ViewModel.cs) because it works well with another bindable property.
But it works if the Text property is added in MyView.cs and used like MyView.Text. It doesn't work only when it is added in TargetClass.cs and used like MyView.TargetClass.Text.
Is it possible to use the nested property in data binding in Xamarin Forms?
I don't think objects in the resource dictionary get a BindingContext assigned to them.
So you should modify it from code-behind to assign a context to any resource of type BindableObject
in your page.axml.cs:
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
if (this.Resources != null)
{
foreach (var resource in this.Resources.Values.OfType<BindableObject>())
{
resource.BindingContext = this.BindingContext;
}
}
}

ControlTemlate Command Binding

Microsoft documentation shows how to inherit from a ControlTemplate and use a ContentPresenter.
It shows how to use string properties to populate string bound items in the template. (e.g. HeaderText)
It doesn't show how to do the same with commands. I want to drive the command behavior of a button in the template via the implementing contentpage/viewmodel.
Following the property example, I tried the same with an ICommand but it gets ignored. Meaning, the button isn't executing the provided command. Is commanding not supported?
Example
This is in my ControlTemplate, called ApplicationChrome.xaml
<Label Grid.Row="0"
Margin="20,0,0,0"
Text="{TemplateBinding HeaderText}"
TextColor="White"
FontSize="Title"
VerticalOptions="Center"/>
<Button Grid.Column="0"
x:Name="LeftButton"
Margin="20,0,0,0"
Text="Change Label"
TextColor="White"
HorizontalOptions="Start"
VerticalOptions="Center"
Command="{TemplateBinding LeftButtonTemplateCommand}"
The code-behind defines both Bindable Properties
public static readonly BindableProperty HeaderTextProperty = BindableProperty.Create("HeaderText", typeof(string), typeof(ContentPage), null, BindingMode.TwoWay);
public string HeaderText
{
get => (string)GetValue(HeaderTextProperty);
set => SetValue(HeaderTextProperty, value);
}
public static readonly BindableProperty LeftButtonTemplateCommandProperty = BindableProperty.Create("LeftButtonCommand", typeof(ICommand), typeof(ApplicationChrome), null);
public ICommand LeftButtonTemplateCommand
{
get => (ICommand) GetValue(LeftButtonTemplateCommandProperty);
set => SetValue(LeftButtonTemplateCommandProperty, value);
}
My implementing view sets both Bindables
<core:ApplicationChrome 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:core="clr-namespace:FEOD.Core;assembly=FEOD"
mc:Ignorable="d"
HeaderText="FE | Home"
LeftButtonTemplateCommand="{Binding LeftButtonCommand}"
x:Class="FEOD.Views.HomeView">
The implementing view's BindingContext is set to it's viewmodel which defines the LeftButtonCommand
public ICommand LeftButtonCommand { get; private set; }
private static void OnLeftButtonClicked(object obj)
{
var a = 1;
}
public HomeViewModel()
{
LeftButtonCommand = new Command(OnLeftButtonClicked);
}
The bound HeaderText displays "FE | Home" just fine. But the bound command never fires OnLeftButtonClicked.
The first parameter of BindableProperty.Create() method has to be "LeftButtonTemplateCommand" not "LeftButtonCommand". The Property name has to exactly match for Binding to work.

Nested views in tabs binding to separate ViewModel

Is there a way how to provide a nested view it's own viewmodel?
Example:
Master view of type TabbedView has multiple tabs.
<mvx:MvxTabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Foo.Core.Pages.Access.MainPage"
xmlns:res="clr-namespace:Foo.Core.Resources;assembly=Foo.Core"
xmlns:mvx="clr-namespace:MvvmCross.Forms.Views;assembly=MvvmCross.Forms"
xmlns:views="clr-namespace:Foo.Core.Pages.Access">
<TabbedPage.Children>
<views:LoginPage></views:LoginPage>
<views:RegisterPage></views:RegisterPage>
</TabbedPage.Children>
</mvx:MvxTabbedPage>
The LoginPage and RegisterPage are in separate views. But all the binding must be in the MainViewModel and I want the bindings to be separately in the LoginViewModel and RegisterViewModel.
Is there a way how to setup the binding to the properties to appropriate viewmodel? Preferably in XAML.
In order that to work you need to let the NavigationService (and therefore the Presenter) to load the children pages:
Xamarin.Forms View Presenter -> MvxTabbedPagePresentationAttribute
In your case it should be something like:
ViewModels
public class MyTabsContainerViewModel : MvxViewModel
{
private readonly IMvxNavigationService _navigationService;
public MyTabsContainerViewModel(IMvxNavigationService navigationService)
{
_navigationService = navigationService ?? throw new ArgumentNullException(nameof(navigationService));
}
public override async void ViewAppearing()
{
await ShowInitialViewModels();
base.ViewAppearing();
}
private async Task ShowInitialViewModels()
{
var tasks = new List<Task>();
tasks.Add(_navigationService.Navigate<LoginViewModel>());
tasks.Add(_navigationService.Navigate<RegisterViewModel>());
await Task.WhenAll(tasks);
}
}
public class LoginViewModel : MvxViewModel
{
}
public class RegisterViewModel : MvxViewModel
{
}
Views
MyTabsContainerPage.xaml
<?xml version="1.0" encoding="utf-8"?>
<views:MvxTabbedPage x:TypeArguments="viewModels:MyTabsContainerViewModel" xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewModels="clr-namespace:Foo.Core.ViewModels;assembly=Foo.Core"
xmlns:views="clr-namespace:MvvmCross.Forms.Views;assembly=MvvmCross.Forms"
x:Class="Foo.Core.Pages.Access.MyTabsContainerPage">
</views:MvxTabbedPage>
MyTabsContainerPage.xaml.cs
[XamlCompilation(XamlCompilationOptions.Compile)]
[MvxMasterDetailPagePresentation(Position = MasterDetailPosition.Detail, NoHistory = true)]
public partial class MyTabsContainerPage : MvxTabbedPage<MyTabsContainerViewModel>
{
public MyTabsContainerPage()
{
InitializeComponent();
}
}
LoginPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<views:MvxContentPage x:TypeArguments="viewModels:LoginViewModel" xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewModels="clr-namespace:Foo.Core.ViewModels;assembly=Foo.Core"
xmlns:views="clr-namespace:MvvmCross.Forms.Views;assembly=MvvmCross.Forms"
x:Class="Foo.Core.Pages.Access.MixedNavTab1Page">
<StackLayout>
<Label Text="This is Tab 1" />
</StackLayout>
</views:MvxContentPage>
LoginPage.xaml.cs
[XamlCompilation(XamlCompilationOptions.Compile)]
[MvxTabbedPagePresentation(WrapInNavigationPage = false, Title = "LoginTab1")]
public partial class LoginPage : MvxContentPage<LoginViewModel>
{
public LoginPage()
{
InitializeComponent();
}
}
RegisterPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<views:MvxContentPage x:TypeArguments="viewModels:RegisterViewModel" xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewModels="clr-namespace:Foo.Core.ViewModels;assembly=Foo.Core"
xmlns:views="clr-namespace:MvvmCross.Forms.Views;assembly=MvvmCross.Forms"
x:Class="Foo.Core.Pages.Access.RegisterPage">
<StackLayout>
<Label Text="This is Tab 2" />
</StackLayout>
</views:MvxContentPage>
RegisterPage.xaml.cs
[XamlCompilation(XamlCompilationOptions.Compile)]
[MvxTabbedPagePresentation(WrapInNavigationPage = false, Title = "RegisterTab2")]
public partial class RegisterPage : MvxContentPage<RegisterViewModel>
{
public RegisterPage()
{
InitializeComponent();
}
}
Full sample in the Playground project
HIH

MasterDetail navigation using prism

I recently started building a Xamarin Forms application using Prism.
I'm not able to navigate with the MasterDetail Navigation. The button I use to navigate seems to not make the binding correctly. I never been able to reach the executed command with a breakpoint when clicking on the button.
Everything except the command binding seems to do the binding correctly, so I really have no idea on what is going on.
I already checked out the GitHub sample made available by the Prism team (HamburgerMenu project). I'm convince to use the exact same configuration as the sample but no way to make it works on my project.
Bellow is the code used currently:
MainPage.xaml
<MasterDetailPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MonkeyVault.Views.MainPage">
<MasterDetailPage.Master>
<NavigationPage Title="Required Foo" Icon="ic_menu.png">
<x:Arguments>
<ContentPage Title="Menu">
<StackLayout Padding="40">
<Label Text="{Binding UserName, StringFormat='Hello, {0}'}"/>
<Button Text="Sites" Command="{Binding NavigateCommand}" CommandParameter="Navigation/Sites" />
</StackLayout>
</ContentPage>
</x:Arguments>
</NavigationPage>
</MasterDetailPage.Master>
</MasterDetailPage>
MainPageViewModel.cs
public class MainPageViewModel : BaseViewModel
{
#region Fields
private string _userName;
#endregion
#region Properties
public string UserName
{
get => _userName;
set => SetProperty(ref _userName, value);
}
public DelegateCommand<string> NavigateCommand;
public DelegateCommand NCommand;
#endregion
public MainPageViewModel(INavigationService navigationService)
: base(navigationService)
{
Title = "Main Page";
NavigateCommand = new DelegateCommand<string>(OnNavigateCommandExecuted);
}
private async void OnNavigateCommandExecuted(string path)
{
await _navigationService.NavigateAsync(path);
}
}
If someone has already encountered this problem or has any idea I would be greatful.
You need to create your DelegateCommand as a Property.
public DelegateCommand<string> NavigateCommand { get; set; }
Admittedly I am just guessing here, but I have had problems binding to Fields before and needed to change it to a property to get binding.

xamarin forms FlowListView not showing data from List

I have a FlowListView control that is not displaying the items from a list. For test purposes I just wanted to display file path of the photo as a label.
Here is the xaml code:
<?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:controls="clr-namespace:XLabs.Forms.Controls;assembly=XLabs.Forms.Controls"
xmlns:flv="clr-namespace:DLToolkit.Forms.Controls;assembly=DLToolkit.Forms.Controls.FlowListView"
x:Class="TamarianApp.ImagePage">
<ContentPage.Content>
<flv:FlowListView FlowColumnCount="3" SeparatorVisibility="None" HasUnevenRows="true" x:Name="image_gallary" ItemsSource="{Binding photos}" HeightRequest="100" VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand">
<flv:FlowListView.FlowColumnTemplate>
<DataTemplate>
<Label Text="{Binding filepath}" TextColor="Black" Margin="20" VerticalOptions="Fill" HorizontalOptions="Fill" XAlign="Center" YAlign="Center"></Label>
</DataTemplate>
</flv:FlowListView.FlowColumnTemplate>
</flv:FlowListView>
</ContentPage.Content>
</ContentPage>
Here is the backend:
public partial class ImagePage : ContentPage
{
public List<Photo> photos = App.rug.photos
public ImagePage()
{
InitializeComponent();
Title = "Photos";
}
}
'App.rug.photos' is a List of the Photo class which contains the string field 'filepath'. Debugging shows field 'photos' is not empty and contains the data from 'App.rug.photos.'
Please Help.
It may be to do with how you are assigning the list, you need to make sure that the list is a Property in order for the binding to work correctly.
Try..
public partial class ImagePage : ContentPage
{
public List<Photo> photos {get; set;}
public ImagePage()
{
InitializeComponent();
Title = "Photos";
photos = App.rug.photos
}
}
You may also need to set the binding context
public partial class ImagePage : ContentPage
{
public List<Photo> photos {get; set;};
public ImagePage()
{
InitializeComponent();
this.BindingContext = this;
Title = "Photos";
photos = App.rug.photos
}
}
Try this code.
public ObservableCollection<object>() photos;
...
var oc = new ObservableCollection<Photo>();
foreach (var item in App.rug.photos)
oc.Add(item);
photos = oc as ObservableCollection<object>;
I was having trouble with ObservableCollection<MyObject>, but solved it by using ObservableCollection<object>.

Resources