WPF just add new items to Collection that is binded to ItemsControl - c#

In ItemsControl I have placed my Collection of Items. As this collection can be very huge, I try to implement some paging - When scroll to the end of visible scroll area, add new items to ItemsControl.
Detecting of scroll bottom I've used from this answers How to find that ScrollViewer is scrolled to the end in WPF?
But I need to implement Adding of items.
if (scrollViewer.VerticalOffset == scrollViewer.ScrollableHeight &&
_productsViewModel.ProductTotal > pageCount*ConfigurationProvider.ItemsPerProductPage)
{
pageCount++;
_productsViewModel.RunPaginationWorker(pageCount);
}
Items are added in BG Worker. As parameter I send pageNumber which is amount of times when user scroll to the end of scroll.
In BG CompleteWork event, I receive items and try to bind them to Property which is binded to ItemsControl
var items= (e.Result) as List<ItemDto>;
Items.AddRange(items);
OnPropertyChanged("Items");
But this doesn't work, it seems like Property Items isn't changed.
public List<ItemDto> Items
{
get { return _items; }
set
{
_items= value;
OnPropertyChanged("Items");
}
}
And Xaml Binding
<ItemsControl VerticalAlignment="Top" MaxWidth="650" HorizontalAlignment="Center" Margin="10,40,10,0" Name="Items"
ItemsSource="{Binding Items}" AlternationCount="{Binding Path=Items.Count,FallbackValue='100'}">
...
</ItemsControl>

You should use ObservableCollection<T> instead of List<T> as it implements the interfaces:
INotifyCollectionChanged
INotifyPropertyChanged
As such it is very useful when you want to know when the collection has changed. An event is triggered that will tell the user what entries have been added/removed or moved.
If you want to have method AddRange(), then just copy this code:
private ObservableCollection<ItemDto> _items=new ObservableCollection<ItemDto>();
public ObservableCollection<ItemDto> Items
{
get { return _items; }
set
{
_items= value;
OnPropertyChanged("Items");
}
}
public void AddRange(IEnumerable<T> collection)
{
foreach (var i in collection)
{
Items.Add(i);
}
}
Update:
If you want just take the last items, I would suggest you linq methods. For example, you can skip 10 items and take the next 10 items:
Items=Items.Skip(10).Take(10).ToList();
Update 1:
When you adding new items to collection and you do not want draw new items, then you should just add VirtualizingStackPanel.VirtualizationMode="Recycling". This will reduce the numbers of times to re-render new controls.

Related

Update ObservableCollection without blinking

Could someone help me, how can I update ObservableCollection, which is binded to ListView ItemSource, without blinking? When I do:
Contacs = _contacs;
the whole ListView is blinking. I would like to search in ListView too, but always after replacing the old results with new one, the listview blinks.
The problem here is, that you are reassigning the whole collection. This does not take advantage of the observability and forces the ListView to reload all items. Try to remove/add the items instead so the ListView only needs to update the Items that actually changed.
In the case of searching hiding the unmatched results might be a viable solution too. To do that create a property on your Contact type (called "IsVisible" for example) and bind it to the ListViewItems Visibility Property. (You might need the build in BooleanToVisibility Converter here)
Update
As pointed out in the comments using a CollectionViewSource is the correct wpf way of implementing a search filter. See this answer for details on how to use it.
If you want to enable filtering in your collection then there is no actual need to perform operations directly on your collection.
Use ICollectionView and CollectionViewSource for this purpose.
As you have an ObservableCollection so you can do something like this.
ICollectionView contactsView;
public ICollectionView ContactsView
{
get { return contactsView; }
set
{
if(contactsView != value)
{
contactsView = value;
}
}
}
And in the setter of the ObservableCollection
public ObservableCollection<ContactType> Contacs
{
get { return _contacs; }
set
{
if(_contacs != value)
{
_contacs = value;
ContactsView = CollectionViewSource.GetDefaultView(value);
ContactsView.Filter = ContactsFilter;
}
}
}
where ContactsFilter is a function with following definition.
bool ContactsFilter(object item)
{
var contact = item as ContactType;
if(condition)
return true; //show this item in ListView.
return false; //Do not show this item in ListView
}
and whenever you want to filter items you can do that just by
ContactsView.Refresh();
which I think will be in the TextChanged Event of your text box in which you are entering search query.
More detailed article is at CollectionViewSource

