Xamarin.Forms从代码后台访问ImagePickerViewModel始终返回null
我正在学习Xamarin.Forms,目前开发了一款可调用相机拍照、将照片显示在FlowListView中的控件,最终希望将这些照片转为邮件附件发送。该控件的相册展示功能正常,邮件功能由他人编写,但我无法从邮件页面(EmailCreatePage)的代码后台访问该控件的ImagePickerViewModel:每次尝试获取AttachmentList的内容时都返回null,即使不在邮件代码后台初始化ImagePickerViewModel,结果依然如此。请问正确的实现方式是什么?
ImagePickerViewModel.cs
[XamlCompilation(XamlCompilationOptions.Compile)] public class ImagePickerViewModel : BindableObject, INotifyPropertyChanged { // HERE WOULD BE A TON OF DEPENDENCIES public ImagePickerViewModel() { // Commands TakePictureCmd = new Command(TakePicture); //ObservableCollection TakenPicturesList = new ObservableCollection<Model.ImagePicker>(); AttachmentList = new List<BaseObject>(); } public event PropertyChangedEventHandler PropertyChanged; public Model.ImagePicker NewPicture { get; set; } public BaseObject attachment { get; set; } public List<BaseObject> AttachmentList { get; set; } public ObservableCollection<Model.ImagePicker> TakenPicturesList { get; set; } public string selectedImage { get; set; } public string Path { get { return NewPicture.ImagePath; } set { NewPicture.ImagePath = value; OnPropertyChanged(nameof(Path)); } } public Command TakePictureCmd { get; set; } private async void TakePicture() { var path = await _cameraService.TakePictureAsync(); TakenPicturesList.Add(new Model.ImagePicker { ImagePath = path }); GetFilesForAttachment(path); } private void GetFilesForAttachement(string filePath) { attachment = Attachment.FromFile(filePath); if (attachment != null) { AttachmentList.Add(attachment); } } public void RaisedOnPropertyChanged(string _PropertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(_PropertyName)); } }
ImagePickerView.xaml
<ContentView xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:xct="http://xamarin.com/schemas/2020/toolkit" xmlns:flv="clr-namespace:DLToolkit.Forms.Controls;assembly=DLToolkit.Forms.Controls.FlowListView" xmlns:ffimageloading="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms" xmlns:local="clr-namespace:SolIT.Mobile.Client" xmlns:vm="clr-namespace:SolIT.Mobile.Client.Features.Shared.Controls.ImagePicker.ViewModel" xmlns:buttons="clr-namespace:Syncfusion.XForms.Buttons;assembly=Syncfusion.Buttons.XForms" x:Class="SolIT.Mobile.Client.Features.Shared.Controls.ImagePicker.View.ImagePickerView" x:Name="View_ImagePickerView"> <ContentView.BindingContext> <vm:ImagePickerViewModel ContextView="{x:Reference View_ImagePickerView}"/> </ContentView.BindingContext> <Frame> <ContentView.Content> <StackLayout> <Grid ColumnSpacing="0" HorizontalOptions="CenterAndExpand"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <buttons:SfButton x:Name="Btn_takePicture" Grid.Column="1" Text="{local:Translate Common.SelectFilesPage.TakePicture}" TextColor="Black" ImageSource="{Binding TakePictureIcon}" ShowIcon="true" BackgroundColor="Transparent" ImageAlignment="Top" HorizontalOptions="FillAndExpand" Margin="3" CornerRadius="5" Command="{Binding TakePictureCmd}"/> </Grid> <BoxView Grid.ColumnSpan="2" Grid.Row="1" HeightRequest="1" BackgroundColor="{StaticResource Win10Devider}"/> <flv:FlowListView x:Name="pictureFlowListView" SeparatorVisibility="None" HasUnevenRows="true" FlowColumnMinWidth="110" FlowItemsSource="{Binding TakenPicturesList}"> <!--FlowItemTappedCommand="{Binding PictureTappedCmd}" FlowLastTappedItem="{Binding SelectedItem}"--> <flv:FlowListView.FlowColumnTemplate> <DataTemplate> <Grid Padding="3"> <Grid.RowDefinitions> <RowDefinition Height="*" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <StackLayout> <ffimageloading:CachedImage HeightRequest="100" Aspect="AspectFill" DownsampleHeight="100" DownsampleUseDipUnits="false" x:Name="imageCached" Source="{Binding ImagePath}"> <ffimageloading:CachedImage.GestureRecognizers> <TapGestureRecognizer Command="{Binding Path=BindingContext.PictureTappedCmd, Source={x:Reference pictureFlowListView}}" CommandParameter="{x:Reference imageCached}"></TapGestureRecognizer> </ffimageloading:CachedImage.GestureRecognizers> </ffimageloading:CachedImage> </StackLayout> </Grid> </DataTemplate> </flv:FlowListView.FlowColumnTemplate> </flv:FlowListView> </StackLayout> </ContentView.Content> </Frame> </ContentView>
EmailCreatePage.xaml.cs
[XamlCompilation(XamlCompilationOptions.Compile)] public partial class EmailCreatePage: ContentPage { // HERE WOULD BE A TON OF DEPENDENCIES public EmailCreateViewModel ViewModel { get; } public ImagePickerViewModel ViewModelImagePicker {get;} public EmailCreatePage() { InitializeComponent(); BindingContext = ViewModel = new EmailCreateViewModel(); ViewModelImagePicker = new ImagePickerViewModel(); } private void CreateAttachment() { var attachmentList = GetFileList(); foreach (var file in attachmentList) { var attachment = Attachment.FromFile(file.ToString()); if (attachment != null) { ViewModel.CubesMessage.ConnectedObjects.Add(attachment); } } public List<BaseObject> GetFileList() { // GET LIST FROM OTHER VIEWMODEL HERE // VIEWMODEL IS ALWAYS NULL var attachmentList = ImagePickerViewModel.AttachmentList; return attachmentList; } // HERE WOULD BE THE CODE THAT SENDS THE EMAIL OFF AFTER A BUTTON CLICK
问题根源
你当前的代码里,EmailCreatePage中新建的ImagePickerViewModel实例和ImagePickerView内部创建的ViewModel实例是完全独立的两个对象,所以前者永远拿不到后者的AttachmentList数据。另外注意:TakePicture方法调用的GetFilesForAttachment和实际定义的GetFilesForAttachement存在拼写错误,这会导致照片根本不会被添加到AttachmentList中,先修正这个拼写问题。
实现步骤
步骤1:修改ImagePickerView,支持外部传入ViewModel
首先移除XAML中硬编码的ViewModel实例,改为通过绑定接收外部传入的实例:
<!-- 删除以下硬编码内容 --> <!-- <ContentView.BindingContext> <vm:ImagePickerViewModel ContextView="{x:Reference View_ImagePickerView}"/> </ContentView.BindingContext> -->
然后在ImagePickerView后台代码中添加ViewModel绑定属性:
public partial class ImagePickerView : ContentView { public static readonly BindableProperty ViewModelProperty = BindableProperty.Create( nameof(ViewModel), typeof(ImagePickerViewModel), typeof(ImagePickerView)); public ImagePickerViewModel ViewModel { get => (ImagePickerViewModel)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); } public ImagePickerView() { InitializeComponent(); BindingContext = this; } }
同时修改ImagePickerView内部的绑定路径,比如按钮命令和列表数据源:
<!-- 按钮Command绑定 --> Command="{Binding ViewModel.TakePictureCmd}" <!-- FlowListView数据源绑定 --> FlowItemsSource="{Binding ViewModel.TakenPicturesList}"
步骤2:在EmailCreateViewModel中共享同一个ImagePickerViewModel实例
修改EmailCreateViewModel,添加共享的ImagePickerViewModel属性:
public class EmailCreateViewModel : BindableObject { public ImagePickerViewModel ImagePickerVm { get; } public EmailCreateViewModel() { ImagePickerVm = new ImagePickerViewModel(); } // 原有代码... }
然后在EmailCreatePage.xaml中,将ImagePickerView绑定到这个共享实例:
<local:ImagePickerView ViewModel="{Binding ImagePickerVm}" />
步骤3:在EmailCreatePage中获取正确的AttachmentList
修改GetFileList方法,从共享的ViewModel实例中获取数据:
public List<BaseObject> GetFileList() { return ViewModel.ImagePickerVm.AttachmentList; }
额外优化建议
- 将
AttachmentList改为ObservableCollection<BaseObject>,这样当集合内容变化时,邮件页面能自动感知到更新 - 确保
OnPropertyChanged方法正确实现,当前代码中Path属性调用的OnPropertyChanged未定义,可直接使用RaisedOnPropertyChanged方法替代
内容的提问来源于stack exchange,提问作者Lisa Maria

