update an ObservableCollection with a BlockingCollection - c#

I subscribe to a service that will raise an Event when a new element is received, I add this element to a BlockingCollection.
I have a second thread running that will loop the BlockingCollection to add/update the element in an observable collection.
The problem is how do you do add in the ObservableCollection?
I know I can't just do an .add on this type of collection, as it needs to be done on the UI thread. So I tried using different ObservableCollection subclass that use the dispatcher to marshal the adding of element, but every single time, I end up with the same error
"An unhandled exception of type 'System.StackOverflowException'
occurred in Unknown Module."
with a troubleshooting tips as:
make sure you don't have an infinite loop or inifiniterecursion.
Well the thing is, I do have some kind of infinite loop actually, as the BlockingQueue is receiving new elements all the time, like 5 to 10 per sec.
I won't get the exception if I don't bind a control to the observablecollection though, or if I use a List instead.
Class ElementHolder
{
private ExternalService _externalService;
private ObservableCollection<Element> _elementsList = new ObservableCollection<Element>();
private BlockingCollection<Element> _elementsReceived = new BlockingCollection<Element>();
public ObservableCollection<Element> ElementsList
{
get
{
return _elementList;
}
set
{
_elementList = value;
}
}
public ElementHolder()
{
Task.Factory.StartNew(ReadElements);
_externalService = new ExternalService();
_externalService.ReceivedNewElement += new Action<Element>(o => _elementsReceived.Add(o));
_externalService.Subscribe();
}
private void ReadElements()
{
foreach (Element element in _elementsReceived.GetConsumingEnumerable())
{
Element item = _elementsList.FirstOrDefault(o => o.ID == element.ID);
if (item == null)
{
_elementList.Add(element);
}
else
{
item.Update(element);
}
}
}
EDIT
The bug disappeared by itself, when i was tracking it down. I was trying to make things simpler to really understand where the issue was, and then it started to work. When putting things back together it still works... BUT it comes back time to time, for what seems unrelated reason like adding a style to my listview. I'm starting to think there's an issue in the third party dll.

This is a perfect example of where the Reactive Extensions are a very useful and ingenious tool. There is a pretty steep learning curve to using them, but since you have a specific case here, I will insert the reactive code that will achieve your goal, assuming I understand your goal correctly.
Note that you will need to install the Reactive Extensions, and you will need two using statements (System.Reactive.Linq and System.Reactive.Subjects) and you will need to reference System.Reactive and System.Reactive.Windows.Threading dlls. See Reactive on MSDN.
class ElementHolder
{
public ObservableCollection<Element> ElementsList { get; set; }
private ExternalService _externalService = new ExternalService();
private IDisposable _elementSubscription;
private Subject<Element> _elementSubject = new Subject<Element>();
public ElementHolder()
{
_externalService.ReceivedNewElement += _elementSubject.OnNext;
_externalService.Subscribe();
ElementList = new ObservableCollection<Element>();
_elementSubscription = _externalService.ObserveOnDispatcher().Subscribe(NextElement);
}
private void NextElement(Element e)
{
Element item = ElementsList.FirstOrDefault(o => o.ID == element.ID);
if (item == null) {
_elementList.Add(element);
}
else {
item.Update(element);
}
}
}

There's a small error in the answer. See the corrected line below:
_elementSubscription = _elementSubject.ObserveOnDispatcher().Subscribe(NextElement);
It took me a while to figure this out, but rmayer06's answer solved my problem. I can't comment on answers or I would have.

Related

How to build a custom loop activity in Elsa workflows

I'm trying to make a custom activity that will eventually do a complicated database query or API call to get a bunch of records and loop over them. I'm sure it could be done with the built in flow control activities, but I want to make this usable by non-programmers who don't know or care what a foreach loop is, so putting a lot of functionality into one box is good.
My first attempt was to inherit from ForEach and do some initialization before letting OnExecute do its thing, but the result feels somewhat hacky.
public class FancyForEach : ForEach
{
private bool? Initialized
{
get
{
return GetState<bool?>("Initialized");
}
set
{
SetState(value, "Initialized");
}
}
protected override IActivityExecutionResult OnExecute(ActivityExecutionContext context)
{
if (Initialized != true)
{
Items = GetThingsFromDatabase();
Initialized = true;
}
return base.OnExecute(context);
}
protected List<DatabaseThings> GetThingsFromDatabase()
{
// Fancy stuff here, including paging eventually.
}
}
It seems like it would be a little cleaner to instantiate a ForEach somewhere within the activity rather than inherit from it, but I can't puzzle out a way to make that work. I imagine a decent solution would be to trigger another workflow for each record, but I'd rather not do that, again to make this easy to digest for people who aren't programmers.
Can anyone offer a suggestion on the best way to make this work? This is my first project using Elsa, so maybe I'm approaching it from an entirely wrong direction!
If I understand correctly, your activity is responsible for loading in the data and looping over it, while the user of the activity should be able to specify what happens in each iteration.
If so, then you might implement something like this:
[Activity(
Category = "Control Flow",
Description = "Iterate over a collection.",
Outcomes = new[] { OutcomeNames.Iterate, OutcomeNames.Done }
)]
public class FancyForEach : Activity
{
private bool? Initialized
{
get => GetState<bool?>();
set => SetState(value);
}
private IList<DatabaseThings>? Items
{
get => GetState<IList<DatabaseThings>?>();
set => SetState(value);
}
private int? CurrentIndex
{
get => GetState<int?>();
set => SetState(value);
}
protected override IActivityExecutionResult OnExecute(ActivityExecutionContext context)
{
if (Initialized != true)
{
Items = GetThingsFromDatabase();
Initialized = true;
}
var collection = Items.ToList();
var currentIndex = CurrentIndex ?? 0;
if (currentIndex < collection.Count)
{
var currentValue = collection[currentIndex];
var scope = context.CreateScope();
scope.Variables.Set("CurrentIndex", currentIndex);
scope.Variables.Set("CurrentValue", currentValue);
CurrentIndex = currentIndex + 1;
context.JournalData.Add("Current Index", currentIndex);
// For each iteration, return an outcome to which the user can connect activities to.
return Outcome(OutcomeNames.Iterate, currentValue);
}
CurrentIndex = null;
return Done();
}
protected List<DatabaseThings> GetThingsFromDatabase()
{
// Fancy stuff here, including paging eventually.
}
}
This example loads the database items into memory once and then stores this list in workflow state (via Items) - which may or may not be desirable, since this has the potential of increasing the size of the workflow instance significantly depending on the size of each record and number of records.
A more scalable approach would be to load just one item per iteration, keeping track of the current index that was loaded, incrementing it (i.e. pagination with a page size of 1).

Cannot access List from a class within an Array

I am a beginner developer, and would very much appreciate if you can help me figure out the problem which is in my code. The code is particularly confusing, mainly because it derives from a framework. The comments should be able to somewhat allow us to understand.
// Create an IBindable List
public static List<IBindable> KyzerBindables = new List<IBindable>();
// Attach elements to a list, for better control over all of them
internal static void AttachBindablesToList(IReadOnlyList<Drawable> children)
{
// For all the children classes located in Drawable list
for (int i = 0; i < children.Count; i++) // children.Count returns 4
{
// For all of the SettingsSubsection which are present in the Drawable array
for (int l = 0; l < (children[i] as SettingsSubsection).Children.Count; l++) // (children[i] as Subsection).Children.Count returns 0.
{
// Get a specific element
var element = (children[i] as SettingsSubsection).Children[l];
// if is a SettingsCheckbox
if (element.GetType() == typeof(SettingsCheckbox))
KyzerBindables.Add((element as SettingsCheckbox).Bindable);
}
}
}
// in another class
public class KyzerSection: SettingsSection
{
public KyzerSection()
{
Children = new Drawable[]
{
new KyzerMiscellaneous(),
};
...AttachElementsToList(Children);
}
}
public class KyzerMiscellaneous: SettingsSubsection
{
[BackgroundDependencyLoader] // Calls load, framework thing.
private void load(OsuConfigManager config)
{
Children = new Drawable[]
{
new SettingsCheckbox
{
LabelText = "Something here",
Bindable = new BindableBool(false),
}
};
}
}
My problem is, the second for loop does not even initiate for the AttachBindablesToList. For whatever particular reason, it isn't recieving a count. I am uncertain of what I am doing wrong.
Edit:
If, in any way, the GitHub repository issue can clear some issues up, please feel free to navigate there and check the commit which contains these changes. https://github.com/Frontear/osuKyzer/issues/3
After reviewing your github repository, I believe the issue is caused at:
private void load(params here)
The above is not being called at the time of AttachBindablesToList. This results in an empty
(children[i] as SettingsSubsection).Children.Count
The best option is to create an empty instantiation method
public KyzerMiscellaneous() { /* create Drawable elements */ }
// then
[BackgroundDependancyLoader]
private void load(params here) { /* doSomething */ }
This will allow access to the children list since it has been initialized before, which therefore allows the second loop to correctly function, and pushes IBindables to your list.

Find out which winforms controls are accessed from a background thread

We have built a huge winforms project, already in progress for multiple years.
Sometimes, our users get an exception which looks like this one.
The resolution of this problem seems to be:
don't acces UI components from a background thread
.
But since our project is a very big project with a lot of different threads, we don't succeed in finding all these.
Is there a way to check (with some tool or debugging option) which components are called from a background thread?
To clarify:
I created a sample winforms project with a single Form, containing two Button
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
button1.Text = "Clicked!";
}
private void button2_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
button2.BackColor = Color.Red; //this does not throw an exception
//button2.Text = "Clicked"; //this throws an exception when uncommented
});
}
}
The background color of button2 is set to red when the button is clicked. This happens in a background thread (which is considered bad behavior). However, it doesn't (immediately) throw an exception. I would like a way to detect this as 'bad behavior'. Preferably by scanning my code, but if it's only possible by debugging, (so pausing as soon as a UI component is accessed from a background thread) it's also fine.
I've got 2 recommendations to use together, the first is a Visual Studio Plugin called DebugSingleThread.
You can freeze all the threads and work on one at a time (obviously the non-main-UI threads) and see each threads access to controls. Tedious I know but not so bad with the second method.
The second method is to get the steps in order to reproduce the problem. If you know the steps to reproduce it, it will be easier to see whats causing it. To do this I made this User Action Log project on Github.
It will record every action a user makes, you can read about it here on SO: User Activity Logging, Telemetry (and Variables in Global Exception Handlers).
I'd recommend you also log the Thread ID, then when you have been able to reproduce the problem, go to the end of the log and work out the exact steps. Its not as painful as it seems and its great for getting application telemetry.
You might be able to customise this project, eg trap a DataSource_Completed event or add a dummy DataSource property that sets the real Grids DataSource property and raises an INotifyPropertyChanged event - and if its a non-main thread ID then Debugger.Break();.
My gut feeling is you're changing a control's (eg a grid) data source in a background thread (for that non-freeze feel) and thats causing a problem with synchronisation. This is what happened to the other DevExpress customer who experienced this. Its discussed here in a different thread to the one you referenced.
Is your app set to ignore cross threading intentionally?
Cross-thread operations should be blowing up all the time in winforms. It checks for them like crazy in just about every method. for a starting point check out https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/Control.cs.
Somewhere in your app, somebody might have put this line of code:
Control.CheckForIllegalCrossThreadCalls = False;
Comment that out and run the app, then follow the exceptions.
(Usually you can fix the problem by wrapping the update in an invoke, e.g., in a worker thread if you see textbox1.text=SomeString; change it to `textbox.invoke(()=>{textbox1.text=SomeString;});.
You may also have to add checking for InvokeRequired, use BeginInvoke to avoid deadlocks, and return values from invoke, those are all separate topics.
this is assuming even a moderate refactor is out of the question which for even a medium sized enterprise app is almost always the case.
Note: it's not possible to guarantee successful discovery of this case thru static analysis (that is, without running the app). unless you can solve the halting problem ... https://cs.stackexchange.com/questions/63403/is-the-halting-problem-decidable-for-pure-programs-on-an-ideal-computer etc...
I did this to search for that specific situation but of course, need to adjust it to your needs, but the purpose of this is to give you at least a possibility.
I called this method SearchForThreads but since it's just an example, you can call it whatever you want.
The main idea here is perhaps adding this Method call to a base class and call it on the constructor, makes it somewhat more flexible.
Then use reflection to invoke this method on all classes deriving from this base, and throw an exception or something if it finds this situation in any class.
There's one pre req, that is the usage of Framework 4.5.
This version of the framework added the CompilerServices attribute that gives us details about the Method's caller.
The documentation for this is here
With it we can open up the source file and dig into it.
What i did was just search for the situation you specified in your question, using rudimentary text search.
But it can give you an insight about how to do this on your solution, since i know very little about your solution, i can only work with the code you put on your post.
public static void SearchForThreads(
[System.Runtime.CompilerServices.CallerMemberName] string memberName = "",
[System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "",
[System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
var startKey = "this.Controls.Add(";
var endKey = ")";
List<string> components = new List<string>();
var designerPath = sourceFilePath.Replace(".cs", ".Designer.cs");
if (File.Exists(designerPath))
{
var designerText = File.ReadAllText(designerPath);
var initSearchPos = designerText.IndexOf(startKey) + startKey.Length;
do
{
var endSearchPos = designerText.IndexOf(endKey, initSearchPos);
var componentName = designerText.Substring(initSearchPos, (endSearchPos - initSearchPos));
componentName = componentName.Replace("this.", "");
if (!components.Contains(componentName))
components.Add(componentName);
} while ((initSearchPos = designerText.IndexOf(startKey, initSearchPos) + startKey.Length) > startKey.Length);
}
if (components.Any())
{
var classText = File.ReadAllText(sourceFilePath);
var ThreadPos = classText.IndexOf("Task.Run");
if (ThreadPos > -1)
{
do
{
var endThreadPos = classText.IndexOf("}", ThreadPos);
if (endThreadPos > -1)
{
foreach (var component in components)
{
var search = classText.IndexOf(component, ThreadPos);
if (search > -1 && search < endThreadPos)
{
Console.WriteLine($"Found a call to UI thread component at pos: {search}");
}
}
}
}
while ((ThreadPos = classText.IndexOf("Task.Run", ++ThreadPos)) < classText.Length && ThreadPos > 0);
}
}
}
I hope it helps you out.
You can get the Line number if you split the text so you can output it, but i didn't want to go through the trouble, since i don't know what would work for you.
string[] lines = classText.Replace("\r","").Split('\n');
Try that:
public static void Main(string[] args)
{
// Add the event handler for handling UI thread exceptions to the event.
Application.ThreadException += new ThreadExceptionEventHandler(exception handler);
// Set the unhandled exception mode to force all Windows Forms errors to go through the handler.
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
// Add the event handler for handling non-UI thread exceptions to the event.
AppDomain.CurrentDomain.UnhandledException += // add the handler here
// Runs the application.
Application.Run(new ......);
}
Then you can log the message and the call stack and that should give you enough information to fix the issue.
I recommend you update your GUI to handle this situation automatically for your convenience. You instead use a set of inherited controls.
The general principle here is to override the property Set methods in a way to make them Thread Safe. So, in each overridden property, instead of a straight update of the base control, there's a check to see if an invoke is required (meaning we're on a separate thread the the GUI). Then, the Invoke call updates the property on the GUI thread, instead of the secondary thread.
So, if the inherited controls are used, the form code that is trying to update GUI elements from a secondary thread can be left as is.
Here is the textbox and button ones. You would add more of them as needed and add other properties as needed. Rather than putting code on individual forms.
You don't need to go into the designer, you can instead do a find/replace on the designer files only. For example, in ALL designer.cs files, you would replace System.Windows.Forms.TextBox with ThreadSafeControls.TextBoxBackgroundThread and System.Windows.Forms.Button with ThreadSafeControls.ButtonBackgroundThread.
Other controls can be created with the same principle, based on which control types & properties are being updated from the background thread.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace ThreadSafeControls
{
class TextBoxBackgroundThread : System.Windows.Forms.TextBox
{
public override string Text
{
get
{
return base.Text;
}
set
{
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate { base.Text = value; });
else
base.Text = value;
}
}
public override System.Drawing.Color ForeColor
{
get
{
return base.ForeColor;
}
set
{
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate { base.ForeColor = value; });
else
base.ForeColor = value;
}
}
public override System.Drawing.Color BackColor
{
get
{
return base.BackColor;
}
set
{
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate { base.BackColor = value; });
else
base.BackColor = value;
}
}
}
class ButtonBackgroundThread : System.Windows.Forms.Button
{
public override string Text
{
get
{
return base.Text;
}
set
{
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate { base.Text = value; });
else
base.Text = value;
}
}
public override System.Drawing.Color ForeColor
{
get
{
return base.ForeColor;
}
set
{
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate { base.ForeColor = value; });
else
base.ForeColor = value;
}
}
public override System.Drawing.Color BackColor
{
get
{
return base.BackColor;
}
set
{
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate { base.BackColor = value; });
else
base.BackColor = value;
}
}
}
}

How can i load items back to the listView control from text file and also add checkBoxes near each item faster?

In this method i'm reading a text file from my hard disk and add the items to the listView.
I also changed in the form1 designer on the listView propeties the property CheckBoxes to true.
Now when i'm running my program it's taking like 10-15 seconds to load it up all.
The form1 constructor:
LoadtoListView();
And the method LoadtoListView:
private void LoadtoListView()
{
int countit = 0;
using (StreamReader sr = new StreamReader(#"c:\listviewfile\databaseEN.txt"))
{
while (-1 < sr.Peek())
{
try
{
string name = sr.ReadLine();
string email = sr.ReadLine();
var lvi = new ListViewItem(name.Substring(name.IndexOf(":") + 1));
lvi.SubItems.Add(email.Substring(email.IndexOf(":") + 1));
listView1.Items.Add(lvi);
countit++;
}
catch (Exception) { }
}
sr.Close();
numberofforums = countit;
}
}
There are 547 items to load and 547 checkBoxes.
I tested now if i change in the designer the listView property of the CheckBoxes to false again it will load fast about 1-2 seconds.
But once i'm turning this property of the CheckBoxes to true it's tkaing more then 10-15 seconds to load.
I guess the problem is that it's taking time to draw all the CheckBoxes.
Is there any way to make it all faster ?
There exist a couple of ways you could make that faster. Actually you could make it blazingly fast, checkboxes or no checkboxes.
Your code requires a few tweaks here and there and you must think about reusability, concern separation and parameterisation.
For instance, the method name LoadToListView is already doing too much work. It loads stuff, and it also populates the listview.
There are 3 ingredients that can get you to Nirvana.
First of all
consider creating an up front materialised list of ListViewItem instances, more particularly, create a primitive array, such as this (by the way, I will also sprinkle some other good practices along the way, even though it is not the lacking of those practices which causes your delays):
public ListViewItem[] LoadItems(string filePath) {
// not hardcoding the filePath is a good idea
List<ListViewItem> accumulator = new List<ListViewItem>();
int countit = 0;
using (StreamReader sr = new StreamReader(filePath)) {
while (-1 < sr.Peek()) {
try
string name = sr.ReadLine();
string email = sr.ReadLine();
var lvi = new ListViewItem(name.Substring(name.IndexOf(":") + 1));
lvi.SubItems.Add(email.Substring(email.IndexOf(":") + 1));
// instead of adding this item to the list
// --> no more this:: listView1.Items.Add(lvi);
// just "accumulate" it
accumulator.Add(lvi);
countit++;
}
catch (Exception) { }
}
// no need to manually close the reader
// sr.Close();
// the using clause will close it for you
numberofforums = countit;
}
return accumulator.ToArray();
}
Okay.. So we've created an array of ListViewItem.
"So what?" you might think.
Well, for each and every Add invocation on your ListView, the ListView will try to react graphically (even if the GUI thread is occupied it will still try). What this reaction is is not for this answer to be concerned. What you must understand is that instead of Add you could call AddRange which takes a primitive array of ListViewItem as its parameter. That will cause just one graphical reaction for all the set of ListViewItem instances which means it will speed up your app a lot.
So here's ingredient number 2
Make another method which calls LoadItems and then calls AddRange on the ListView:
public void SomeOtherPlace() {
string filePath = #"....";
ListViewItem[] items = LoadItems(filePath);
this.listView1.Items.AddRange( items );
}
This will have already given your app the extra speed you were looking for, but even if the next step will not make your app elegant, it will surely help.
And ingredient number 3 :: Asynchrony
It would be grand if your UI didn't freeze while the LoadItems method was being called.
This "non freezing" ability can be achieved in many ways, but the most modern and coolest way is to use Task<T> and the async and await operators introduced in .NET 4.5 and C# 5.0.
If you don't have a clue about what these things are then, just enjoy the first two ingredients but don't hesitate to learn about the entities I've mentioned.
Basically what you need to do is:
make sure you can't possibly call SomeOtherPlace() twice, since what we're about to do is to make this a possibility. So if you have a button's event handler, for instance, which is calling SomeOtherPlace then we should disable that button and reenable it once we're done
we will make the SomeOtherPlace() method be an async method, which allows it to await tasks
we will run the LoadItems code on a separate thread all nicely wrapped in a Task<ListViewItem[]> and await it on the GUI thread
Let's go. The first change is this:
public void SomeOtherPlace() {
becomes
public async void SomeOtherPlace() {
Secondly, we disable the button I talked about:
public async void SomeOtherPlace() {
this.button1.Enabled = false;
...
this.button1.Enabled = true;
}
Third, we turn this line:
ListViewItem[] items = LoadItems(filePath);
into this:
ListViewItem[] items = await Task.Factory.StartNew(() => LoadItems(filePath));
Now your method should look something like this:
public async void SomeOtherPlace() {
string filePath = #"....";
ListViewItem[] items = await Task.Factory.StartNew(() => LoadItems(filePath));
this.listView1.Items.AddRange( items );
}
Hope I didn't forget anything.
Good luck and don't settle for not understanding how things work under the hood!
Use listView1.Items.AddRange instead of adding one ListViewItem at a time. This should improve your load time.

Create a ListView with LoadMoreItemsAsync on end of scroll

I have a ListView in my Windows Phone 8.1 application and I can have something like 1000 or more results, so I need to implement a Load More feature each time the scroll hits bottom, or some other logic and natural way of triggering the adding of more items to the List.
I found that the ListView has support for an ISupportIncrementalLoading, and found this implementation: https://marcominerva.wordpress.com/2013/05/22/implementing-the-isupportincrementalloading-interface-in-a-window-store-app/
This was the better solution I found, since it does not specify a type, i.e., it's generic.
My problem with this solution is that when the ListView is Loaded, the LoadMoreItemsAsync runs all the times needed until it got all the results, meaning that the Load More is not triggered by the user. I'm not sure what make the LoadMoreItemsAsync trigger, but something is not right, because it assumes that happens when I open the page and loads all items on the spot, without me doing anything, or any scrolling. Here's the implementation:
IncrementalLoadingCollection.cs
public interface IIncrementalSource<T> {
Task<IEnumerable<T>> GetPagedItems(int pageIndex, int pageSize);
void SetType(int type);
}
public class IncrementalLoadingCollection<T, I> : ObservableCollection<I>, ISupportIncrementalLoading where T : IIncrementalSource<I>, new() {
private T source;
private int itemsPerPage;
private bool hasMoreItems;
private int currentPage;
public IncrementalLoadingCollection(int type, int itemsPerPage = 10) {
this.source = new T();
this.source.SetType(type);
this.itemsPerPage = itemsPerPage;
this.hasMoreItems = true;
}
public bool HasMoreItems {
get { return hasMoreItems; }
}
public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count) {
var dispatcher = Window.Current.Dispatcher;
return Task.Run<LoadMoreItemsResult>(
async () => {
uint resultCount = 0;
var result = await source.GetPagedItems(currentPage++, itemsPerPage);
if(result == null || result.Count() == 0) {
hasMoreItems = false;
}
else {
resultCount = (uint)result.Count();
await dispatcher.RunAsync(
CoreDispatcherPriority.Normal,
() => {
foreach(I item in result)
this.Add(item);
});
}
return new LoadMoreItemsResult() { Count = resultCount };
}).AsAsyncOperation<LoadMoreItemsResult>();
}
}
Here's the PersonModelSource.cs
public class DatabaseNotificationModelSource : IIncrementalSource<DatabaseNotificationModel> {
private ObservableCollection<DatabaseNotificationModel> notifications;
private int _type = "";
public DatabaseNotificationModelSource() {
//
}
public void SetType(int type) {
_type = type;
}
public async Task<IEnumerable<DatabaseNotificationModel>> GetPagedItems(int pageIndex, int pageSize) {
if(notifications == null) {
notifications = new ObservableCollection<DatabaseNotificationModel>();
notifications = await DatabaseService.GetNotifications(_type);
}
return await Task.Run<IEnumerable<DatabaseNotificationModel>>(() => {
var result = (from p in notifications select p).Skip(pageIndex * pageSize).Take(pageSize);
return result;
});
}
}
I changed it a bit, because the call to my Database is Asynchronous and it was the only way I found to make sure I could wait for the query before filling the collection.
And in my DatabaseNotificationViewModel.cs
IncrementalNotificationsList = new IncrementalLoadingCollection<DatabaseNotificationModelSource, DatabaseNotificationModel>(type);
Everything works fine, apart from the not so normal "Load More". What's wrong in my code?
I created a very simplified example of this issue here, and raised this issue on the MSDN forums here. Honestly, I don't know why this weird behavior is happening.
What I observed
The ListView will call LoadMoreItemsAsync first with a count of 1. I assume this is to determine the size of a single item so that it can work out the number of items to request for the next call.
If the ListView is behaving nicely, the second call to LoadMoreItemsAsync should happen immediately after the first call, but with the correct number of items (count > 1), and then no more calls to LoadMoreItemsAsync will occur unless you scroll down. In your example, however, it may incorrectly call LoadMoreItemsAsync with a count of 1 again.
In the worst case, which actually occurs quite frequently in your example, is that the ListView will continue to call LoadMoreItemsAsync with a count of 1 over and over, in order, until HasMoreItems becomes false, in which case it has loaded all of the items one at a time. When this happens, there is a noticeable UI delay while the ListView loads the items. The UI thread isn't blocked, though. The ListView is just hogging the UI thread with sequential calls to LoadMoreItemsAsync.
The ListView won't always exhaust all of the items though. Sometimes it will load 100, or 200, or 500 items. In each case, the pattern is: many calls of LoadMoreItemsAsync(1) followed by a single call to LoadMoreItemsAsync(> 1) if not all of the items have been loaded by the prior calls.
It only seems to occur on page load.
The issue is persistent on Windows Phone 8.1 as well as Windows 8.1.
What causes the problem
The issue seems to be very short lived awaited tasks in the LoadMoreItemsAsync method before you've added the items to the list (awaiting tasks after you've added the items to the list is fine).
The issue doesn't occur if you remove all awaits inside LoadMoreItemsAsync, thus forcing it to execute synchronously. Specifically, if you remove the dispatcher.RunAsync wrapper and await source.GetPagedItems (just mock the items instead), then the ListView will behave nicely.
Having removed all awaits, the issue will reappear even if all you add is a seemingly harmless await Task.Run(() => {}). How bizarre!
How to fix the problem
If most of the time spent in a LoadMoreItemsAsync call is waiting for a HTTP request for the next page of items, as I expect most apps are, then the issue won't occur. So, we can extend the time spent in the method by awaiting a Task.Delay(10), like this maybe:
await Task.WhenAll(Task.Delay(10), dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
foreach (I item in result)
this.Add(item);
}).AsTask());
I've merely provided a (hacky) workaround for your example, but not an explanation why. If anyone knows why this is happening, please let me know.
This is not the only thing that can cause this issue. If your ListView is inside a ScrollViewer, it will continue loading all of the items and ALSO will not virtualize properly, negatively impacting performance. The solution is to give your ListView a specific height.

Categories

Resources