Xamarin Android MVVM - Grouped CollectionView does not update UI after list changes - c#

I have a grouped CollectionView with an ObservableCollection as ItemSource andthe list shows just fine,  with the itens grouped. The problem is that the UI does not get updated when i add or remove something from the collection. I need to pop and call the ProductsPage again to see the changes.
I even tried to refresh the entire list by calling the CreateGroups method after a change, it didn't work either.
Here´s parte of the code (i removed some unrelated code so there may be some inconsistencies)
ProdutosGroup
public class ProdutosGroup : ObservableCollection<Produto>
{
public string Titulo { get; set; }
public ProdutosGroup(string titulo, ObservableCollection<Produto> produtos) : base(produtos)
{
Titulo = titulo;
}
}
ProductsViewModel
public ObservableCollection<Produto> Produtos { get; set; } //the actual list of products
public ObservableCollection<ProdutosGroup> ProdutosAgrupadosList { get; set; }//the grouped list
public ListaDeProdutosViewModel(int idListaDeProdutos)
{
PopulateList();
CreateGroups();
}
public void CarregarProdutos()
{
this.Produtos = new ObservableCollection<Produto(App._produtoRepo.GetProdutos);
}
public void CreateGroups()
{
var idsCat = Produtos.Select(x => x.IdCategoria).Distinct();
var cats = new ObservableCollection<ProdutoCategoria>();
foreach (var idCat in idsCat)
{
cats.Add(App._categoriaRepo.GetProdutoCategoriaById(idCat));
}
foreach (var item in cats)
{
ObservableCollection<Produto> produtosDaCategoria = new ObservableCollection<Produto>();
foreach (var prod in Produtos)
{
if (prod.IdCategoria == item.Id)
produtosDaCategoria.Add(prod);
}
ProdutosAgrupadosList.Add(new ProdutosGroup(item.Descricao, new ObservableCollection<Produto>(produtosDaCategoria)));
}
}
ProductsPage
<ContentPage.Content>
<Grid>
<ScrollView >
<CollectionView ItemsSource="{Binding ProdutosAgrupadosList}" x:Name="Listas" IsGrouped="true">
<CollectionView.GroupHeaderTemplate>
<DataTemplate x:DataType="models:ProdutosGroup">
<Label Text="{Binding Titulo}" FontSize="28"/>
</DataTemplate>
</CollectionView.GroupHeaderTemplate>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Produto">
<Label VerticalTextAlignment="Center" Text="{Binding Nome}" FontSize="28"/>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ScrollView>
<ImageButton Padding="12" Source="BasketPlus" Grid.Row="1" Command="{Binding AddForm}" HorizontalOptions="End" WidthRequest="68" HeightRequest="68" VerticalOptions="End" CornerRadius="100" Margin="0,0,16,22" BackgroundColor="{StaticResource Verde}"/>
</Grid>
</ContentPage.Content>

If you want to add an item in a group, you could simply use the following code:
ProdutosAgrupadosList[0].Add(
new Produto
{
Nome = "and"
}); // this will add the item at the end
or
ProdutosAgrupadosList[0].Insert(1,
new Produto
{
Nome = "and"
}); // add the item after the index1 item
To remove, you could either use ProdutosAgrupadosList[0].RemoveAt(index) or use a SwipeView which you could refer to CollectionView Context menus for more info. A simple demo using SwipeView like the following
<CollectionView.ItemTemplate>
<DataTemplate >
<SwipeView>
<SwipeView.LeftItems>
<SwipeItems>
<SwipeItem Text="Delete"
BackgroundColor="LightPink"
Command="{Binding Source={x:Reference Listas}, Path=BindingContext.DeleteCommand}"
CommandParameter="{Binding}" />
</SwipeItems>
</SwipeView.LeftItems>
<StackLayout>
<Label VerticalTextAlignment="Center" Text="{Binding Nome}" FontSize="28"/>
</StackLayout>
</SwipeView>
</DataTemplate>
</CollectionView.ItemTemplate>
That works for me based on the code in your question. For more info, you could refer to Xamarin.Forms CollectionView
Hope it works for you.

