Reputation: 25
I have a cross-platform app on Xamarin forms for android and iOS.
In some view, i am using an ObservableCollection as Source of a ListView
.
In the listview there are buttons that users can interact and change the UI.
On android everything is working fine but when i tested it on iOS, the changes are performed only after 1 click.
I used MVVM architecture to accomplish my needs and it works very well on android.. the only problem is iPhone.
c# code :
[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 code :
<?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>
I expect the ObservableCollection to update the UI on iOS without needing to click.
EDIT : Used plugins => https://github.com/jamesmontemagno/PermissionsPlugin; https://github.com/jamesmontemagno/ContactsPlugin;
model definition
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: After changing constructor of the view model, the view was getting updated but i couldnt do anything else. It looked like it was blocked.
public ConvidarAmigosEmailViewModel()
{
ContactosComEmail = new ObservableCollection<Contacto>();
Task.Run(async () => { await ObterContactos(); });
}
EDIT3: After spending a lot of time trying to figure out what was happening, i came to conclusion that the problem was with this part of code:
<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>
I dont know why this is happening...
Upvotes: 0
Views: 297
Reputation: 496
I think this would be enough, its dificult without debugger and source code.
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)
{
}
}
Viewmodel creation
public ConvidarAmigosEmailViewModel()
{
ContactosComEmail = new ObservableCollection<Contacto>();
}
public async Task LoadData()
{
await ObterContactos();
}
Inside ObterContactos
//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
...
Upvotes: 0