Michael
Michael

Reputation: 41

Xamarin - How do I inject a property into a ContentView from the ContentPage or ContentPageViewModel

Update: I've updated this a bit to remove the reference to the error. @michal-diviš gave the correction solution to that. However, my larger issue still remains.


I'm new to Xamarin and trying to learn by making a simple email client. I'm trying to set a property on a ContentPage I have created.

The Setup

The MainPage simply has a grid with two columns; the left side features an CollectionView of the inbox, the right side is my custom ContentPage MessageDisplayView. When an email is clicked in the CollectionView, the CurrentMessage property on the MainPageViewModel is updated to the selected item.

The Issue

I'm trying to bind the property MessageDisplayView.Message to the MainPageViewModel.CurrentMessage property, but the contentpage never updates. I've tried with and without BindableProperty, as well as other ideas found while searching Google and Stackoverflow.

The Question

How do I handle setting and updating a property that I would like to live with the ContentPage?

The Code

MainPage.xaml

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:c="Microsoft.Toolkit.Uwp.UI.Controls"
             xmlns:vm="clr-namespace:Project.ViewModel"
             xmlns:view="clr-namespace:Project.View"
             xmlns:fa="clr-namespace:FontAwesome"
             x:Class="Project.MainPage">
    <ContentPage.BindingContext>
        <vm:MainPageViewModel/>
    </ContentPage.BindingContext>
    <ContentPage.Resources>
        <ResourceDictionary>
            <ResourceDictionary Source="ResourceDictionaries/EmailResourceDictionary.xaml"/>
        </ResourceDictionary>
    </ContentPage.Resources>

    <Grid x:Name="MainPageGrid">

    <!-- other xaml code -->

        <view:MessageDisplayView
            x:Name="MyDisplayView"
            Grid.Column="1"
            Message="{Binding CurrentMessage}"  <!-- Error -->
            />
    </Grid>
</ContentPage>

MainPageViewModel.cs

using MimeKit;
using Project.EmailLogic;
using System.Collections.ObjectModel;
using System.Windows.Input;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;

namespace Project.ViewModel
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public class MainPageViewModel: ObservableObject
    {

            private MimeMessage currentMessage;

            public MimeMessage CurrentMessage
            {
                get => currentMessage; 
                set => SetProperty(ref currentMessage, value, nameof(MessageDisplayView.Message));
            }

        public MainPageViewModel()
        {
            
        }
    }
}

MessageDisplayView.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" 
             xmlns:view="clr-namespace:Project.View" 
             xmlns:vm="clr-namespace:Project.ViewModel" 
             x:DataType="view:MessageDisplayView"
             xmlns:fa="clr-namespace:FontAwesome"
             x:Class="Project.View.MessageDisplayView">

  <ContentView.Content>
      <Grid>

        <!-- Various standard xaml things, for example... -->
            <!-- Subject Line -->
            <Label x:Name="SubjectLine"
                Grid.Row="1"
                   Text="{Binding Message.Subject}"
                />

        </Grid>
  </ContentView.Content>
</ContentView>

MessageDisplayView.xaml.cs

using MimeKit;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;

namespace Project.View
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class MessageDisplayView : ContentView
    {
        private MimeMessage message;
        public MimeMessage Message
        {
            get
            {
                return (MimeMessage)GetValue(MessageProperty);
            }
            set
            {
                SetValue(MessageProperty, value);
                BodyHtmlViewSource.Html = Message.HtmlBody;
            }
        }

        public BindableProperty MessageProperty =
            BindableProperty.Create(nameof(Message), typeof(MimeMessage), typeof(MessageDisplayView));

        public HtmlWebViewSource BodyHtmlViewSource { get; set; }

        public MessageDisplayView()
        {
            InitializeComponent();
            
        }
    }
}

Upvotes: 1

Views: 954

Answers (2)

Michal Diviš
Michal Diviš

Reputation: 2206

Fix