Related

Maui Listview grouping

I have a list which consists of around 100 object. Each object has 2 properties (name, punch_time). The class is as follows:
public class Trxs
{
public string punch_time { get; set; }
public string name { get; set;}
}
the list name is (Punch_Times):
List<Trxs> Punch_times = new List<Trxs>();
It is a finger-print machine transactions. I want to populate a list view in Maui, so that the data will be grouped based on the name, as below:
It should show like this
I Tried the following grouping of the list, It showed the list grouped but without the group name. The listview name is (Trx_List):
var sorted = Punch_times.GroupBy(x => x.name)
.Select(grp => grp.ToList())
.ToList();
Trx_List.ItemsSource = sorted ;
the result showed like this (the group names are empty):
But it is showing like this
I have created a class to represent to new list, which will be the item source of the Trx_List as follows:
public class Grouped_list
{
public string emp_name { get; set; }
public List<Trxs> trxes { get; set; }
}
And created a new list:
List<Grouped_list> new_list = new List<Grouped_list>();
but how to copy the items from (sorted) to (new_list). Or is it needed? how to make item source of the list view grouped by name?
Any help please!
Thank you
To be frank, I recommend to use CollectionView instead of ListView. There is a known issue about iOS GroupHeaderTemplate :ListView GroupHeaderTemplate produces blank headers on iOS and MacCatalyst. It just render a blank headers. That's why i recommend to use CollectionView. The usage is almost the same. You could refer to Display grouped data in a CollectionView.
For your case, how to reflect sorted to new_list really matters. I made a small demo following the official documentation based on your code.
For MainPageViewModel.cs,
public class MainPageViewModel
{
public List<Trxs> Punch_times { get; set; } = new List<Trxs>();
public List<Grouped_list> new_list { get; set; } = new List<Grouped_list>();
public MainPageViewModel()
{
//Add some data for test
Punch_times.Add(new Trxs
{
name = "John",
punch_time = "13:33"
});
......
// translate list to dict (key is name)
var dict = Punch_times.GroupBy(o => o.name)
.ToDictionary(g => g.Key, g => g.ToList());
foreach (KeyValuePair<string, List<Trxs>> item in dict)
{
new_list.Add(new Grouped_list(item.Key,new List<Trxs>(item.Value)));
}
}
}
For Grouped_list.cs,
public class Grouped_list : List<Trxs>
{
public string Name { get; set; }
public Grouped_list(string name, List<Trxs> trxs) : base(trxs)
{
Name = name;
}
}
For MainPage.xaml which consumes the ListView or CollectionView,
<CollectionView ItemsSource="{Binding new_list}"
IsGrouped="True">
<CollectionView.GroupHeaderTemplate>
<DataTemplate>
<StackLayout>
<Label Text="{Binding Name}" TextColor="Black" VerticalOptions="CenterAndExpand"
BackgroundColor="LightGray"
FontSize="20"
FontAttributes="Bold" />
</StackLayout>
</DataTemplate>
</CollectionView.GroupHeaderTemplate>
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout>
<Label Text="{Binding punch_time}" BackgroundColor="Yellow"
FontSize="20" HorizontalTextAlignment="Center"
VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand"/>
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
or you may use ListView but not render correctly on iOS as i mention above.
<ListView ItemsSource="{Binding new_list}"
IsGroupingEnabled="True">
<ListView.GroupHeaderTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding Name}" TextColor="Black" VerticalOptions="CenterAndExpand"
BackgroundColor="LightGray"
FontSize="20"
FontAttributes="Bold" />
</ViewCell>
</DataTemplate>
</ListView.GroupHeaderTemplate>
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout>
<Label Text="{Binding punch_time}" BackgroundColor="Yellow"
FontSize="20" HorizontalTextAlignment="Center"
VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
For more info, you could refer to ListView : Display grouped data and Display grouped data in a CollectionView
Hope it works for you.
public class Grouped_list : List<Trxs>
{
public string emp_name { get; set; }
public Grouped_list(string name, List<Trxs> list) : base(list)
{
emp_name = name;
}
}
You need something like that.
And then your observable is:
public ObservableCollection<Grouped_list> GroupedLists...
Set it as ItemSource, and don't forget to set IsGrouped to true.
In the GroupHeaderTemplate there should be binding to emp_name.
And its DataType should be Grouped_list.
Also, It is EmpName, not emp_name. And GroupedList not Grouped_list.
Follow the naming conventions.

