0

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??

Clemens
  • 123,504
  • 12
  • 155
  • 268
njun
  • 65
  • 10
  • Is this even compiling? – H.A.H. Jul 19 '23 at 06:17
  • 2
    you can only bind to **public properties**. `VM` is not public. Further, `ContentViews` do not typically have their own VM, they inherit their `BindingContext` from whatever page or other container they are placed within. – Jason Jul 19 '23 at 11:36
  • NOTE: Since you already linked the Q&A that attempts to cover this topic, [.Net MAUI data binding not carrying through to custom component](https://stackoverflow.com/questions/73011596/net-maui-data-binding-not-carrying-through-to-custom-component), I've altered my answer there to clarify that a component usually DOES NOT have a viewmodel. And to emphasize the correct use of BindableProperties to control its behavior. Since these points of Peter Wessberg's answer were useful to you. [This question should now be considered a duplicate.] – ToolmakerSteve Jul 19 '23 at 19:10

2 Answers2

1

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>
Peter Wessberg
  • 879
  • 6
  • 13
  • While the comment provided by @Jason was the direct solution to my problem, this answer gives me what I was really missing and was not able to find anywhere. I was not aware that ViewModels were not meant to be used with ContentViews. Thank you. – njun Jul 19 '23 at 17:23
0

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>
Jessie Zhang -MSFT
  • 9,830
  • 1
  • 7
  • 19