Changing Property of ObservableCollection does nothing - mvvm

In my Xamarin project I have a ListView, which gets populated by an ObservableCollection, which holds "Item" objects with some properties. If I add items to the collection the UI gets updated, but if I change only a property, it won't it does nothing. Even after an UI update through adding an item does nothing, although the property gets correctly changed. How can I make the UI refresh if a property gets changed?
BindableBase is a class from PRISM that implements INotifyPropertyChanged and DelegateCommand implements ICommand, btw.
Here's my XAML:
<ListView ItemsSource="{Binding ListItems}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell x:Name="viewCell">
<ContentView Padding="0,0,0,5"
HeightRequest="50">
<ContentView.GestureRecognizers>
<TapGestureRecognizer BindingContext="{Binding Source={x:Reference listView}, Path=BindingContext}"
Command="{Binding ItemTappedCommand}"
CommandParameter="{Binding Source={x:Reference viewCell}, Path=BindingContext}" />
</ContentView.GestureRecognizers>
<Frame OutlineColor="{Binding Color}" Padding="8">
<StackLayout Orientation="Horizontal" >
<Image x:Name="checkedImage"
HeightRequest="30"
WidthRequest="30"
BackgroundColor="{Binding Color}"
/>
<Label Text="{Binding Text}"
TextColor="{Binding Color}"
Margin="20,0,0,0"
VerticalTextAlignment="Center"
HorizontalOptions="FillAndExpand"/>
<Image Source="{Binding Image}" />
</StackLayout>
</Frame>
</ContentView>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Here's my ViewModel:
public class DetailPageViewModel : BindableBase
{
public DetailPageViewModel()
{
_listItems.Add(new ViewModels.Item("#123456", "Test1", "XamarinForms.Assets.Yellow.png"));
_listItems.Add(new ViewModels.Item("#654321", "Test3", "XamarinForms.Assets.close.png"));
}
private ObservableCollection<Item> _listItems = new ObservableCollection<Item>();
public ObservableCollection<Item> ListItems
{
get { return _listItems; }
set { SetProperty(ref _listItems, value); }
}
public DelegateCommand<Item> ItemTappedCommand => new DelegateCommand<Item>(ItemTapped);
private void ItemTapped(Item listItem)
{
// Adding an item refreshes the UI.
_listItems.Add(new ViewModels.Item("#654321", "Test3", "XamarinForms.Assets.close.png"));
// But changing the color of an item does nothing. Not even after an UI refresh.
_listItems.ElementAt(_listItems.IndexOf(listItem)).Color="#987654";
}
}
And my Item class:
public class Item
{
public string Color { set; get; }
public ImageSource Check { set; get; }
public string Text { private set; get; }
public ImageSource Image { private set; get; }
public Item(string color, string text, string imageSource)
{
Check = ImageSource.FromResource("XamarinForms.Assets.checkmark-outlined-verification-sign.png");
Color = color;
Text = text;
Image = ImageSource.FromResource(imageSource);
}
}

This is because also your item class needs to implement INotifyPropertyChanged. In your case as you are using Prism you just need to make your item class extend BindableBase (Prism base class which already implements INotifyPropertyChanged for you).
Link: https://github.com/PrismLibrary/Prism/blob/a60d38013c02b60807e9287db9ba7f7506af0e84/Source/Prism/Mvvm/BindableBase.cs
That should make it work.
Also I see in you are doing this:
public ObservableCollection<Item> ListItems
{
get { return _listItems; }
set { SetProperty(ref _listItems, value); }
}
With ObservableCollections you don't need to raise the event manually as they already do it internally. They can be defined as regular properties.
public ObservableCollection<Item> ListItems {get; set;}

Related

Bound items display as name of item type, instead of contents of each item

