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.
I'm trying to make CollectionView in Shell but it's not updating.
I have one view model connected to Page and AppShell but when I update Collection view only page is updationg.
`public class AppShellViewModel : INotifyPropertyChanged
{
public Command Load { get; }
public ObservableCollection<ListData> _lists { get; set; }
public ObservableCollection<ListData> Lists
{
get { return _lists; }
set
{
_lists = value;
OnPropertyChanged();
}
}
public AppShellViewModel()
{
Lists = new ObservableCollection<ListData>()
{
new ListData(){id=0,name="test",UserId=0},
new ListData(){id=1,name="test1",UserId=1},
new ListData(){id=2,name="test2",UserId=2},
new ListData(){id=3,name="test3",UserId=3},
new ListData(){id=4,name="test4",UserId=4}
};
Load = new Command(async () => await GetUserLists());
}
async Task GetUserLists()
{
for (int i = 5; i < 15; i++)
{
Lists.Add(new ListData {id=i, name=$"test{ i }", UserId=i });
}
}
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}`
Then i have App Shell Collection View
`<Shell.FlyoutContent>
<StackLayout BackgroundColor="#34495e">
<Label Text="YOUR LISTS" FontSize="50" />
<CollectionView ItemsSource="{Binding Lists}" >
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout Padding="10" x:DataType="model:ListData">
<Label Text="{Binding name}"
LineBreakMode="NoWrap"
FontSize="13" />
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</StackLayout>
</Shell.FlyoutContent>`
And There is Page CollectionView
`<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="ToDoApp.Views.AboutPage"
xmlns:model="clr-namespace:ToDoApp.Models">
<StackLayout>
<Button Text="Load" Command="{Binding Load}"/>
<Label Text="{Binding error}"/>
<CollectionView ItemsSource="{Binding Lists}">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout Padding="10" x:DataType="model:ListData">
<Label Text="{Binding name}"
LineBreakMode="NoWrap"
FontSize="13" />
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</StackLayout>
</ContentPage>`
before update it looks like this
Page before update
Shell before update
And after update the only what changed is content page and shell is the same as before
Page after update
Shell after update
Related to Jason's comment.
WON'T CHANGE TOGETHER
NOT the same instance - BindingContexts similar to these:
// In AppShell.xaml.cs.
public AppShell()
{
InitializeComponent();
BindingContext = new AppShellViewModel();
}
// In AboutPage.xaml.cs.
public AboutPage()
{
InitializeComponent();
BindingContext = new AppShellViewModel();
}
GOOD (SHARED BETWEEN TWO PLACES)
BindingContexts are SAME instance:
// In AppShellViewModel.cs.
public class AppShellViewModel ...
{
private static AppShellViewModel _it;
public static AppShellViewModel It
{
get {
if (_it == null)
_it = new AppShellViewModel();
return _it;
}
}
}
// In AppShell.xaml.cs.
public AppShell()
{
InitializeComponent();
BindingContext = AppShellViewModel.It;
}
// In AboutPage.xaml.cs.
public AboutPage()
{
InitializeComponent();
BindingContext = AppShellViewModel.It;
}
I am filling my settings in list view, however I wanted to leave it generic and just set class like this however when I switch the value it doesn't fire the command
for my switch I have used this to add the command.
It works outside of a list https://damienaicheh.github.io/xamarin/xamarin.forms/2019/05/17/how-to-add-command-to-a-switch-in-xamarin-forms-en.html
public class Settings
{
public string Type { get; set; }
public bool Value { get; set; }
public Command command { get; set; }
}
then I am loading specific settings like this
public CustomSettingsViewModel()
{
LoadSetting();
}
private void LoadSetting()
{
foreach (var setting in _customSettings.ScanSettings)
{
var detail = new Settings()
{
Type = setting.Type,
Value = setting.Value,
command = new Command<Settings>((value) => ExecutePageCommand(value))
};
Setting.Add(detail);
}
}
command
private void ExecutePageCommand(Settings settings)
{
foreach (var result in _customSettings.ScanSettings.Where(d => d.Type == settings.Type))
{
var detail = new ScanSetting()
{
Value = settings.Value,
};
}
}
<Grid BackgroundColor="{DynamicResource PageBackgroundColor}" x:Name="grid" HorizontalOptions="FillAndExpand" ColumnSpacing="0">
<!--Data-->
<Label Grid.Row="0" Text ="{Binding Type}" Style="{StaticResource SubLabelStyle}" HorizontalOptions="Start" Padding="10" VerticalOptions="End" />
<Switch Grid.Row="0" IsToggled="{Binding Value}" HorizontalOptions="End" VerticalOptions="CenterAndExpand" >
<Switch.Behaviors>
<control:SwitchBehaviour Command="{Binding command}" />
</Switch.Behaviors>
</Switch>
</Grid>
However I am unsure how to change the value of the switch on the exact property so it would change only one value. Can you please advise?
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" />
I'm very new to Xamarin.Forms and MVVM and posting questions here on StackOverflow so bear with me please. I'm trying to fill a listview in Xamarin.Forms. I first programmed it without MVVM and it all worked like I wanted it to, but now I wanted to get in in MVVM and that is where it went wrong, now my list won't fill up anymore.
I made a viewmodel and only put all the binding in the viewmodel, I have not yet implemented eventhandlers.
This is a part of the code behind (I have a couple more eventhandlers but that is not relevant right now):
namespace CCXamarinApp
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class PatientsWithTagPage : ContentPage
{
public PatientsWithTagPage()
{
BindingContext = new PatientsWithTagViewModel();
InitializeComponent();
(BindingContext as PatientsWithTagViewModel).GetAllPatients();
if((BindingContext as PatientsWithTagViewModel).IsEmptyPatientList)
HandleEmptyList();
else
(BindingContext as PatientsWithTagViewModel).SortAndShowPatients();
}
private void SearchBar_OnTextChanged(object sender, TextChangedEventArgs e)
{
(BindingContext as PatientsWithTagViewModel).Searching(e.NewTextValue);
}
...
This is my XAML page:
<SearchBar x:Name="SearchBar" Placeholder="Zoek op naam of plaats..." HeightRequest="25" Margin="10"
TextChanged="SearchBar_OnTextChanged"/>
<Label Text="{Binding LastRefreshed}" FontAttributes="Italic" FontSize="15" />
<Label x:Name="LabelEmptyList" FontSize="17" Text="Geen gegevens gevonden" FontAttributes="Bold"
IsVisible="False" />
<ListView x:Name="PatientListView" HasUnevenRows="True" SeparatorColor="Accent"
VerticalOptions="FillAndExpand" IsPullToRefreshEnabled="True" IsRefreshing="{Binding IsFetchingData, Mode=TwoWay}"
Refreshing="PatientListView_OnRefreshing" SelectedItem="{Binding SelectedPatient, Mode=TwoWay}"
ItemSelected="PatientListView_OnItemSelected" ItemsSource="{Binding Patients}"
IsVisible="True" BackgroundColor="Aqua">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Spacing="4">
<StackLayout Orientation="Horizontal" Margin="10,7,10,1">
<Label Text="{Binding FullName}" FontAttributes="Bold" FontSize="16" />
<Label Text="{Binding DisplayTimeOfLastScan, StringFormat='{0}'}"
HorizontalOptions="EndAndExpand" />
</StackLayout>
<StackLayout Orientation="Horizontal" Margin="10,0,10,7">
<Label Text="{Binding LastLocation}" HorizontalOptions="Start" />
<Label Text="{Binding DisplayDurationSinceLastScan, StringFormat='al {0}'}"
HorizontalOptions="EndAndExpand" />
</StackLayout>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage.Content>
This is my viewmodel (not all the code but the code that is most relevant). The BaseViewModel it derives from is from the nuget package "Refractored.Mvvmhelpers":
class PatientsWithTagViewModel : BaseViewModel
{
public ObservableCollection<PatientViewModel> Patients { get; private set; } = new ObservableCollection<PatientViewModel>();
private PatientViewModel selectedPatient;
public PatientViewModel SelectedPatient
{
get => selectedPatient;
set => SetProperty(ref selectedPatient, value);
}
private readonly JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings
{
DateFormatString = "dd-MM-yyyTH:mm",
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
};
public bool IsEmptyPatientList => Patients.Count == 0;
private string testJson = "[{\"firstName\":\"P.\",\"lastName\":\"Selie\",\"tag\":{\"tagId\":\"124\",\"tagSerialNumber\":\"ABC135\"},\"scans\":[{\"location\":\"Tuin\",\"dateTime\":\"May01,2018,10:10\"},{\"location\":\"Eetzaal\",\"dateTime\":\"May02,2018,10:15\"},{\"location\":\"Gang\",\"dateTime\":\"May02,2018,11:10\"},{\"location\":\"Kamer23\",\"dateTime\":\"May02,2018,12:09\"}],\"id\":\"dcc4fe9929b3681f\"}," +
"{\"firstName\":\"W.\",\"lastName\":\"Janssens\",\"tag\":{\"tagId\":\"132\",\"tagSerialNumber\":\"ABC167\"},\"scans\":[{\"location\":\"Kamer23\",\"dateTime\":\"May01,2018,23:39\"},{\"location\":\"Gang\",\"dateTime\":\"May02,2018,04:10\"},{\"location\":\"Eetzaal\",\"dateTime\":\"May02,2018,04:11\"},{\"location\":\"Gang\",\"dateTime\":\"May02,2018,04:20\"},{\"location\":\"Kamer23\",\"dateTime\":\"May02,2018,04:22\"}],\"id\":\"a6dac28475327922\"}]";
public void GetAllPatients()
{
IsFetchingData = true;
try
{
Patients = new ObservableCollection<PatientViewModel>(
JsonConvert.DeserializeObject<ObservableCollection<PatientViewModel>>(testJson,
jsonSerializerSettings));
}
catch(Exception e)
{
Console.WriteLine("*****ERROR kon API niet ophalen");
Console.WriteLine(e.Message);
}
finally
{
IsFetchingData = false;
}
}
This is the model I use:
public class Patient : ObservableObject
{
public string FirstName { get; set; }
public string LastName { get; set; }
public Tag Tag { get; set; }
public List<Scan> Scans { get; set; }
public string Id { get; set; }
public override string ToString()
{
return LastName + ", " + FirstName;
}
}
It also has its own viewmodel with properties like DisplayDurationSinceLastScan, but I don't think it is relevant here, if you think it is, let me know.
So with this code I get my page but there seems to be no items in the list, if I debug, Patients is filled with items so it is not empty at all, but something goes wrong with the binding I guess but no error is given.
Here is a picture of what I get: the listview is shown (I added a blue background so I would know if the listview was visible or not), but there are no items in there. Still Patients is filled when I debug the app.
Does someone see the mistake I made?
I see some issues with your code here from a maintainability point of view. Try to keep all your code inside the view model instead of both the view model and code behind. Ideally, your code behind contains nothing and if anything it strictly has to do with visual things.
Anyway, in regard to you problem: you are creating a new ObservableCollection each time. That breaks the binding. Just keep the new ObservableCollection at the top and then when new data comes in, clear that and repopulate it. Like this:
public void GetAllPatients()
{
IsFetchingData = true;
try
{
var resultPatients = JsonConvert.DeserializeObject<ObservableCollection<PatientViewModel>>(testJson, jsonSerializerSettings);
Patients.Clear();
foreach (var patient in resultPatients)
Patients.Add(patient);
}
catch(Exception e)
{
Console.WriteLine("*****ERROR kon API niet ophalen");
Console.WriteLine(e.Message);
}
finally
{
IsFetchingData = false;
}
}
Your data should now show up.