njun
njun

Reputation: 135

.Net Maui - ContentView with a ViewModel

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

Answers (3)

Max
Max

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

Peter Wessberg
Peter Wessberg

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

Jessie Zhang -MSFT
Jessie Zhang -MSFT

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

Related Questions