//My Model
public class BookInfo
{
public string BookName { get; set; }
public string BookDescription { get; set; }
}
//my View Model
public ObservableCollection<BookInfo> Bookmodel { get; set; }
public BookRepoInfo()
{
Bookmodel = new ObservableCollection<BookInfo> { //**is this correct way.**
new BookInfo { BookName = "Object-Oriented Programming in C#", BookDescription = "Object-oriented programming is a programming paradigm based on the concept of objects" },
......
};
}
XAML page:
<ContentPage.BindingContext>
<local:BookRepoInfo />
</ContentPage.BindingContext>
<X:YList ItemsSource="{Binding Bookmodel}"></X:YList>
Load the list item using MVVM pattern
Assuming that YList is either a inheritance from ListView or CollectionView, you'll need to provide some sort of template which you want to apply to each cell of that list.
Right now what is happening is that it will just call the ToString() on the object that you put in.
Change your code to be something like:
<X:YList ItemsSource="{Binding Bookmodel}">
<X:YList.ItemTemplate>
<DataTemplate>
<VerticalStackLayout>
<Label Text="{Binding BookName}"/>
<Label Text="{Binding BookDescription}"/>
</VerticalStackLayout>
</DataTemplate>
</X:YList.ItemTemplate>
</X:YList>
More information is here: https://learn.microsoft.com/dotnet/maui/user-interface/controls/collectionview/populate-data?view=net-maui-7.0#define-item-appearance

How to create a footer template in a Grouped ListView in Xamarin Form