How to track changes of a property of a model

I have this properties on my model.
public class Student : BindableBase
{
public Guid Id { get; set; }
public string Fullname { get; set; }
private bool _isSelected;
public bool IsSelected { get => _isSelected; set => SetProperty(ref _isSelected, value); }
}
And in my ViewModel basically loads all the students and assigned it into the Collection.
public ObservableRangeCollection<Student> Students { get; } = new();
private List<Guid> SelectedIds { get; set; }
public override async Task OnActivatedAsync()
{
var results = await _service.GetAllStudents(take: 100);
Students.ReplaceRange(results);
}
And in my Xaml
<CollectionView ItemsSource="{x:Binding Students}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="vm:Student">
<StackLayout>
<StackLayout Margin="0,20" Orientation="Horizontal">
<CheckBox IsChecked="{x:Binding IsSelected}" />
<StackLayout>
<Label
FontAttributes="Bold"
FontSize="17"
Text="{x:Binding Fullname}"
TextColor="{x:StaticResource ColorBlack}"
VerticalOptions="Center" />
</StackLayout>
</StackLayout>
<BoxView HeightRequest="1" Color="Gray" />
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
Now what I want to achieve is that whenever I select an item I want it to be added to a new List of items object right away. But for now I don't have an idea how to do such thing.
Any help is much appreciated.
Thanks in advance
I can check who among of the students that has IsSelected = true by using Linq but this is during Save button. But what I wanted to do now is during check and uncheck it is being added/remove in a new List object, which is I'm having a hard time how to implement.

Xamarin Forms CollectionView stays empty after binding it

I have a problem. I created this class that creates an ImageSource collection ObservableCollection<TemplateSource>:
public class TemplateListViewModel
{
public ObservableCollection<TemplateSource> sourceList { get; set; }
public TemplateListViewModel()
{
sourceList = new ObservableCollection<TemplateSource>();
loadingTemplates += onLoadingTemplates;
LoadTemplateList();
}
private event EventHandler loadingTemplates = delegate { };
private void LoadTemplateList()
{
loadingTemplates(this, EventArgs.Empty);
}
private async void onLoadingTemplates(object sender, EventArgs args)
{
List<Template> templateList = await App.RestService.GetTemplates(App.User);
foreach (var template in templateList)
{
ImageSource source = ImageSource.FromUri(new Uri("mysite.org/myapp/" + template.FileName));
TemplateSource templateSource = new TemplateSource { Id = template.Id, Source = source };
sourceList.Add(templateSource);
}
}
}
And in my XAML I use this code:
<ContentPage.Content>
<StackLayout HorizontalOptions="Fill">
<Frame IsClippedToBounds="True" HeightRequest="45" CornerRadius="5" Padding="0" Margin="15,15,15,0" BackgroundColor="Transparent">
<Entry Placeholder="Search" ReturnType="Done" PlaceholderColor="Gray" x:Name="txtSearch" Margin="5,0,0,0" TextColor="White" />
</Frame>
<CollectionView ItemsLayout="HorizontalList" ItemsSource="{Binding sourceList}">
<CollectionView.ItemTemplate>
<DataTemplate>
<ff:CachedImage
Source="{Binding .}"
VerticalOptions="Center"
HorizontalOptions="Fill" />
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</StackLayout>
</ContentPage.Content>
And finally in the page.xaml.cs (code behind):
protected override void OnAppearing()
{
TemplateListViewModel vm = new TemplateListViewModel();
BindingContext = vm;
base.OnAppearing();
}
Now I already got help with this code from #Deczaloth, but he couldn't figure out why the CollectionView stays emtpy after I bind it. Now I already checked, but the sourceList does get filled, so thats not the problem.
What am I doing wrong?
I can see one potential problem in your code XD:
When you bind the Source property of CachedImage you set the binding to ".", but you should instead bind to the Source property of the TemplateSource class (in your context "." means a TemplateSource item!), that is you should change your code like so:
<ff:CachedImage
Source="{Binding Source}"
VerticalOptions="Center"
HorizontalOptions="Fill" />

