Reputation: 135
So I am trying to work out data binding in a ContentView with a view model. I thought this should be pretty easy since MVVM is supposed to be the thing for MAUI but maybe I am missing something. The current solution is based on Databinding issue
So I have a simple ContentView like this:
<?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:MyProject.ViewModels"
x:Name="view"
x:Class="MyProject.Components.ContentViewComponent">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="10"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="10"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="10"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="1" Grid.Column="1"
//This NEVER picks up the value of Title >> Why??
Text="{Binding VM.Title, Source={x:Reference view}}"
FontSize="24"
FontAttributes="Bold"/>
</Grid>
</ContentView>
And the Code-Behind for my simple ContentView:
using MyProject.ViewModels;
namespace MyProject.Components;
public partial class ContentViewComponent: ContentView
{
internal MyViewModel VM { get; set; }
public static readonly BindableProperty TProperty = BindableProperty.Create(nameof(T), typeof(string), typeof(MetricImperialDropdownConverter), string.Empty, propertyChanged : TitleChanged);
private static void TitleChanged(BindableObject bindable, object oldvalue, object newvalue)
{
//This fires and sets Title to T
((ContentViewComponent)bindable).VM.Title = (string)newvalue;
}
//I want to be able to set this when reusing the component
public string T
{
get => (string)GetValue(TProperty);
set => SetValue(TProperty, value);
}
public MetricImperialDropdownConverter()
{
VM = new MyViewModel();
InitializeComponent();
}
}
And then I have a ViewModel for that like this:
using System.ComponentModel;
namespace MyProject.ViewModels
{
public class MetricImperialDropdownConverterViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnProperyChanged(string propertyName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
private string _title = string.Empty;
public string Title
{
get { return _title; }
set { _title = value; OnProperyChanged(nameof(Title)); }
}
}
And then to use this and pass in a value:
<?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:components="clr-namespace:MyProject.Components"
x:Class="MyProject.Pages.SomePage"
x:Name="this">
<VerticalStackLayout BindingContext="{x:Reference this}">
//This works and sets T correctly
<components:ContentViewCompontent T="Here is my Title"/>
</VerticalStackLayout>
</ContentPage>
T
for the component is correctly set.
On setting T
, the Title
property in my ViewModel, VM
, is through the PropertyChanged
event.
But the UI is never updated with the value for Title
.
I assume it is because the UI doesn't respond to events that happen outside their own context.
But what should I do in this case?? How can I get the UI to update correctly??
Upvotes: 5
Views: 7288
Reputation: 123
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:myApp.ViewModels"
x:Class="mynamespace.Views.ContentViews.MyContentView"
x:Name="this">
<Grid BindingContext="{Binding BindingContext, Source={x:Reference this}}"
ColumnDefinitions="*,Auto"
x:DataType="vm:MySubViewmodel"
ColumnSpacing="10">
// here you have Intellisense access to MySubViewmodel and all its properties
</Grid>
</ContentView>
this can be used in a page like this:
<mynamespace:MyContentView BindingContext="{Binding MySubViewmodel}"/>
this way you have access to Intellisense and it works with compiled bindings too. The subviewmodel should be instantiated in the constructor of the page viewmodel and can therefore have access to all DI objects too. All DI objects that are needed in the subviewmodel need to be accessed by the page viewmodels constructor and passed down to the subviewmodel, like so:
public MySubViewmodel MySubViewmodel { get; private set; }
public PageViewModel(ILogger<PageViewModel> logger, DataService dataservice){
MySubViewmodel = new MySubViewmodel(dataservice);
}
Upvotes: 0
Reputation: 1921
What I can tell from your code you set the Label from the View and not from the ViewModel and to be frank you should not have the ContentView communicating with the ViewModel. It should be resuable. The View communicate with the ViewModel and with the control.
I think you are mixing ContentPage (Views) and ContentViews (controls). So the complete ContentView should look something like this:
public partial class ContentViewComponent : ContentView
{
public static readonly BindableProperty TProperty = BindableProperty.Create(nameof(T), typeof(string), typeof(ContentViewComponent), string.Empty, BindingMode.OneWay, propertyChanged: TitleChanged);
public ContentViewComponent()
{
InitializeComponent();
}
private static void TitleChanged(BindableObject bindable, object oldvalue, object newvalue)
{
((ContentViewComponent)bindable).MyLabel.Text = (string)newvalue;
}
public string T
{
get => (string)GetValue(TProperty);
set => SetValue(TProperty, value);
}
}
And the xaml:
<ContentView
x:Class="MauiTest.Controls.ContentViewComponent"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<VerticalStackLayout>
<Label
x:Name="MyLabel"
Text=""
TextColor="Black" />
</VerticalStackLayout>
Upvotes: 4
Reputation: 13889
Based on your code, I achieved this function, you can refer to the following code:
ContentViewComponent.xaml.cs
Add a Bindable Property YourName
. You can change it to yours.
public partial class ContentViewComponent : ContentView
{
public String YourName
{
get
{
String value = (String)GetValue(YourNameProperty);
return value;
}
set
{
SetValue(YourNameProperty, value);
}
}
public static readonly BindableProperty YourNameProperty = BindableProperty.Create(nameof(YourName)
, typeof(String)
, typeof(ChildView), defaultBindingMode: BindingMode.TwoWay, propertyChanged: OnYourNameChanged);
static void OnYourNameChanged(BindableObject bindable, object oldValue, object newValue)
{
Console.WriteLine("-----------------> " + newValue);
}
public ContentViewComponent()
{
InitializeComponent();
}
}
ContentViewComponent.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"
x:Class="MauiContentViewApp.ContentViewComponent"
x:Name="view"
>
<VerticalStackLayout>
<Label Text="{Binding Source={x:Reference view}, Path=YourName}"
VerticalOptions="Center"
HorizontalOptions="Center" />
</VerticalStackLayout>
</ContentView>
MetricImperialDropdownConverterViewModel.cs
In this view model, I added a command for a Button to update the value of Title
public class MetricImperialDropdownConverterViewModel: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnProperyChanged(string propertyName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
private string _title = string.Empty;
public string Title
{
get { return _title; }
set { _title = value; OnProperyChanged(nameof(Title)); }
}
public MetricImperialDropdownConverterViewModel()
{
Title = "initial title";
}
public ICommand ChangeNameCommand => new Command(changeMethod);
private void changeMethod()
{
Title = "update data here";
}
}
Usage example:
<?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"
x:Class="MauiContentViewApp.NewPage2"
xmlns:mauiapp="clr-namespace:MauiContentViewApp"
Title="NewPage2">
<ContentPage.BindingContext>
<mauiapp:MetricImperialDropdownConverterViewModel></mauiapp:MetricImperialDropdownConverterViewModel>
</ContentPage.BindingContext>
<VerticalStackLayout>
<mauiapp:ContentViewComponent YourName="{Binding Title}" ></mauiapp:ContentViewComponent>
<Button Text="change value" Command="{Binding ChangeNameCommand}"></Button>
</VerticalStackLayout>
</ContentPage>
Upvotes: 1