为什么 ObservableCollection 在 Android 而不是 iOS 上更新 UI?[Pendant]

Why is ObservableCollection updating UI on Android but not iOS?[Pendent]

我在 android 和 iOS 的 Xamarin 表单上有一个跨平台应用程序。

在某些方面,我使用 ObservableCollection 作为 ListView.

的来源

在列表视图中有用户可以交互和更改的按钮 UI。

在 android 上一切正常,但是当我在 iOS 上测试它时,只有单击 1 次后才会执行更改。

我使用 MVVM 架构来满足我的需求,它在 android 上运行良好。唯一的问题是 iPhone。

c# 代码:

[XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class ConvidarAmigosEmail : ContentView
    {
        public ConvidarAmigosEmail ()
        {
                        BindingContext = new ConvidarAmigosEmailViewModel();
            InitializeComponent ();
        }

        private void ListaEmail_ItemSelected(object sender, SelectedItemChangedEventArgs e)
        {
            if (e.SelectedItem == null) return;
            listaEmail.SelectedItem = null;
        }

        private void TapGestureRecognizer_Tapped(object sender, EventArgs e)
        {

        }
    }

    public class ConvidarAmigosEmailViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private ObservableCollection<Contacto> _contactosComEmail;
        public ObservableCollection<Contacto> ContactosComEmail { get => _contactosComEmail; set { _contactosComEmail = value; OnPropertyChanged(nameof(ContactosComEmail)); } }

        public ICommand SeleccionarContacto
        {
            get => new Command<Contacto>((contacto) =>
            {
                if (!contacto.Seleccionado)
                {
                    contacto.Seleccionado = !contacto.Seleccionado;
                    contacto.Opcao = AppResource.Seleccionado;
                    ContactosSeleccionadosEmail.Add(contacto);
                }
                else
                {
                    contacto.Seleccionado = !contacto.Seleccionado;
                    contacto.Opcao = AppResource.Select;
                    ContactosSeleccionadosEmail.Remove(contacto);
                }
            });
        }

        public ICommand SeleccionarTodos
        {
            get => new Command(() =>
            {
                if (ContactosComEmail.Count != ContactosSeleccionadosEmail.Count)
                {
                    ContactosSeleccionadosEmail.Clear();
                    foreach (var obj in ContactosComEmail)
                    {
                        obj.Seleccionado = true;
                        obj.Opcao = AppResource.Seleccionado;
                        ContactosSeleccionadosEmail.Add(obj);
                    }
                }
                else
                {
                    ContactosSeleccionadosEmail.Clear();
                    foreach (var obj in ContactosComEmail)
                    {
                        obj.Seleccionado = false;
                        obj.Opcao = AppResource.Select;
                    }
                }
            });
        }

        public List<Contacto> ContactosSeleccionadosEmail { get; set; } = new List<Contacto>();

        private string _textoSelectTodos = AppResource.SelTodos;
        public string TextoSelectTodos
        {
            get
            {

                if (ContactosSeleccionadosEmail.Count == ContactosComEmail.Count)
                {
                    _textoSelectTodos = AppResource.Anull;
                    OnPropertyChanged(nameof(TextoSelectTodos));
                    return _textoSelectTodos;
                }
                _textoSelectTodos = AppResource.SelTodos;
                OnPropertyChanged(nameof(TextoSelectTodos));
                return _textoSelectTodos;
            }
        }

        private void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged == null)
                return;

            PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        public ConvidarAmigosEmailViewModel()
        {
            ContactosComEmail = new ObservableCollection<Contacto>();
            ObterContactos();
        }

        #region ObterContactos
        /*
         Params Entrada => nenhum
         Params Saída   => nenhum
         Lógica         => Passo 1 : Acede a lista de contactos do telefone e extrai os dados;
                           Passo 2 : Coloca os contactos com email na variável ContactosE; os que não têm email em ContactosT
                           Passo 3 : ContentLoader para aceder ao caminho real do ficheiro de imagem (foto) do contacto;
                           Passo 4 : Adiciona os contactos com Email a uma lista da classe "Contacto", Contactos#. Os sem email para ContactosT
         Plugins : Obter permissoes => https://www.nuget.org/packages/plugin.permissions/;
                   Aceder aos contactos =>
        */
        public async void ObterContactos()
        {
            //verifica se aplicacao tem permissao pra aceder a lista de contactos
            var statusPermissao = await CrossPermissions.Current.CheckPermissionStatusAsync(Permission.Contacts);

            //requisitar permissao 
            if (statusPermissao == PermissionStatus.Unknown)
            {
                var resposta = await CrossPermissions.Current.RequestPermissionsAsync(Permission.Contacts);
                statusPermissao = resposta[Permission.Contacts];
            }

            if (statusPermissao == PermissionStatus.Denied)
            {
                var op = await App.Current?.MainPage?.DisplayAlert(AppResource.contactosNegados, AppResource.SemFuncionalidade, AppResource.Definicoes, AppResource.MaisTarde);

                if (op == true)
                {
                    CrossPermissions.Current.OpenAppSettings();
                }

                else
                {
                    await App.Current?.MainPage?.Navigation?.PopAsync();
                }
            }

            if (statusPermissao == PermissionStatus.Granted)
            {
                var contactos = await Plugin.ContactService.CrossContactService.Current.GetContactListAsync();
                //contactos com email
                var contactosE = contactos.Where(x => x.Email != null).OrderBy(x => x.Name).ToList();

                //imagem inicial de cada contacto, caso esta não tenha imagem de perfil.
                var aux = ImageSource.FromResource("KiaiDay.Images.user.png");
                foreach (var c in contactosE)
                {
                    aux = ImageSource.FromResource("KiaiDay.Images.user.png");
                    if (c.PhotoUri != null)
                    {
                        //caso tenho imagem de perfil, aceder ao caminho real no ficheiro e extrair a imagem
                        var contentLoader = DependencyService.Get<IContentLoader>();
                        var uri = new Uri(c.PhotoUri);
                        aux = contentLoader.LoadFromContentUri(uri);
                    }

                    ContactosComEmail.Add(
                        new Contacto()
                        {
                            Email = c.Email,
                            Foto = aux,
                            Numero = c.Number,
                            Nome = c.Name,
                            Opcao = AppResource.Select
                        });
                }
            }
            else
            {
                //await App.Current?.MainPage?.DisplayAlert(AppResource.contactosNegados, AppResource.SemFuncionalidade, AppResource.ok);
                //App.Current?.MainPage?.Navigation?.PopAsync();
            }
        }
        #endregion
    }

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"
             x:Class="KiaiDay.Views.PosLogin.ConvidarAmigosEmail" x:Name="pagina"
             xmlns:FFIL="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms"
             xmlns:FFT="clr-namespace:FFImageLoading.Transformations;assembly=FFImageLoading.Transformations">
  <ContentView.Content>
        <Grid BackgroundColor="White" RowSpacing="0">
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="7*"/>
            </Grid.RowDefinitions>

            <Grid ColumnSpacing="0" Grid.Row="0" HorizontalOptions="FillAndExpand"  VerticalOptions="FillAndExpand">
                <Grid.RowDefinitions>
                    <RowDefinition Height="*"/>
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <Frame Grid.Row="0" Grid.Column="2" Padding="8" BackgroundColor="#f2f2f2" Margin="-25,0,0,0"
                       HorizontalOptions="CenterAndExpand" VerticalOptions="Center" BorderColor="LightGray" HasShadow="False">
                    <Frame.CornerRadius>
                        <OnPlatform Android="80" iOS="10"/>
                    </Frame.CornerRadius>
                    <Label Grid.ColumnSpan="2" Text="{Binding TextoSelectTodos}" FontFamily="{StaticResource RegularFont}" TextColor="{Binding CorSelectTodos}" FontSize="10" VerticalOptions="FillAndExpand" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
                    <Frame.GestureRecognizers>
                        <TapGestureRecognizer NumberOfTapsRequired="1" Command="{Binding SeleccionarTodos}"/>
                    </Frame.GestureRecognizers>
                </Frame>
            </Grid>


            <ListView SeparatorColor="LightGray" ItemsSource="{Binding ContactosComEmail}" HasUnevenRows="True" x:Name="listaEmail"
                      Margin="30,0,30,0" ItemSelected="ListaEmail_ItemSelected" IsGroupingEnabled="False" Grid.Row="1" CachingStrategy="RetainElement">
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <ViewCell>
                            <Grid ColumnSpacing="10">
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="3"/>
                                    <RowDefinition Height="*"/>
                                    <RowDefinition Height="*"/>
                                    <RowDefinition Height="3"/>
                                </Grid.RowDefinitions>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="2*"/>
                                    <ColumnDefinition Width="5*"/>
                                    <ColumnDefinition Width="3*"/>
                                </Grid.ColumnDefinitions>
                                <FFIL:CachedImage Grid.Row="1" Grid.Column="0" Grid.RowSpan="2"
                                    Source="{Binding Foto}" DownsampleToViewSize="True" BitmapOptimizations="False"
                                    Aspect="AspectFit" RetryCount="0" RetryDelay="250" HeightRequest="50" 
                                    LoadingPriority="Highest" HorizontalOptions="Center" VerticalOptions="Center" 
                                    FadeAnimationForCachedImages="False" WidthRequest="50">
                                    <FFIL:CachedImage.Transformations>
                                        <FFT:CircleTransformation/>
                                    </FFIL:CachedImage.Transformations>
                                </FFIL:CachedImage>
                                <Label Grid.Row="1" Grid.Column="1" Text="{Binding Nome}" FontFamily="{StaticResource RegularFont}" TextColor="#3c3c3b" LineBreakMode="TailTruncation"/>
                                <Label Grid.Row="2" Grid.Column="1" Text="{Binding Email}" FontFamily="{StaticResource RegularFont}" TextColor="#3c3c3b" LineBreakMode="TailTruncation"/>
                                <Frame Grid.Row="1" Grid.Column="2" Padding="8" BackgroundColor="#f2f2f2" Grid.RowSpan="2" 
                                       HorizontalOptions="CenterAndExpand" VerticalOptions="Center" BorderColor="LightGray" HasShadow="False">
                                    <Frame.CornerRadius>
                                        <OnPlatform Android="80" iOS="10"/>
                                    </Frame.CornerRadius>
                                    <Grid HorizontalOptions="Center">
                                        <Grid.ColumnDefinitions>
                                            <ColumnDefinition Width="*"/>
                                            <ColumnDefinition Width="6*"/>
                                        </Grid.ColumnDefinitions>
                                        <FFIL:CachedImage Source="{Binding Icone}" HeightRequest="8" WidthRequest="8" BackgroundColor="Transparent" Grid.Column="0" 
                                            VerticalOptions="Center" LoadingPriority="Highest" RetryCount="0" RetryDelay="250" DownsampleToViewSize="True" BitmapOptimizations="False"/>
                                        <Label Text="{Binding Opcao}" FontFamily="{StaticResource RegularFont}" FontSize="8" TextColor="{Binding Cor}" Grid.Column="1" VerticalOptions="Center"/>
                                    </Grid>


                                    <Frame.GestureRecognizers>
                                        <TapGestureRecognizer NumberOfTapsRequired="1"  Command="{Binding Source={x:Reference pagina},Path=BindingContext.SeleccionarContacto}" CommandParameter="{Binding .}" Tapped="TapGestureRecognizer_Tapped"/>
                                    </Frame.GestureRecognizers>
                                </Frame>
                            </Grid>
                        </ViewCell>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
        </Grid>
    </ContentView.Content>