ListView not deleting items in Xamarin.Forms. I have assigned an ObservableCollection to the ListView itemsource. MVVM

In my Xamarin.Forms app I have a simple Contact class [Model]. In the UI [View] there exists a ListView that displays the Contacts. In my model view class I have a list of Contacts (_listOfContacts) that is assigned to the itemSource property of the ListView. This list of Contacts is an ObservableCollection. My issue is the when user clicks Delete from ContextActions I can see that the _listOfContacts is updated but the ListView is not.
The ListView is only updated when I reassign its itemsource to the _listOfContacts. This should not be needed if _listOfContacts is an ObservableCollection of Contacts.
I am new to MVVM so I need to clear these basic MVVM concepts before I go on to learn more advanced techniques.
Here is my code:
Model
class Contact
{
public String Name { get; set; }
public String Status { get; set; }
public String ImageUrl { get; set; }
}
Model View
public partial class ContactListPage : ContentPage
{
private ObservableCollection<Contact> _listOfContacts;
public ContactListPage()
{
InitializeComponent();
_listOfContacts = new ObservableCollection<Contact>
{
new Contact {Name="Item1", ImageUrl="http://lorempixel.com/100/100/people/1" , Status="Hey"},
new Contact { Name = "Item2", ImageUrl = "http://lorempixel.com/100/100/people/2", Status="Hey" },
};
contactList.ItemsSource = _listOfContacts.ToList();
}
private void EditContactClick(object sender, EventArgs e)
{
DisplayAlert("Alert", "Clicked Edit", "Cancel");
}
private void DeleteContactClick(object sender, EventArgs e)
{
var contact = (sender as MenuItem).CommandParameter as Contact;
_listOfContacts.Remove(contact);
//following line of code should not be needed since _listOfContacts is
//an ObservableCollection and removing an item should update the bound control automatically
**contactList.ItemsSource = _listOfContacts.ToList();**
}
}
View
<ContentPage.Content>
<StackLayout>
<ListView x:Name="contactList" HasUnevenRows="True">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Horizontal" Padding="10">
<Image Source="{Binding ImageUrl}"/>
<StackLayout HorizontalOptions="StartAndExpand">
<Label Text="{Binding Name}" Margin="0,2,0,2"/>
<Label Text="{Binding Status}" Margin="0,2,0,2" />
</StackLayout>
</StackLayout>
<ViewCell.ContextActions>
<MenuItem Text="Edit" Clicked="EditContactClick" CommandParameter="{Binding .}"/>
<MenuItem Text="Delete" Clicked="DeleteContactClick" IsDestructive="True" CommandParameter="{Binding .}"/>
</ViewCell.ContextActions>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage.Content>
I have tested your code, it is just the method ToList()which caused this question:
contactList.ItemsSource = _listOfContacts.ToList();
At the beginning, the type of _listOfContacts is ObservableCollection, but when you use the method ToList(),then it will been converted to List again.
So just delete the method 'ToList()', your code will work properly, just as follows:
contactList.ItemsSource = _listOfContacts;
Remove .toList() from contactList.ItemsSource = _listOfContacts.ToList(); and try again.
_listOfContacts is an ObservableCollection which should be used as your ItemsSource directly. Maybe go and check out the ObservableCollection documentation.

