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.
Related
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);
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
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>
I have an existing solution of my WPF UI but it's ViewModel implementation is clunky and I'm looking to improve.
Below is a gif of how my current system works:
There's a Current Task (note: only ever one item)
There's a Task List for Tasks (note: possibly many) that need to run in the future
When the user selects one list box, the other selection is removed
The problem is, I'm implementing Current Task as a Listbox with only one item. This means I have to lug around a backing IList for the ItemSource and another property for the SelectedItem.
Is there another control I can use to behave like ListBoxItem, but I can bind my CurrentTask directly to it and not have to muck around with an List for ItemSource as well?
EDIT: To get the selection to go away when one listbox is selected, I have a trigger set up on the SelectionChanged event.
(deleted my previous answer)
It occurs to me that at least part of the functionality you're looking for is implemented by the RadioButton class. Multiple RadioButtons in the same scope guarantee that only one of them is selected. You'll probably have to do a little work to make sure that your RadioButtons can be scoped correctly in your UI, and you'll probably need to retemplate some things to get exactly the UI you need. Additionally, RadioButton does not have a SelectedItem/SelectValue property to which it can write to, because WPF provides no built-in mechanism for multiple controls to safely bind to a "SelectedWhatever" property. But you could roll this yourself pretty easily with codebehind or triggers.
Here's the implementation I went with:
XAML View
<!-- The Current Task box -->
<ListBox x:Name="CurrentTaskBox" FlowDirection="RightToLeft" Background="{StaticResource WhiteBrush}">
<ListBoxItem IsSelected="{Binding CurrentTaskSelected, Mode=TwoWay}" Content="{Binding CurrentTask.TaskId}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Selected">
<command:EventToCommand Command="{Binding SetTaskDetailsFromCurrentTaskCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ListBoxItem>
</ListBox>
<!-- The Task List box -->
<ListBox x:Name="TaskListBox" SelectedIndex="{Binding TaskListIndex}" SelectedValue="{Binding TaskListSelection}" IsSynchronizedWithCurrentItem="True" HorizontalContentAlignment="Stretch" ItemsSource="{Binding TaskList}" FlowDirection="RightToLeft" DisplayMemberPath="TaskId" Margin="3">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<command:EventToCommand Command="{Binding SetTaskDetailsFromTaskListCommand}" CommandParameter="{Binding SelectedItem, ElementName=TaskListBox}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ListBox>
ViewModel
/* Omitted most INPC property declarations...kinda boring */
public ICommand SetTaskDetailsFromCurrentTaskCommand { get { return new RelayCommand(SetTaskDetailsFromCurrentTask); } }
public ICommand SetTaskDetailsFromTaskListCommand { get { return new RelayCommand<TaskScheduleSequenceDto>(async taskSelection => await SetTaskDetailsFromTaskList(taskSelection)); } }
private bool _currentTaskSelected;
public bool CurrentTaskSelected
{
get
{
return _currentTaskSelected;
}
set
{
Set(() => CurrentTaskSelected, ref _currentTaskSelected, value);
}
}
private async Task SetTaskDetailsFromTaskList(TaskScheduleSequenceDto taskListSelection)
{
if (taskListSelection == null)
{
return;
}
var taskDetails = await _broker.RetrieveTaskDetails(taskListSelection.TaskId);
TaskDetails = taskDetails;
CurrentTaskSelected = false;
}
private void SetTaskDetailsFromCurrentTask()
{
TaskDetails = CurrentTask;
TaskListSelection = null;
CurrentTaskSelected = true;
}
This works fine and only requires that I have a single CurrentTask property in my VM, which I think is much cleaner.
As i am not very advanced in C# yet, I try to learn how to make my code more efficient.
I stored a lot of strings in some of the properties.
At the start of the application, i load all the seperatie properties into the textboxes.
I now ise this code to load them all:
private void LoadStoredStrings()
{
txtT1S1.Text = Properties.Settings.Default.strT1L1;
txtT1S2.Text = Properties.Settings.Default.strT1L2;
txtT1S3.Text = Properties.Settings.Default.strT1L3;
txtT1S4.Text = Properties.Settings.Default.strT1L4;
txtT1S5.Text = Properties.Settings.Default.strT1L5;
txtT1S6.Text = Properties.Settings.Default.strT1L6;
txtT1S7.Text = Properties.Settings.Default.strT1L7;
txtT1S8.Text = Properties.Settings.Default.strT1L8;
txtT1S9.Text = Properties.Settings.Default.strT1L9;
txtT1S10.Text = Properties.Settings.Default.strT1L10;
}
Obvious i can see the logic that each stored propertie ending with T1L1 also fits to the txt that ends with T1S1.
I just know this should be done in a more elegant and solid way than what i did now.
Could anyone push me in the right direction?
you can bind your properties directly to your textboxes
<UserControl xmlns:Properties="clr-namespace:MyProjectNamespace.Properties" >
<TextBox Text="{Binding Source={x:Static Properties:Settings.Default}, Path=strT1L1, Mode=TwoWay}" />
If you can get all of those constants into a List<string>, you could use it to bind to an ItemsControl with TextBlock inside:
Code behind or View Model
private ObservableCollection<string> _defaultProperties = new ObservableCollection<string>();
public ObservableCollection<string> DefaultProperties
{
get { return _defaultProperties; }
}
XAML
<ListBox ItemsSource="{Binding Path=DefaultProperties"}>
<ListBox.ItemTemplate>
<DataTemplate>
<!--Just saying "Binding" allows binding directly to the current data context vs. a property on the data context-->
<TextBlock Text="{Binding}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>