</ContentView>

我希望 ObservableCollection 无需单击即可在 iOS 上更新 UI。

编辑: 使用的插件 => https://github.com/jamesmontemagno/PermissionsPlugin; https://github.com/jamesmontemagno/ContactsPlugin;

模型定义

 public class Contacto : INotifyPropertyChanged, IEntity
    {

        public event PropertyChangedEventHandler PropertyChanged;

        [PrimaryKey, AutoIncrement]
        public int Id { get; set; }

        public string Nome { get; set; }

        public string Email { get; set; }

        public string Numero { get; set; }


        [Ignore]
        public ImageSource Foto { get; set; }

        private ImageSource _icone = ImageSource.FromFile("mais.png");
        [Ignore] public ImageSource Icone
        {
            get
            {
                if (Seleccionado)
                {
                    _icone = ImageSource.FromFile("certo.png");
                    OnPropertyChanged(nameof(Icone));
                    return _icone;
                }
                _icone = ImageSource.FromFile("mais.png");
                OnPropertyChanged(nameof(Icone));
                return _icone;
            }
            set
            {
                _icone = value;
                OnPropertyChanged(nameof(Icone));
            }
        }

        private string _opcao;
        [Ignore]
        public string Opcao
        {
            get
            {
                return _opcao;
            }
            set
            {
                _opcao = value;
                OnPropertyChanged(nameof(Opcao));
            }
        }


        private bool _seleccionado = false;
        public bool Seleccionado
        {
            get
            {
                return _seleccionado;
            }
            set
            {
                _seleccionado = value;
                OnPropertyChanged(nameof(Seleccionado));
            }
        }


        private Color _cor = Color.FromHex("#3c3c3b");
        [Ignore]
        public Color Cor
        {
            get
            {
                if (Seleccionado)
                {
                    _cor = Color.FromHex("#4297d3");
                    OnPropertyChanged(nameof(Cor));
                    return _cor;
                }
                _cor = Color.FromHex("#3c3c3b");
                OnPropertyChanged(nameof(Cor));
                return _cor;
            }
            set
            {
                _cor = value;
                OnPropertyChanged(nameof(Cor));
            }
        }

        #region INotifyPropertyChanged Implementation
        void OnPropertyChanged([CallerMemberName] string propertyName = "")
        {
            if (PropertyChanged == null)
                return;

            PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion

    }
}

