Xamarin - 如何从 ContentPage 或 ContentPageViewModel 将 属性 注入 ContentView

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

更新: 我对此进行了一些更新以删除对错误的引用。 @michal-diviš 对此给出了更正解决方案。然而,我更大的问题仍然存在。


我是 Xamarin 的新手,正在尝试通过制作一个简单的电子邮件客户端来学习。我正在尝试在我创建的 ContentPage 上设置 属性。

设置

MainPage 只是一个包含两列的网格;左侧是收件箱的 CollectionView,右侧是我的自定义 ContentPage MessageDisplayView。在 CollectionView 中单击电子邮件时,MainPageViewModel 上的 CurrentMessage 属性 将更新为所选项目。

问题

我正在尝试将 属性 MessageDisplayView.Message 绑定到 MainPageViewModel.CurrentMessage 属性,但内容页面从未更新。我尝试过使用和不使用 BindableProperty,以及在搜索 Google 和 Whosebug 时发现的其他想法。

问题

如何设置和更新我希望与 ContentPage 一起使用的 属性?

代码

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();
            
        }
    }
}

修复

BindableProperty的定义! 你有(在 MessageDisplayView.xaml.cs):

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

你需要像这样 static readonly:

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

INotifyPropertyChanged 的​​用法

您的 MainPageViewModel 中的 CurrentMessage 属性 似乎是问题所在。您已将其创建为 BindableProperty,但是,这意味着要由用户控件而不是视图模型使用。

你需要在视图模型中实现INotifyPropertyChanged接口并调用属性setter中的PropertyChanged事件。这样做是为了 UI 会在 CurrentMessage 属性 发生变化时更新它自己。

像这样调整你的MainViewModel.cs

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));
    }
}

在这个例子中,我直接在你的视图模型中实现了 INotifyPropertyChanged,但更好的方法是从已经实现的基础 class 继承,比如这个:ObservableObject from James Montemagno's MVVM Helpers library。生成的视图模型如下所示:

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);
        }
    }
}

问题是 BindableObject 没有听到 属性 更改的通知。

解决方案是将 OnPropertyChanged 方法添加到 ContentView 的代码后面,而不是 ContentPageViewModel

此“解决方案”正确地更新了代码中的 属性,但没有更新xaml/UI。我认为这可能是一个单独的问题。


起初这让我感到困惑,当@michal-diviš 指出 OnPropertyChanged 调用时,我认为我应该在后面的 ContentView 代码中自己连接事件订阅。但是在偶然发现 this article 之后,我意识到该方法在其他地方是必需的。

我觉得一个主要问题是关于在 elements/UserControls/ContentPages 之间传递数据或属性的信息不多。在过去的两天里,我阅读和观看了相当多的内容在 BindableProperties 上,但很少看到 OnPropertyChanged 的​​使用或从其他地方更新属性。也许我遗漏了谈论它的地方,或者它比我意识到的更容易或更明显,但事后看来,这似乎应该在每个 BindableProperty 101 中提到。

当然,除了官方文档之外,如果有人知道关于 classes/views/whatever 之间 sharing/binding/updating 属性的好文章或视​​频,我很乐意查看。

这是最终的工作代码示例:

    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.
                }
            }
        }

再次感谢@michal-diviš 的帮助!