How to Insert Method Inside MasterDetailPage XamarinForms

I have masterdetailpage that have Logout function inside that (so instead navigating to other page it will shows display alert), but i dont know how to insert the logout method inside the masterdetailpage, i already tried using ICommand but seem it didnt works and make my application force close .
Here is my MasterPageItem Model
public class MasterPageItem
{
public string Title { get; set; }
public string Icon { get; set; }
public Type TargetType { get; set; }
public ICommand Commando { get; set; }
}
here is the listview for MasterDetailPage
<ListView x:Name="navigationDrawerList"
RowHeight="45"
SeparatorVisibility="None"
BackgroundColor="#000000"
ItemSelected="OnMenuItemSelected">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<!-- Main design for our menu items -->
<StackLayout BackgroundColor="#000000" VerticalOptions="FillAndExpand"
Orientation="Horizontal"
Padding="20,10,0,10"
Spacing="20">
<Image Source="{Binding Icon}"
WidthRequest="60"
HeightRequest="60"
VerticalOptions="Center" />
<Label FontFamily="Panton-LightCaps.otf#Panton-LightCaps" Text="{Binding Title}"
FontSize="Medium"
VerticalOptions="Center"
TextColor="White"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
and i tried to insert the method like this
public ICommand GetOff { get; private set; }
public MainPage()
{
GetOff = new Command(LogoutCommand)
var page9 = new MasterPageItem() { Title = "LOGOUT", Commando = GetOff };
}
public async void LogoutCommand ()
{
var result = await this.DisplayAlert("Alert!", "Do you really want to exit?", "Yes", "No");
if (result == true)
{
App.AuthenticationClient.UserTokenCache.Clear(Constants.ApplicationID);
Application.Current.MainPage = new NavigationPage(new NewPageLogin());
}
}
Is there another way to insert method inside MasterdetailPage ? Any suggestion would be appreciated
Option 1
You can add a footer to your listview and attach a tap gesture recognizer to it, like this:
<ListView.Footer>
<StackLayout BackgroundColor="#000000"
VerticalOptions="FillAndExpand"
Orientation="Horizontal"
Padding="20,10,0,10"
Spacing="20">
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Command="{Binding LogoutCommand}" />
</StackLayout.GestureRecognizers>
<Image Source="YourIcon"
WidthRequest="60"
HeightRequest="60"
VerticalOptions="Center" />
<Label FontFamily="Panton-LightCaps.otf#Panton-LightCaps"
Text="{Binding Title}"
FontSize="Medium"
VerticalOptions="Center"
TextColor="White"/>
</StackLayout>
</ListView.Footer>
It should go inside the ListView tag.
As you see, it supports Command, so you can use the one you already have.
Option 2
You can set the TargetType of your sign out item to null and do something like this:
private void OnMenuItemSelected(object sender, SelectedItemChangedEventArgs e)
{
var item = e.SelectedItem as MasterPageItem;
if (item == null)
return;
// Check if sign out was tapped
if (item.TargetType != null)
{
var page = (Page)Activator.CreateInstance(item.TargetType);
page.Title = item.Title;
Detail = new NavigationPage(page);
IsPresented = false;
}
else
{
// Manage your sign out action
var result = await this.DisplayAlert("Alert!", "Do you really want to exit?", "Yes", "No");
if (result == true)
{
App.AuthenticationClient.UserTokenCache.Clear(Constants.ApplicationID);
Application.Current.MainPage = new NavigationPage(new NewPageLogin());
}
}
}

Categories