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
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 datagrid used to display the log information which read from a data server.
As the log number is huge, I don't want to load data at once when page initialize. So below is my design:
Load the first 20 lines in initialize. When user drag the scrollbar, I will calculate the distance which the slider moved, get the index of log and then retrieve new log from server. The old log will be cleared to make sure the datagrid's memory consumption is not large.
But during implement, I met following problem
1. How to set the size of slider? If I only add 20 lines to datagrid, the slider is very long
2. During I drag the slider(not complete), the content of datagride will also change automatically. How should I disable it.
Below is my code:
<DataGrid x:Name="LogsGrid"
Margin="0,0,0,0"
Height="457"
HeadersVisibility="Column"
ItemsSource="{Binding LogsList}"
SelectedItem="{Binding SelectedRow, Mode=TwoWay}"
ScrollViewer.CanContentScroll="True">
<i:Interaction.Triggers>
<local:RoutedEventTrigger RoutedEvent="ScrollViewer.ScrollChanged">
<local:CustomCommandAction Command="{Binding ScrollCommand}" />
</local:RoutedEventTrigger>
</i:Interaction.Triggers>
<DataGrid.Columns>
...
</DataGrid.Columns>
Blew is my viewmodel:
public LogsViewModel()
{
InitializeCommand();
scrollTimer = new DispatcherTimer();
scrollTimer.Interval = new TimeSpan(0,0,0,0,250);
scrollTimer.Tick += new EventHandler(ScrollTimer_Elapsed);
}
private void ScrollTimer_Elapsed(object sender, EventArgs e)
{
scrollTimer.Stop();
GetNewLog(nCurrentScrollIndex);
}
private int nCurrentScrollIndex = 0;
private void OnScroll(object param)
{
ScrollChangedEventArgs args = param as ScrollChangedEventArgs;
if (args.VerticalOffset == 0)
{
return;
}
int nLogTotalNumber = int.Parse(LogTotalNumber);
nCurrentScrollIndex = (int)(args.VerticalOffset/args.ExtentHeight * nLogTotalNumber);
if (scrollTimer!= null || scrollTimer.IsEnabled)
{
scrollTimer.Stop();
}
scrollTimer.Start();
}
I don't think it's necessary going into the nuts-n-bolts of WPF like this. DataGrid supports virtualization, so unless you've done something to break it it should only fetch the items actually visible on-screen. That said, I've never been able to get DataGrid to properly fetch items asynchronously without locking up the GUI thread, so you'll probably have to handle this yourself in the view model.
Here's an example that easily manages 10 million records on my i7, I'll start by templating the DataGrid:
<DataGrid ItemsSource="{Binding Items}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Text">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
Here's the main view model, which creates an array of items and fetching each one only when requested:
public class MainViewModel : ViewModelBase
{
public Item[] Items { get; }
public MainViewModel()
{
this.Items = Enumerable
.Range(1, 1000)
.Select(i => new Item { Value = i })
.ToArray();
}
}
public class Item : ViewModelBase
{
public int Value { get; set; }
private string _Text;
public string Text
{
get
{
// if already loaded then return what we got
if (!String.IsNullOrEmpty(this._Text))
return this._Text;
// otherwise load the value in a task (this will raise property changed event)
Task.Run(async () => {
Debug.WriteLine($"Fetching value # {Value}");
await Task.Delay(1000); // simulate delay in fetching the value
this.Text = $"Item # {Value}";
});
// return default string for now
return "Loading...";
}
set
{
if (this._Text != value)
{
this._Text = value;
RaisePropertyChanged(() => this.Text);
}
}
}
}
This is a very basic example of course, shown only to demonstrate the point. In the real world you would want to set up some kind of cache, and fetch blocks of records at a time. You'd probably also want some kind of prioritized pre-meditated queuing that anticipates records likely to be fetched in future. Either way, WPF doesn't support data virtualization, so I think you're going to have to wind up doing something like this to get the exact behavior you're after.
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 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.
I'm using Caliburn Micro on a Windows Store App.
I have a ListView which has a SelectedItem that works the first time I use it. However, when I clear the ListView and re-bind it to another Collection, the selected item no longer appears selected.
The selectedItem property is being set correctly, since I can hit the breakpoint, and everything works has expected, just the View is not being updated with the selected item, after I clear the collection.
What could be wrong?
Thanks.
Edit:
View Code:
<ListView x:Name="DetailNotes"
ItemsSource="{Binding DetailNotes}"
SelectedItem="{Binding SelectedDetailNote}"
ItemTemplate="{StaticResource Notes600ItemTemplate}"
IsItemClickEnabled="True"
caliburn:Message.Attach="[Event ItemClick] = [DetailNoteSelected($eventArgs)]"/>
ViewModel Code:
(...)
private Note selectedDetailNote;
public Note SelectedDetailNote
{
get { return this.selectedDetailNote; }
set
{
this.selectedDetailNote = value;
this.NotifyOfPropertyChange(() => this.SelectedDetailNote);
}
}
(...)
public void DetailNoteSelected(ItemClickEventArgs eventArgs)
{
Note n = (Note)eventArgs.ClickedItem;
this.SelectedDetailNote = n;
}
Sorry! The problem was my explicit binding. I just left Caliburn do his work, and now it works!
Solution below:
View Code:
<ListView x:Name="DetailNotes"
ItemTemplate="{StaticResource Notes600ItemTemplate}"/>
ViewModel Code:
private Note selectedDetailNote;
public Note SelectedDetailNote
{
get { return this.selectedDetailNote; }
set
{
this.selectedDetailNote = value;
this.NotifyOfPropertyChange(() => this.SelectedDetailNote);
}
}
I know it's late, but your problem was binding mode. You should set it to TwoWay:
SelectedItem="{Binding SelectedDetailNote, Mode=TwoWay}"
In WinRT XAML default is OneWay.