EDIT2:更改视图模型的构造函数后,视图正在更新,但我无能为力。好像被屏蔽了。

        public ConvidarAmigosEmailViewModel()
        {
            ContactosComEmail = new ObservableCollection<Contacto>();
            Task.Run(async () => { await ObterContactos(); });
        }

EDIT3:在花了很多时间试图弄清楚发生了什么之后,我得出结论,问题出在这部分代码上:

                                <Frame Grid.Row="1" Grid.Column="2" Padding="8" BackgroundColor="#f2f2f2" Grid.RowSpan="2" HorizontalOptions="CenterAndExpand" VerticalOptions="Center" HasShadow="False">
                                    <Frame.CornerRadius>
                                        <OnPlatform Android="80" iOS="10"/>
                                    </Frame.CornerRadius>
                                    <Grid HorizontalOptions="Center">
                                        <Grid.ColumnDefinitions>
                                            <ColumnDefinition Width="*"/>
                                            <ColumnDefinition Width="6*"/>
                                        </Grid.ColumnDefinitions>
                                        <FFIL:CachedImage Source="{Binding Icone}" HeightRequest="8" WidthRequest="8" BackgroundColor="Transparent" Grid.Column="0" 
                                            DownsampleToViewSize="True" BitmapOptimizations="False" Aspect="AspectFit" RetryCount="0" RetryDelay="250"
                                            LoadingPriority="Highest" HorizontalOptions="Center" VerticalOptions="Center" FadeAnimationForCachedImages="False"/>
                                        <Label Text="{Binding Opcao}" FontFamily="{StaticResource RegularFont}" FontSize="8" TextColor="{Binding Cor}" Grid.Column="1" VerticalOptions="Center"/>
                                    </Grid>


                                    <Frame.GestureRecognizers>
                                        <TapGestureRecognizer NumberOfTapsRequired="1"  Command="{Binding Source={x:Reference pagina},Path=BindingContext.SeleccionarContacto}" CommandParameter="{Binding .}"/>
                                    </Frame.GestureRecognizers>
                                </Frame>