It's the BindableProperty definition! You have (in the MessageDisplayView.xaml.cs):

public BindableProperty MessageProperty = BindableProperty.Create(nameof(Message), typeof(MimeMessage), typeof(MessageDisplayView));

you need to make it static readonly like this:

public static readonly BindableProperty MessageProperty = BindableProperty.Create(nameof(Message), typeof(MimeMessage), typeof(MessageDisplayView));

Usage of INotifyPropertyChanged

The CurrentMessage property in your MainPageViewModel seems to be the problem. You've created it as a BindableProperty, however, that's meant to be used by user controls, not view models.

What you need in the view model is to implement the INotifyPropertyChanged interface and invoke the PropertyChanged event in the property setter. That is done so the UI will update itseld whenever the CurrentMessage property changes.

Tweak your MainViewModel.cs like this:

using MimeKit;
using Project.EmailLogic;
using Xamarin.Forms;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace Project.ViewModel
{
    public class MainPageViewModel : INotifyPropertyChanged
    {
        private MimeMessage currentMessage;
        public MimeMessage CurrentMessage
        {
            get => currentMessage;
            set {
                currentMessage = value;
                OnPropertyChanged(nameof(CurrentMessage))
            };
        }
        
        public event PropertyChangedEventHandler? PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = "") =>
         PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

In this example, I've implemented the INotifyPropertyChanged directly in you view model, but a better way to do it is to inherit from a base class that already has that implemented, like this one: ObservableObject from James Montemagno's MVVM Helpers library. The resulting view model would look like this:

using MimeKit;
using Project.EmailLogic;
using MvvmHelpers;

namespace Project.ViewModel
{
    public class MainPageViewModel : ObservableObject
    {
        private MimeMessage currentMessage;
        public MimeMessage CurrentMessage
        {
            get => currentMessage;
            set => SetProperty(ref currentMessage, value);
        }
    }
}

EDIT: Lately I've been using the CommunityToolkit.Mvvm library instead of Refactored.MvvmHelpers as it's more updated and feature rich.

Upvotes: 1

Michael
Michael

Reputation: 41

The problem was the BindableObject was not hearing the notifications of the property changing.

The solution was to add the OnPropertyChanged method to the code behind of the ContentView, not the ContentPageViewModel.

This "solution" correctly updates the property in the code, but it does not update the xaml/UI. I think this might a separate issue.


This confused me at first, when @michal-diviš pointed out the OnPropertyChanged calls, as I thought I was suppose to wire up the event subscription myself in the ContentView code behind. But after stumbling across this article, I realized that the method was required elsewhere.

I feel like a major issue is that there isn't a lot of information about passing data or properties between elements/UserControls/ContentPages, etc. Over the last two days, I've read and watched a fair amount on BindableProperties, but seen very little use of OnPropertyChanged or updating the properties from elsewhere. Perhaps I'm missing the places where it's talked about, or maybe it's more easy or obvious than I realize, but in hindsight, this seems like something that should have been mentioned in every BindableProperty 101.

Beyond the official documentation of course, if anyone knows a good article or video going over sharing/binding/updating properties between classes/views/whatever, I'd love to check that out.

Here's an example of the final, working code:

    public partial class MessageDisplayView : ContentView
    {
        public MimeMessage Message
        {
            get
            {
                return (MimeMessage)GetValue(MessageProperty);
            }
            set
            {
                SetValue(MessageProperty, value);
            }
        }

        public static BindableProperty MessageProperty =
            BindableProperty.Create(nameof(Message), typeof(MimeMessage), typeof(MessageDisplayView), new MimeMessage(),
                BindingMode.TwoWay);

        protected override void OnPropertyChanged(string propertyName = null)
        {
            base.OnPropertyChanged(propertyName);

            if (propertyName == MessageProperty.PropertyName)
            {
                if(Message != null)
                {
                    // Update ContentView properties and elements.
                }
            }
        }

Thank you again to @michal-diviš for your help!

Upvotes: 1

Related Questions