New Xamarin.Forms CollectionView doesn't allow multi pre-selection - c#

I have a CollectionView in my Xamarin.Forms project:
<CollectionView ItemsSource="{Binding Categories}" ItemSizingStrategy="MeasureFirstItem" x:Name="CategoryColView"
SelectionMode="Multiple" SelectionChangedCommand="{Binding SelectionChangedCommand}"
SelectionChangedCommandParameter="{Binding Source={x:Reference CategoryColView}, Path=SelectedItems}"
SelectedItems="{Binding SelectedCategoryItems}">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout ...>
<BoxView .../>
<StackLayout ...>
<Label .../>
<Image .../>
</StackLayout>
<BoxView/>
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
I included the entire XAML element, but the only important part is the SelectedItems property. It is bound to the following viewmodel implementation:
class ViewModel {
private ObservableCollection<object> selectedCategories { get; set; }
public ObservableCollection<object> SelectedCategories {
get => selectedCategories;
set {
selectedCategories = value;
OnPropertyChanged();
}
//...
ctor() {
//...
var alreadySelectedCategoryItems = alreadySelectedCategories.Select(pc => new CategoryItem { PlantCategory = pc, IsSelected = true }).Cast<object>();
SelectedCategoryItems = new ObservableCollection<object>(alreadySelectedCategoryItems);
//...
}
}
The rest of the implementation should be irrelevant. My aim is to have pre-selected values.
First: I noticed that if the T in ObservableCollection<T> is not object, everything is ignored. Just like in Microsoft's example here. If the T is e.g. of type CategoryItem, literally nothing happens, as if the ObserveableCollection were completely ignored.
Second: alreadySelectedCategoryItem contains 2 elements in debugger mode, but then the last line in the constructor throws a:
System.ArgumentOutOfRangeException
Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index
Of course, since this is Xamarin.Forms and VS for Mac, the error is thrown on the Main function, not at its actual location...
Am I doing something wrong, or is CollectionView just still buggy?

The issue was that I was creating new CategoryItem instances as the pre-selected ones, which is invalid, as they weren't by default the same instances that were in the CollectionView.ItemsSource property. I should have filtered the ItemsSource instances and put them as the pre-selected ones. Like this:
var alreadySelectedCategoryItems = alreadySelectedCategories.Select(pc => new CategoryItem { PlantCategory = pc, IsSelected = true }).Cast<object>();
SelectedCategoryItems = Categories
.Where(sci =>
alreadySelectedCategoryItems.Any(alreadySelected =>
alreadySelected.PlantCategory.Id == sci.PlantCategory.Id);
So the items are selected off the ItemsSource itself and not created as new.
Although the error message was not as desired, so Xamarin.Forms team is going to fix that.

SelectedItems is read-only,you could not use like SelectedItems="{Binding SelectedCategoryItems}" in xaml
you could try to change in your behind code like:
CategoryColView.SelectedItems.Add(your selectItem 1);
CategoryColView.SelectedItems.Add(your selectItem 2);
CategoryColView.SelectedItems.Add(your selectItem 3);

Related

How to get ListView item index from custom ViewCell in Xamarin Forms?

I created a ListView that have a custom ViewCell as below:
<ListView x:Name="ListView1" ItemTapped="ListView1_ItemTapped"
SeparatorVisibility="None" RowHeight="192" HasUnevenRows="False"
FlowDirection="RightToLeft" CachingStrategy="RecycleElement" >
<ListView.ItemTemplate>
<DataTemplate>
<custom:ViewCell1 />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
and here is the XAML for the custom ViewCell
<ViewCell.View>
<StackLayout>
<Label Text="{Binding Name}" />
<Label Text="{Binding ID}" />
<Button x:Name="Button1" Text="Get index" Clicked="Button1_Clicked" />
</StackLayout>
</ViewCell.View>
All I need is when I click on Button1, I get the ListView1 item index (or ViewCell index)
The problem is I can't access ListView1 from Button1_Clicked event from the code behind in the custom ViewCell and gets tapped item index of ListView1 (or even gets ViewCell tapped item index).
I searched a lot, and found that it can be done by 3 ways:
1- Create an attached property for the ViewCell to get its index.
2- Use indexer for the ViewCell and get its index.
3- Use ITemplatedItemsView interface as mentioned in this question
But unfortunately I couldn't implement any of them from Button1_Clicked event in the custom ViewCell code behind because I'm not an expert in either MVVM or C#.
Can I have an expert assistance please.
Thanks
There are many ways which can implements it . If you are new to data binding and MVVM. I will provide the easiest way .
Firstly , add a property in the model of ItemSource .
public class YourModel
{
public int Index { get; }
//other properties like name and ID
public YourModel(int index)
{
Index = index;
}
}
And set the value of Index when init the ItemSource of ListView .
sources = new ObservableCollection<YourModel>() { };
for(int i=0;i<20;i++)
{
sources.Add(new YourModel(i) { /**other propertes**/});
}
In CustomCell
Get it like following
var model = this.BindingContext as YourModel;
int index = model.Index;
try using Button1.Parent.Parent.Parent... and so on, unless you get your listview's object.
Also pass BindingContext of viewcell in button's CommandParameter like CommandParameter={Binding} and then get the index of your received object from ItemsSource.

I need to put my table data into ListView (MVVM)

Database connection and binding context are okay. Data can insert into a table without a problem. I need to add last of my table data row to a list view
<ListView ItemsSource="{Binding YourList}"
verticalOptions="FillAndExpand" >
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Horizontal">
<Label Text="{Binding Catagoryview}"/>
<Label Text="{Binding Date}"/>
<Label Text="{Binding Expense}"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
this is my XAML file. I create every property for in binding here. with backing fields.
in my ViewModel
private void add()
{
database = new Database();
var expensedata = database.GetExpenses(id);
if (expensedata != null)
{
Catagoryview = expensedata.Catagory;
Date= expensedata.Date;
Expense = expensedata.Expense;
}
}
I know this code is not perfect. I really need to get the last 10 rows of my data into a list view.
also, here my get expenses method I used
public AddExpenses GetExpenses(int expense)
{
return Conn.Table<AddExpenses>().FirstOrDefault(t => t.Id == expense);
}
To me it seems as if your add method does not actually add anything. In your viewmodel you'll need to have a public property YourList, which is propagated with add. Furthermore you are getting only one item from your expenses database, which won't work for adding 10 items.
The core question as far as I understood is
How can I add the 10 last items from an SQLite database to a Xamarin.Forms ListView using an MVVM approach.
Disregarding the fact that I do not know how and when you are calling add, I'll try to give an answer on how to get and add these elements.
First of all we need a property that will hold your items in your viewmodel. Depending on your requirements, either an IEnumerable<AddExpenses> or an ObservableCollection<AddExpenses> will do. I will demonstrate the ObservableCollection approach
ObservableCollection<AddExpenses> _expenses;
public ObservableCollection<AddExpenses> Expenses
{
get
{
if(_expenses == null)
{
_expenses = new ObserrvableCollection<AddExpenses>();
}
return _expenses;
}
}
You can bind to that property from your XAML
<ListView ItemsSource="{Binding Expenses}"
VerticalOptions="FillAndExpand">
<!-- What ever -->
</ListView>
And then add items to that collection from your AddExpenses method (I felt free to give it a more meaningful name):
private void AddExpenses()
{
var database = new Database();
var expenses = database.GetLastExpenses(numberOfExpenses: 10);
foreach(var expense in expenses)
{
Expenses.Add(expense);
}
}
Since we are now having objects of type AddExpenses in your collection, we'll have to change the binding in the ListView from Categoryview to Category:
<ListView ItemsSource="{Binding YourList}"
VerticalOptions="FillAndExpand">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Horizontal">
<Label Text="{Binding Category}"/>
<Label Text="{Binding Date}"/>
<Label Text="{Binding Expense}"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Last but not least we'll have to implement GetLastExpenses (this should work, while it's possible that there are better options)
public AddExpenses[] GetLastExpenses(int numberOfExpenses)
{
return Conn.Table<AddExpenses>()
.OrderByDescending(expenses => expenses.Date)
.Take(numberOfExpenses)
.OrderBy(expenses => expenses.Date) // If you need them ordered ascending
.ToArray()
}

Strange performance issue with WPF ComboBox binding

I have a performance issue that I just solved but I really don't understand why the solution works.
I have a ComboBox with about 4,000 items that I bind to a collection using the ItemSource property; if I bind to a property in the view-model with a getter and a setter everything works fine, but if I bind to a property with only a getter, the first time that I click on the combobox it works fine but everytime after that first time if I click on the combobox the application hangs for about 1 minute with the CPU for the process at ~100% before displaying the combo box items
View:
...
<ComboBox
Grid.Column="1"
ItemsSource="{Binding AvailableDispositionCodes}"
DisplayMemberPath="DisplayName"
SelectedItem="{Binding SelectedDispositionCode}"
Width="Auto"
Height="25"
Margin="5 0 0 0">
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
</ComboBox>
...
Working view-model:
...
private IEnumerable<DispositionCodeViewModel> availableDispositionCodes = new List<DispositionCodeViewModel>();
...
public IEnumerable<DispositionCodeViewModel> AvailableDispositionCodes
{
get
{
return this.availableDispositionCodes;
}
set
{
this.availableDispositionCodes = value;
this.OnPropertyChanged();
}
}
...
public void Initialize()
{
...
this.AvailableDispositionCodes = resultCodeViewModels.OrderBy(x => x.Name);
...
}
...
View-model that causes the application to hang:
...
private List<DispositionCodeViewModel> availableDispositionCodes = new List<DispositionCodeViewModel>();
...
public IEnumerable<DispositionCodeViewModel> AvailableDispositionCodes
{
get { return this.availableDispositionCodes; }
}
...
public void Initialize()
{
...
this.availableDispositionCodes.AddRange(resultCodeViewModels.OrderBy(x => x.Name));
this.OnPropertyChanged(nameof(this.AvailableDispositionCodes));
...
}
...
The method Initialize of the view-model initializes the collection that is binded to the combobox and this method is called just once shortly after the view and the view-model are created. After that the collection doesn't change
Does anybody knows what causes this weird behavior?
I think it is about the List.AddRange() rather than the property.
If the new Count (the current Count plus the size of the collection) will be greater than Capacity, the capacity of the List is increased by automatically reallocating the internal array to accommodate the new elements, and the existing elements are copied to the new array before the new elements are added.
msdn

Unable to reselect single item in listview

I have a listview in WPF in an MVVM/PRISM app which may contain 1-to-many elements. When the listview contains only 1 element, and I select it, I cannot subsequently reselect it even though I set the SelectedIndedx value to -1. Worse, if I make the app update the listview with a different single element, I can't select that one either. The only way I can achieve selection of an item when it is the only item in the listview is to make the app display multiple items and select something other than the first. Then, when I make the app display a listview containing a single item, I can select it again - but only once.
In those cases where I cannot select the single item in the listview, the servicing routine never fires.
I tried implementing a XAML suggestion I found here using "Listview.Container.Style" and the IsSelected property, but that did not work.
My listview is fairly straightforward:
<ListView Name="lstEditInstance"
Grid.Row="5"
ItemsSource="{Binding Path=InstanceList,Mode=TwoWay}"
Width="488"
FontFamily="Arial" FontSize="11"
HorizontalAlignment="Left" VerticalAlignment="Stretch"
Margin="10,96,0,28"
SelectedIndex="{Binding Path=InstanceSelectedIndex}">
</ListView>
The servicing routine is:
private void OnInstanceSelectedIndexChanged()
{
// Handle case where user hits Enter without making a selection:
if (_instanceIndex == -1) return;
// Get the instance record for the row the user clicked on as a
// ResourceInstance class named "InstanceRecord".
InstanceRecord = _instanceList[_instanceIndex];
_instanceNumber = InstanceRecord.Instance;
FormInstName = InstanceRecord.InstName;
FormInstEnabled = InstanceRecord.Enabled;
FormInstState = InstanceRecord.InitialState;
FormInstIPAddress = InstanceRecord.IPAddress;
FormInstPort = InstanceRecord.Port.ToString();
FormInstSelectedURL = InstanceRecord.UrlHandler;
} // End of "OnResourceSelectedIndexChanged" method.
"InstanceList" is an observable collection.
I'd appreciate some suggestions. Thanks in advance for any help.
In a MVVM scenario, I'd use a ViewModel that contains the selected item instead:
class MyViewModel {
private IList<Item> instanceList= new List<Item>();
public IList<Item> List
{
get {return list; }
set {
list = value;
RaisePropertyChanged(() => List);
}
}
private Item selectedItem;
public Item SelectedItem {
get {return selectedItem;}
set {
selectedItem = value;
RaisePropertyChanged(() => SelectedItem);
}
}}
And the XAML:
<ListView Name="lstEditInstance"
Grid.Row="5"
ItemsSource="{Binding Path=InstanceList}"
Width="488"
FontFamily="Arial" FontSize="11"
HorizontalAlignment="Left" VerticalAlignment="Stretch"
Margin="10,96,0,28"
SelectedItem="{Binding Path=SelectedItem, Mode=TwoWay}}">
Notice that observableCollection is not required unless you have to modify the list items, in the same way the binding should be the default one for the list.
The SelectedItem / SelectedIndex should be TwoWay or Onewaytosource, the latter if you think you don't need to change the selectedItem programmatically
The service routine should be called from the ViewModel
EDIT:
your code of the service routine should be placed there:
set {
selectedItem = value;
// your code
RaisePropertyChanged(() => SelectedItem);
}
Another valid approach is to use Blend on XAML, by invoking a command on changed index and process under the ViewModel.
To do this, first add reference to System.Windows.Interactivity in your project and in XAML add
xmlns:interactivity="http://schemas.microsoft.com/expression/2010/interactivity
Then modify ListView with the following:
<ListView Name="lstEditInstance"
Grid.Row="5"
ItemsSource="{Binding Path=InstanceList}"
Width="488"
FontFamily="Arial" FontSize="11"
HorizontalAlignment="Left" VerticalAlignment="Stretch"
Margin="10,96,0,28"
SelectedItem="{Binding Path=SelectedItem, Mode=TwoWay}}">
<interactivity:Interaction.Triggers>
<interactivity:EventTrigger EventName="SelectionChanged">
<interactivity:InvokeCommandAction Command="{Binding YourCommand}"
CommandParameter="{Binding YourCommandParameter}" />
</interactivity:EventTrigger>
</interactivity:Interaction.Triggers>

ICollectionView's SourceCollection is null

I have a ViewModel with two ICollectionViews which are bound as ItemsSources to two different ListBoxes. Both wrap the same ObservableCollection, but with different filters. Everything works fine initially and both ListBoxes appear properly filled.
However when I change an item in the ObservableCollection and modify a property which is relevant for filtering, the ListBoxes don't get updated. In the debugger I found that SourceCollection for both ICollectionVIews is null although my ObservableCollection is still there.
This is how I modify an item making sure that the ICollectionViews are updated by removing and adding the same item:
private void UnassignTag(TagViewModel tag)
{
TrackChangedTagOnCollectionViews(tag, t => t.IsAssigned = false);
}
private void TrackChangedTagOnCollectionViews(TagViewModel tag, Action<TagViewModel> changeTagAction)
{
_tags.Remove(tag);
changeTagAction.Invoke(tag);
_tags.Add(tag);
}
The mechanism works in another context where I use the same class.
Also I realized that the problem disappears if I register listeners on the ICollectionViews' CollectionChanged events. I made sure that I create and modify them from the GUI thread and suspect that garbage collection is the problem, but currently I'm stuck... Ideas?
Update:
While debugging I realized that the SourceCollections are still there right before I call ShowDialog() on the WinForms Form in which my UserControl is hosted. When the dialog is shown they're gone.
I create the ICollectionViews like this:
AvailableTags = new CollectionViewSource { Source = _tags }.View;
AssignedTags = new CollectionViewSource { Source = _tags }.View;
Here's how I bind one of the two (the other one is pretty similar):
<ListBox Grid.Column="0" ItemsSource="{Binding AvailableTags}" Style="{StaticResource ListBoxStyle}">
<ListBox.ItemTemplate>
<DataTemplate>
<Border Style="{StaticResource ListBoxItemBorderStyle}">
<DockPanel>
<Button DockPanel.Dock="Right" ToolTip="Assign" Style="{StaticResource IconButtonStyle}"
Command="{Binding Path=DataContext.AssignSelectedTagCommand, RelativeSource={RelativeSource AncestorType={x:Type tags:TagsListView}}}"
CommandParameter="{Binding}">
<Image Source="..."/>
</Button>
<TextBlock Text="{Binding Name}" Style="{StaticResource TagNameTextBlockStyle}"/>
</DockPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I use MvvmLight's RelayCommand<T> as ICommand implementation in my ViewModel:
AssignSelectedTagCommand = new RelayCommand<TagViewModel>(AssignTag);
I had this issue too, with a similar use-case. When I updated the underlying collection, I would call Refresh() on all the filtered views. Sometimes, this would result in a NullReferenceException thrown from within ListCollectionView.PrepareLocalArray() because SourceCollection is null.
The problem is that you shouldn't be binding to the CollectionView, but to the CollectionViewSource.View property.
Here's how I do it:
public class ViewModel {
// ...
public ViewModel(ObservableCollection<ItemViewModel> items)
{
_source = new CollectionViewSource()
{
Source = items,
IsLiveFilteringRequested = true,
LiveFilteringProperties = { "FilterProperty" }
};
_source.Filter += (src, args) =>
{
args.Accepted = ((ItemViewModel) args.Item).FilterProperty == FilterField;
};
}
// ...
public ICollectionView View
{
get { return _source.View; }
}
// ...
}
The reason for your issue is that the CollectionViewSource is getting garbage collected.

Categories

Resources