I'm working with Xamarin Form and C#. So, I have a listview in a Xamarin project which I enable the property IsGroupingEnabled. The items and header of items work perfectly but I cannot set a footer template.
I need to create a listview, items must be composed of:
Header: User information.
Items: List of subitems.
Footer: Actions buttons (shared, comments, etc).
This is part of my code:
Model:
public class Post
{
public long PostID { get; set; }
public string Name { get; set; }
public List<OptionDefault> OptionsDefault { get; set; }
}
public class OptionDefault
{
public long OptionTypeID { get; set; }
public string SubItemName { get; set; }
}
ViewModel
public class PostsViewModel
{
public InfiniteScrollCollection<Grouping<Post, OptionDefault>> Items { get; set; } = new InfiniteScrollCollection<Grouping<Post, OptionDefault>>();
public IPostsService repository;
public PostsViewModel(INavigation navigation)
{
repository = new PostsService();
Items = new InfiniteScrollCollection<Grouping<Post, OptionDefault>>
{
OnLoadMore = async () => await GetItems()
};
Items.LoadMoreAsync();
}
async Task<List<Grouping<Post, OptionDefault>>> GetItems()
{
IsWorking = true;
List<Grouping<Post, OptionDefault>> items = new List<Grouping<Post, OptionDefault>>();
try
{
// load the next page
var lists = await repository.GetList(Items.Count);
foreach (var item in lists)
{
for (int i = 0; i < item.OptionsDefault.Count; i++)
{
if ((i + 1) == item.OptionsDefault.Count)
item.OptionsDefault[i].LastItem = true;
}
var group = new Grouping<Post, OptionDefault>(item, item.OptionsDefault);
items.Add(group);
}
}
catch (Exception ex)
{
ErrorHelper.RegisterError(ex);
}
finally
{
IsWorking = false;
}
return items;
}
.xaml:
<ListView ItemsSource="{Binding Items}"
IsGroupingEnabled="True">
<ListView.GroupHeaderTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Margin="5,15,5,0">
<Label Text="{Binding Key.User.UserName}" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.GroupHeaderTemplate>
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Margin="5,0,5,5">
<Label Text="{Binding SubItemName}" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
I've tried using footertemplate, but it doesn't work:
<ListView.FooterTemplate>
<DataTemplate>
<StackLayout Margin="5,0,5,5">
<Label Text="This is my footer" />
</StackLayout>
</DataTemplate>
</ListView.FooterTemplate>
Whether it's <ListView.Footer> or <ListView.FooterTemplate>, the display position is the same, right at the bottom of the ListView.
Currently, ListView of xamarin form does not have this property to make each items have a footer.
Maybe you need to rethink the requirements and UI design of your app.
CollectionView is now available in Xamarin.Forms. It have both GroupHeaderTemplate and GroupFooterTemplate. You can use it to show the Group of data. The code to do so is identical with ListView
It is not <ListView.FooterTemplate> but <ListView.Footer>
<ListView.Footer>
<StackLayout Margin="5,0,5,5">
<Label Text="This is my footer" />
</StackLayout>
</ListView.Footer>
For more info, see:
https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/listview/customizing-list-appearance#headers-and-footers

Xamarin Forms - How to write typeof() in XAML?

The xamarin forms class DataTemplate recieves a parameter with Type in ctor.
public class DataTemplate : ElementTemplate, IDataTemplateController
{
public DataTemplate(Type type);
...
}
How to pass the type of a specific class to a ctor in XAML? In c# I would write new DataTemplate(typeof(DeviceViewModel)). But I have to write this in XAML.
Pseudo code:
<DataTemplate>
<x:Arguments>
<typeof(viewModels:DeviceViewModel)/>
</x:Arguments>
<myControls:MyCustomControl/>
</DataTemplate>
EDIT
To make my goals more clear I created a derived example. Let's say there is following structure:
Picture
Music
Document
txt
pdf
xml
My List contains these elements. Every of this type has to be handled in own DataTemplate. In my case the items are quite complex so thats why I create a ViewModel for each Item. In code it could look like this:
public abstract class BaseViewModel
public class PictureViewModel : BaseViewModel
public class MusicViewModel : BaseViewModel
public class DocumentViewModel : BaseViewModel
/* My List full of different ViewModels*/
List<BaseViewModel> itemList;
Now I create a TemplateSelector which holds other TemplateSelector. It calls the right one based on view model type:
using System;
using Xamarin.Forms;
using MyApp.ViewModels;
namespace MyApp.TemplateSelectors
{
public class MyItemTemplateSelector : DataTemplateSelector
{
public DataTemplateSelector PictureTemplateSelector { get; set; }
public DataTemplateSelector MusicTemplateSelector { get; set; }
public DataTemplateSelector DocumentTemplateSelector { get; set; }
public DataTemplateSelector DefaultTemplate { get; set; }
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
{
switch (item)
{
case PictureViewModel vm:
return PictureTemplateSelector.SelectTemplate(item, container);
case MusicViewModel vm:
return MusicTemplateSelector.SelectTemplate(item, container);
case DocumentViewModel vm:
return DocumentTemplateSelector.SelectTemplate(item, container);
default:
return DefaultTemplate.SelectTemplate(item, container);
}
}
}
}
In XAML all the assignment happens:
<DataTemplate x:Key="PictureDefaultTemplate">
<ViewCell>
<Image Source="{Binding FilePath}"/>
</ViewCell>
</DataTemplate>
<DataTemplate x:Key="MusicDefaultTemplate">
<ViewCell>
<Button Text="Play this!"/>
</ViewCell>
</DataTemplate>
<DataTemplate x:Key="DocumentDefaultTemplate">
<ViewCell>
<Label Text="show filename at least"/>
</ViewCell>
</DataTemplate>
<!-- The DocumentTemplateSelector handles file extensions differently -->
<DataTemplate x:Key="MyTxtTemplate">
<ViewCell>
<Label Text="This is a text file"/>
</ViewCell>
</DataTemplate>
<DataTemplate x:Key="MyPdfTemplate">
<ViewCell>
<Label Text="This is a pdf file"/>
</ViewCell>
</DataTemplate>
<DataTemplate x:Key="MyXmlTemplate">
<ViewCell>
<Label Text="This is a xml File"/>
</ViewCell>
</DataTemplate>
<!--provide specific template selectors with initiated datatemplates-->
<templateSelectors:PictureTemplateSelector x:Key="pictureTemplateSelector" DefaultTemplate="{StaticResource PictureDefaultTemplate}"/>
<templateSelectors:MusicTemplateSelector x:Key="musicTemplateSelector" DefaultTemplate="{StaticResource MusicDefaultTemplate}"/>
<templateSelectors:DocumentTemplateSelector x:Key="documentTemplateSelector" DefaultTemplate="{StaticResource DocumentDefaultTemplate}"
TxtTemplate="{StaticResource MyTxtTemplate}"
PdfTemplate="{StaticResource MyPdfTemplate}"
XmlTemplate="{StaticResource MyXmlTemplate}"/>
<!--provide superior template selector with other template selectors -->
<templateSelectors:MyItemTemplateSelector x:Key="myItemTemplateSelector"
PictureTemplateSelector="{StaticResource pictureTemplateSelector}"
MusicTemplateSelector="{StaticResource musicTemplateSelector}"
DocumentTemplateSelector="{StaticResource documentTemplateSelector}"/>
Last bind MyItems (objects of type BaseViewModel) and set superior template selector. This is where I set the CachingStrategy to RecycleElementAndDataTemplate:
<ListView ItemsSource="{Binding MyItems}"
ItemTemplate="{StaticResource myItemTemplateSelector}"
IsPullToRefreshEnabled="True"
CachingStrategy="RecycleElementAndDataTemplate"/>
The Exception is thrown in DocumentTemplateSelector.
using System;
using Xamarin.Forms;
using MyApp.ViewModels;
namespace MyApp.TemplateSelectors
{
public class DocumentTemplateSelector : DataTemplateSelector
{
public DataTemplate TxtTemplate { get; set; }
public DataTemplate PdfTemplate { get; set; }
public DataTemplate XmlTemplate { get; set; }
public DataTemplate DefaultTemplate { get; set; }
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
{
var doc = (DocumentViewModel)item;
switch (doc.FileExtension)
{
case "txt":
return TxtTemplate; // Exception
case "pdf":
return PdfTemplate; // Exception
case "xml":
return XmlTemplate; // Exception
default:
return DefaultTemplate; // Exception
}
}
}
}
Following exception is thrown:
System.NotSupportedException: RecycleElementAndDataTemplate requires DataTemplate activated with ctor taking a type.
#IvanIčin #G.hakim I'm not worried that the default mechanism is not working but after studying the xamarin forms code from github I see no other solution to make it work without passing the type in DataTemplate ctor.
Update: new Xamarin Forms enhancement proposal
https://github.com/xamarin/Xamarin.Forms/issues/7060
Above enhancement would solve my request.

How to code a UWP MVVM ComboBox within an ItemTemplate?

I'm trying to figure out how to display a category for a selected item in a ComboBox which is itself within a GridView.ItemsTemplate. Nothing that I have tried so far has worked. I've tried using SelectedValue and SelectedValuePath.
I can display the Item's Category in a textbox from within the GridView.ItemsTemplate, but not the collection in a ComboBox.
// FiledNotesPageViewModel ViewModel
public class FiledNotesPageViewModel : ViewModelBase
{
public ObservableCollection<MemoryItem> NoteItems { get; set; } = new ObservableCollection<MemoryItem>();
}
// Public Properties
public int FiledNotesId
{
get
{
return _filedNotesId;
}
set
{
_filedNotesId = value;
OnPropertyChanged("FiledNotesId");
}
}
public string Name
{
get
{
return _name;
}
set
{
_name = value;
OnPropertyChanged("Name");
}
}
public string Note
{
get
{
return _note;
}
set
{
_note = value;
OnPropertyChanged("Note");
}
}
public string Category
{
get
{
return _category;
}
set
{
_category = value;
OnPropertyChanged("Category");
}
}
// MemoryItem Model
public int FiledNotesId { get; set; }
public string Note { get; set; }
public string Name { get; set; }
public string Category { get; set; }
FiledNotesPage View
<GridView x:Name="FiledNotesGrid"
RelativePanel.Below="FilterNoteGrid"
ItemsSource="{x:Bind ViewModel.NoteItems}">
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsWrapGrid Orientation="Horizontal"
Margin="5"/>
</ItemsPanelTemplate>
</GridView.ItemsPanel>
<GridView.ItemTemplate>
<DataTemplate x:DataType="data:MemoryItem">
<StackPanel Orientation="Vertical"
BorderBrush="{ThemeResource AppBarBorderThemeBrush}"
BorderThickness="2,2,2,4"
Margin="20"
Background="#FFF7F713" >
<TextBox x:Name="NoteNameTextBox"
Text="{x:Bind Name, Mode=TwoWay}"/>
<TextBox x:Name="NoteTextBox"
TextWrapping="Wrap"
AcceptsReturn="True"
Text="{x:Bind Note, Mode=TwoWay}"/>
<StackPanel Orientation="Horizontal">
<TextBlock Margin="5,0">Category:</TextBlock>
<ComboBox x:Name="NoteCategoryComboBox"
SelectedItem="{Binding FiledNotesId, Mode=TwoWay}"
MinWidth="125">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Category}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
</StackPanel>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
Your combobox doesn't have an ItemsSource
You need to do something like this...
In your CS File create an ObservableCollection property to bind to.
public ObservableCollection<int> FieldNoteIds {get; set;}
In your view, bind to your collection
<ComboBox ItemsSource="{Binding FieldNoteIds}">
Right now you're providing a template for how to display each item that is populated into the combobox but you're not giving it anything to populate.
MSDN Tutorial on ComboBoxes and Binding
https://code.msdn.microsoft.com/windowsdesktop/Best-ComboBox-Tutorial-5cc27f82#content