Windows 10 UWP - GridView Items With no DataContext when Not Visible

I have an issue with a GridView in a UWP application that I'm working on...
Items in the GridView load correctly, however items that are out of view (off the page and not visible) do not have a DataContext assigned, and no event ever fires when the DataContext is assigned. Various bindings do work as TextBlocks that are bound get updated, but the the normal event workflow and Loaded events get all strange.
<GridView Grid.Row="1" Name="SearchGrid" ItemsSource="{Binding SearchItems}" ItemClick="SearchGrid_ItemClick">
<GridView.ItemTemplate>
<DataTemplate>
<local:RsrItemGridViewItem />
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
The grids all show correctly, except, for being able to properly delay load some items because the DataContext isn't set at time of load (and a DataContextChanged event isn't fired when the context is updated).
Does anyone have any ideas how to get notified when the control becomes visible? This seems like a notification bug, or there is some binding thing that I'm missing.
Thank you!
Does anyone have any ideas how to get notified when the control becomes visible?
You can't use FrameworkElement.Loaded event here to get notify when your RsrItemGridViewItem becomes visible, this event occurs when a FrameworkElement has been constructed and added to the object tree, and is ready for interaction.
GirdView control implements UI virtualization for better UI performance, if your GridView is bound to a collection of many items, it might download only items 1-50, When the user scrolls near the end of the list, then items 51 – 100 are downloaded and so on. But for example, there are only 20 items now be shown, but it might have loaded 45 items, 25 items could not be seen in this moment.
If you change the default ItemsPanel of GridView which is ItemsWrapGrid to for example VariableSizedWrapGrid, GridView will lose virtualization, and all items will be loaded at the same time even most of them can not be seen at one moment.
For you problem, I think what you can give a try is calculating the ScrollViewer's VerticalOffset with your GridView's height and the items's count be shown, and then you can know which items are been shown at this moment.
For example here:
private ObservableCollection<MyList> list = new ObservableCollection<MyList>();
public MainPage()
{
this.InitializeComponent();
this.Loaded += MainPage_Loaded;
}
private double viewheight;
private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
var scrollViewer = FindChildOfType<ScrollViewer>(gridView);
scrollViewer.ViewChanged += ScrollViewer_ViewChanged;
viewheight = gridView.ActualHeight;
}
private void ScrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
var scrollViewer = sender as ScrollViewer;
var Y = scrollViewer.VerticalOffset;
//calculate here to get the displayed items.
}
public static T FindChildOfType<T>(DependencyObject root) where T : class
{
var queue = new Queue<DependencyObject>();
queue.Enqueue(root);
while (queue.Count > 0)
{
DependencyObject current = queue.Dequeue();
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(current); i++)
{
var child = VisualTreeHelper.GetChild(current, i);
var typedChild = child as T;
if (typedChild != null)
{
return typedChild;
}
queue.Enqueue(child);
}
}
return null;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
list.Clear();
for (int i = 0; i < 200; i++)
{
list.Add(new MyList { text = "Item " + i });
}
}
Since GridView control's layout is adaptive to the app's size, the current displayed count is dynamic, you can try other height based properties (for example each item's height) and the ScrollViewer's VerticalOffset to calculate, there is no ready-made method to get your work done, it's a little complex to calculate, but I think there is no better solution for now.
After doing some testing with this, what I found out worked (though it's not very clean, and I believe there is a bug with bindings) was to add the custom control to the GridView, then in the grid view adding a DataContext={Binding} to the Image I wanted to get notified of an update on.
<UserControl ...><Image DataContext="{Binding}" DataContextChanged="ItemImage_DataContextChanged" /></UserControl>
The main control doesn't get notified of a DataContext change, but the child elements are notified.

WPF ListView: Changing ItemsSource does not change ListView

I am using a ListView control to display some lines of data. There is a background task which receives external updates to the content of the list. The newly received data may contain less, more or the same number of items and also the items itself may have changed.
The ListView.ItemsSource is bound to an OberservableCollection (_itemList) so that changes to _itemList should be visible also in the ListView.
_itemList = new ObservableCollection<PmemCombItem>();
_itemList.CollectionChanged += new NotifyCollectionChangedEventHandler(OnCollectionChanged);
L_PmemCombList.ItemsSource = _itemList;
In order to avoid refreshing the complete ListView I do a simple comparison of the newly retrieved list with the current _itemList, change items which are not the same and add/remove items if necessary. The collection "newList" contains newly created objects, so replacing an item in _itemList is correctly sending a "Refresh" notification (which I can log by using the event handler OnCollectionChanged of the ObservableCollection`)
Action action = () =>
{
for (int i = 0; i < newList.Count; i++)
{
// item exists in old list -> replace if changed
if (i < _itemList.Count)
{
if (!_itemList[i].SameDataAs(newList[i]))
_itemList[i] = newList[i];
}
// new list contains more items -> add items
else
_itemList.Add(newList[i]);
}
// new list contains less items -> remove items
for (int i = _itemList.Count - 1; i >= newList.Count; i--)
_itemList.RemoveAt(i);
};
Dispatcher.BeginInvoke(DispatcherPriority.Background, action);
My problem is that if many items are changed in this loop, the ListView is NOT refreshing and the data on screen stay as they are...and this I don't understand.
Even a simpler version like this (exchanging ALL elements)
List<PmemCombItem> newList = new List<PmemCombItem>();
foreach (PmemViewItem comb in combList)
newList.Add(new PmemCombItem(comb));
if (_itemList.Count == newList.Count)
for (int i = 0; i < newList.Count; i++)
_itemList[i] = newList[i];
else
{
_itemList.Clear();
foreach (PmemCombItem item in newList)
_itemList.Add(item);
}
is not working properly
Any clue on this?
UPDATE
If I call the following code manually after updating all elements, everything works fine
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
But of course this causes the UI to update everything which I still want to avoid.
After a change, you can use the following to refresh the Listview, it's more easy
listView.Items.Refresh();
This is what I had to do to get it to work.
MyListView.ItemsSource = null;
MyListView.ItemsSource = MyDataSource;
I know that's an old question, but I just stumbled upon this issue. I didn't really want to use the null assignation trick or the refresh for just a field that was updated.
So, after looking at MSDN, I found this article:
https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.inotifypropertychanged?redirectedfrom=MSDN&view=netframework-4.7.2
To summarize, you just need the item to implement this interface and it will automatically detect that this object can be observed.
public class MyItem : INotifyPropertyChanged
{
private string status;
public string Status
{
get => status;
set
{
OnPropertyChanged(nameof(Status));
status = value;
}
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
So, the event will be called everytime someone changes the Status. And, in your case, the listview will add a handler automatically on the PropertyChanged event.
This doesn't really handle the issue in your case (add/remove).
But for that, I would suggest that you have a look at BindingList<T>
https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.bindinglist-1?view=netframework-4.7.2
Using the same pattern, your listview will be updated properly without using any tricks.
You should not reset ItemsSource of ListView each time observable collection changed. Just set proper binding that will do your trick. In xaml:
<ListView ItemsSource='{Binding ItemsCollection}'
...
</ListView>
And in code-behind (suggest to use MVVM) property that will be responsible for holding _itemList:
public ObservableCollection<PmemCombItem> ItemsCollection
{
get
{
if (_itemList == null)
{
_itemList = new ObservableCollection<PmemCombItem>();
}
return _itemList;
}
}
UPDATE:
There is similar post which most probably will Answer your question: How do I update an ObservableCollection via a worker thread?
I found a way to do it. It is not really that great but it works.
YourList.ItemsSource = null;
// Update the List containing your elements (lets call it x)
YourList.ItemsSource = x;
this should refresh your ListView (it works for my UAP :) )
An alternative on Xopher's answer.
MyListView.ItemsSource = MyDataSource.ToList();
This refreshes the Listview because it's a other list.
Please check this answer:
Passing ListView Items to Commands using Prism Library
List view Items needs to notify about changes (done is setter)
public ObservableCollection<Model.Step> Steps
{
get { return _steps; }
set { SetProperty(ref _steps, value); }
}
and UpdateSourceTrigger need to be set in xaml
<Image Source="{Binding ImageData, UpdateSourceTrigger=PropertyChanged}" />

Windows Store App: How to make ListView with expandable/enlargeable ListItems?

I have a Listview with items, in a C# Windows Store App (is that what you call these? I heard they're not called Metro Apps anymore).
Similar to the ExpandableListView in Android, I want to be able to tap on listitems (not the buttons) for that listitem to expand, tap on the expanded listitem for it to collapse, and if you tap on another listitem, the currently expanded listitem will collapse and the other will expand.
In my particular case I have a DataTemplate for both the expanded and non-expanded view of the listitems. I've seen that Android's ExpandableListView can expand the listitem with additional information (the Expander from WPF does something similar to that), instead of replacing it with a larger item, but is there a common solution for this in Windows Store Apps?
If not, what is the closest equivalent?
Like on the following drawing, I want to know if there is a component that can expand listitems in this way, or if not, which alternatives I have:
I ended up with a solution that works but doesn't look too fancy. It switches DataTemplate when you click items but there's no animation: it switches instantly.
Here's the important code parts:
XAML
<Page.Resources>
<DataTemplate x:Key="dtSmall">
<!--Component template for the un-expanded listitems-->
</DataTemplate>
<DataTemplate x:Key="dtEnlarged">
<!--Component template for the expanded listitems-->
</DataTemplate>
</Page.Resources>
<Grid>
<ListView x:Name="lvEnlargeable"
IsItemClickEnabled="True"
ItemTemplate="{StaticResource dtSmall}"
ItemsSource="{Binding ...}"
SelectionChanged="LVEnlargeable_SelectionChanged"
ItemClick="LVEnlargeable_ItemClick"/>
</Grid>
XAML.CS
public sealed partial class MainPage : Page
{
private DataTemplate dtSmall;
private DataTemplate dtEnlarged;
public MainPage()
{
this.InitializeComponent();
dtSmall = (DataTemplate)Resources["dtSmall"];
dtEnlarged = (DataTemplate)Resources["dtEnlarged"];
}
// A selected item is treated as an expanded/enlarged item
private void LVEnlargeable_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
/* First we set all the items that has been deselected
to be collapsed, aka. using the dtSmall DataTemplate.
We expect 0 or 1 item to have been deselected
but handle all cases easily with a foreach loop.
*/
foreach (var item in e.RemovedItems)
{
// Set the DataTemplate of the deselected ListViewItems
((ListViewItem)(sender as ListView).ContainerFromItem(item)).ContentTemplate = dtSmall;
}
/* Then we set all the items that has been selected
to be expanded.
We should probably throw an Exception if more than 1 was found,
because it's unwanted behavior, but we'll ignore that for now.
*/
foreach (var item in e.AddedItems)
{
((ListViewItem)(sender as ListView).ContainerFromItem(e.AddedItems[0])).ContentTemplate = dtEnlarged;
}
}
/* We need click events because SelectionChanged-events
cannot detect clicks on an already selected item */
private void LVEnlargeable_ItemClick(object sender, ItemClickEventArgs e)
{
ListView lv = (sender as ListView);
/* Having set the IsItemClickEnabled property on the ListView to True
we have to handle selection events manually.
If nothing is selected when this click occurs, then select this item*/
if (lv.SelectedItem == null)
{
lv.SelectedItem = e.ClickedItem;
}
else
{
// Clicking on an expanded/selected/enlarged item will deselect it
if (lv.SelectedItem.Equals(e.ClickedItem))
{
lv.SelectedItem = null;
}
else
{ /* If it's not a selected item, then select it
(and let SelectionChanged unselect the already selected item) */
lv.SelectedItem = e.ClickedItem;
}
}
}
}
I haven't tested if this isolated code is enough, on its own, for this solution, but I hope it is, and this code at least contain the key points. It's late and I just wanted to post something for the curious-minded people. If this shows not to work for you, then please leave a comment about the issue and I'll make sure to add the missing parts.
I also messed with the ListViewItemStyleContainer's ListViewItemPresenter to have better selection effects etc. but I figure it's best to keep it short. If you find this interesting as well, then feel free to leave a comment for that too, and I'll try include it.

MVVM Dynamic DataGrid Sorting Filtering

I have a DataGrid that gets its data updated every few seconds via a Thread. The DataGrid needs to offer Column Header sorting, grouping and filtering.
I currently have a DataGrid bound to a ICollectionView and the source of the ICollectionView is an ObservableCollection. Which seems to be the good way to do it from what I read on other threads.
The sort-ing "works" but it gets lost when the ICollectionView.Source gets updated following an update of the ObservableCollection. I have tried saving the SortDescriptions before the update and re-add it to the ICollectionView after the update is done. But it's the same result.
May someone point me to what I'm missing?
Edit Here's some code...
View (XAML)
<DataGrid ItemsSource="{Binding CollectionView, Source={StaticResource ViewModel}}>
ViewModel
public ICollectionView CollectionView
{
get
{
collectionViewSource.Source = dataColl;
if (SortDescriptions != null)
{
foreach (SortDescription sd in SortDescriptions)
{
collectionViewSource.View.SortDescriptions.Add(sd);
}
}
collectionViewSource.View.Refresh();
return collectionViewSource.View;
}
}
public ObservableCollection<SomeObject> DataColl
{
get { return dataColl; }
private set
{
this.dataColl= value;
OnPropertyChanged("CollectionView");
}
}
Following is the method that updates the data every few seconds...
private void UpdateData()
{
while (true)
{
System.Threading.Thread.Sleep(mDataRefreshRate);
// SortDescriptions is a Property of the ViewModel class.
SortDescriptions = collectionViewSource.View.SortDescriptions;
ObservableCollection<SomeObject> wDataColl
= new ObservableCollection<SomeObject>();
//... Irrelevant code that puts the data in wDataColl ...
DataColl= wDataColl;
}
}
[YourObservableCollection].ViewHandler.View.Filter
+= new FilterEventHandler(myFilterHandler);
private void myFilterHandler(object sender, FilterEventArgs e)
{
}
Can be used to directly add your filter handler and you can do the same with SortDescriptions to Add/Remove
[YourObservableCollection].ViewHandler.View.SortDescriptions.Add(mySortDescription);
If you are doing allot of sorting and filtering best to create own class encapsulating a CollectionViewSource and implement adding and removing SortDescriptions and Filtering etc
When you say:
The sort "works" but it gets lost when the ICollectionView.Source gets
updated following an update of the ObservableCollection
What do you mean by update? you mean you are changing the Source? Rather than adding/removing items from the collection?
EDIT based on your XAML example you added:
<DataGrid ItemsSource="{Binding CollectionView, Source={StaticResource ViewModel}}>
You are binding itemsource to the CollectionViewSource where you should bind the datacontext to it:
Example:
<Page.Resources>
<CollectionViewSource x:Key="myViewSource"
Source="{Binding CollectionView, Source={StaticResource ViewModel}}"
/>
</Page.Resources>
In page:
<Grid DataContext="{StaticResource myViewSource}">
<DataGrid x:Name="myGrid" ItemsSource="{Binding}"...
Or something along those lines
EDIT again didnt see code scroll down :p
ObservableCollection<SomeObject> wDataColl= new ObservableCollection<SomeObject>();
You create new instance every time of collection lol this is main problem
Also:
public ICollectionView CollectionView
{
get
{
collectionViewSource.Source = dataColl;
if (SortDescriptions != null)
{
foreach (SortDescription sd in SortDescriptions)
{
collectionViewSource.View.SortDescriptions.Add(sd);
}
}
collectionViewSource.View.Refresh();
return collectionViewSource.View;
}
}
Here where you return collection you are setting the Source and adding the SortDescriptions and also refreshing the view every time, you only need set these values once
You would only call refresh on the View if you add/remove SortDescriptions
I think you should get to grips with basics of CollectionViewSource
The problem is that you swap out the entire ObservableCollection every time you add new data.
ObservableCollection<SomeObject> wDataColl= new ObservableCollection<SomeObject>();
... Unrelevant code that puts the data in wDataColl ...
DataColl= wDataColl;
Make sure to use Add to the existing collection instead (perhaps after using Clear() first if that is necessary)... If you still have problems after that please comment and i will try to help.
Also, try to avoid using the Refresh() as it rebuilds the entire view and is unnecessarily expensive. If you do sorting, adding, removing etc. the correct way use of Refresh() isn't needed.

Categories

Resources