我不知道为什么会这样...

我想这就足够了,没有调试器和源代码很难。

ConvidarAmigosEmail.xaml

  [XamlCompilation(XamlCompilationOptions.Compile)]
        public partial class ConvidarAmigosEmail : ContentView
        {
            public ConvidarAmigosEmailViewModel ViewModel { get; set; }
            public ConvidarAmigosEmail ()
            {
                ViewModel = new ConvidarAmigosEmailViewModel();
                InitializeComponent ();
                BindingContext = new ConvidarAmigosEmailViewModel();
            }

            public async override OnAppearing()
            {
                // will be good that you show loading icon while processing data
                await ViewModel.LoadData();
            }
            private void ListaEmail_ItemSelected(object sender, SelectedItemChangedEventArgs e)
            {
                if (e.SelectedItem == null) return;
                listaEmail.SelectedItem = null;
            }

            private void TapGestureRecognizer_Tapped(object sender, EventArgs e)
            {

            }
        }

视图模型创建

    public ConvidarAmigosEmailViewModel()
        {
            ContactosComEmail = new ObservableCollection<Contacto>();

        }

        public async Task LoadData()
        {
            await ObterContactos();
        }

Obtener Contactos 内部

      //imagem inicial de cada contacto, caso esta não tenha imagem de perfil.
            var aux = ImageSource.FromResource("KiaiDay.Images.user.png");
            var ListContactos = new List<Contacto>();
            foreach (var c in contactosE)
            {
                aux = ImageSource.FromResource("KiaiDay.Images.user.png");
                if (c.PhotoUri != null)
                {
                    //caso tenho imagem de perfil, aceder ao caminho real no ficheiro e extrair a imagem
                    var contentLoader = DependencyService.Get<IContentLoader>();
                    var uri = new Uri(c.PhotoUri);
                    aux = contentLoader.LoadFromContentUri(uri);
                }

                // Add method inside foreach generate too many innecesary property changed
                // notifications
                ListContactos.Add(
                    new Contacto()
                    {
                        Email = c.Email,
                        Foto = aux,
                        Numero = c.Number,
                        Nome = c.Name,
                        Opcao = AppResource.Select
                    });
            }

            ObterContactos = new ObservableCollection<Contacto>(ListContactos);
        }
        else
...