ViewCell not binding

I have the following listView with binding on my xaml page. I am binding data to it from my viewModel. But the values are not binding to the cell.
<ListView x:Name="historyList" ItemsSource="{Binding History}" HasUnevenRows="true">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ViewCell.View>
<StackLayout Orientation="Horizontal" Padding="10, 5, 10, 5" Spacing="0" HorizontalOptions="FillAndExpand" >
<Label Text="{Binding History.DayNumber}" HorizontalOptions="Center" FontSize="15" TextColor="Red" VerticalOptions="Center"></Label>
</StackLayout>
</ViewCell.View>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
My History object has the property DayNumber, but the value is not binding to the label?
Any suggestion? or am i missing something?
Thank you.
Edit:
Here is how I get my History List:
void GetAllHistory()
{
foreach(CheckInTimeHistory h in App.Database.GetHistoryCheckins(null))
{
CheckInHistoryItem hi = new CheckInHistoryItem();
hi.CheckedIn = (h.AutoCheckIn ? false : true);
hi.CheckInTime = h.CheckInTime.AddSeconds(-h.CheckInTime.Second).ToString("HH:mm-ss tt");
hi.DayNumber = h.CheckInTime.Day.ToString();
hi.DayText = h.CheckInTime.DayOfWeek.ToString().Substring(0, 2);
History.Add(hi);
}
}
public class CheckInHistoryItem
{
public String DayText, DayNumber, CheckInTime;
public Boolean CheckedIn;
}
You will see warnings like this in your debug output:
Binding: 'DayNumber' property not found on '....CheckInHistoryItem', target property: 'Xamarin.Forms.Label.Text'
The properties have to be C# properties. Just change your class to
public class CheckInHistoryItem
{
public String DayText { get; set; }
public String DayNumber { get; set; }
public String CheckInTime { get; set; }
public Boolean CheckedIn { get; set; }
}
and use DayNumber instead of History.DayNumber
<Label Text="{Binding DayNumber}" HorizontalOptions="Center" FontSize="15" TextColor="Red" VerticalOptions="Center"></Label>
Assuming History is a static var:
ItemsSource="{x:Static local:History}">
And change:
{Binding History.DayNumber}
to:
{Binding DayNumber}
Binding Basics:
https://developer.xamarin.com/guides/xamarin-forms/user-interface/xaml-basics/data_binding_basics/#Bindings_and_Collections