On my view I have a GridView. As the number of items can be very high I'm trying to implement ISupportIncrementalLoading as new ItemsSource.
public class IncrementalCollection : ObservableCollection<Object>, ISupportIncrementalLoading
{
private int _addedItems = 0;
private const int _PAGESIZE = 20;
private IList<BookingDTO> _bookings;
public IncrementalCollection(Guid guid)
{
LoadBookings(guid);
}
private async Task LoadBookings(Guid guid)
{
var data = await IoC.Resolve<IMBAMService>().GetOrderedBookingsForAccountAsync(guid);
_bookings = data.Data;
}
public bool HasMoreItems
{
get { return (this.Count < _bookings.Count); }
}
public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
{
var coreDispatcher = Window.Current.Dispatcher;
return Task.Run<LoadMoreItemsResult>(async () =>
{
await coreDispatcher.RunAsync(CoreDispatcherPriority.High,
() =>
{
foreach (var item in _bookings.Skip(_addedItems).Take((int)count))
{
_addedItems++;
this.Add(item);
}
});
return new LoadMoreItemsResult() { Count = count };
}).AsAsyncOperation<LoadMoreItemsResult>();
}
}
In the navigation function I create a new instance.
BookingList.ItemsSource = new IncrementalCollection(ViewModel.Account.Id);BookingList.ItemsSource = new IncrementalCollection(Guid);
My problem is now that LoadMoreItemsAsync is called so many times that the hole list will be displayed and not as expected after scrolling.
What do I need to change that it loads only the first 50 items and the rest when it's needed after scrolling?
I've tried to implement it like here:
http://blogs.msdn.com/b/devosaure/archive/2012/10/15/isupportincrementalloading-loading-a-subsets-of-data.aspx
It's possible that the GridView is trying to make use of infinite space instead of limiting its size to that of the visible screen. This would cause it to keep querying to load items to fill up the available space, even if those items are not visible. Read this page for more info on this, specifically where it mentions ScrollViewer: http://msdn.microsoft.com/en-us/library/windows/apps/hh994637.aspx.
Related
I have implemented incremental loading like this:
public class IncrementalDataSource : ObservableCollection<Files>, ISupportIncrementalLoading
{
private int id;
private int itemsPerPage;
private int currentPage;
public IncrementalMediaEntrySource(int ID, int ipp = 4)
{
HasMoreItems = true;
id = ID;
itemsPerPage = ipp;
currentPage = 1;
}
public bool HasMoreItems
{
get;
private set;
}
public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
{
var dispatcher = Window.Current.Dispatcher;
return Task.Run<LoadMoreItemsResult>(
async () =>
{
uint resultCount = 0;
var result = await GetData(id, itemsPerPage, currentPage++);
if (result == null || result.Count() == 0)
{
HasMoreItems = false;
}
else
{
resultCount = (uint)result.Count();
await dispatcher.RunAsync(
CoreDispatcherPriority.Normal,
() =>
{
foreach (var item in result)
this.Add(item);
});
}
return new LoadMoreItemsResult() { Count = resultCount };
}).AsAsyncOperation<LoadMoreItemsResult>();
}
}
But it's not loading all data. It only gets around 16 items from 100+ items. If I change itemsPerPage from 4 to 10, it gets all items. But it takes a bit too long to load by 10 items, so I would like to stay with 4 items. And of course I would like to know, what is causing this, to avoid that. Where could be the problem?
UPDATE:
A friend of mine suggested to add "fake" objects, if itemsPerPage is less than 7(minimums number with which it is working correctly), so that total objects recieved would be 7. I tried that, and it actually worked, but of course added empty objects, which is totally useless. So I added check before adding objects to ObservableCollection<Files> like this:
foreach (var item in result)
if (item.Id != null) this.Add(item);
But then it is not working again. So maybe the problem is in adding? But I cannot think of possible cause for this...
I have been trying to implement ISupportIncrementalLoading interface for an ObservableCollection. My implementation works for the most part but it behaves strangely under uncertain circumstances. Here is my code to IncrementalCollection class.
public class IncrementalCollection<T> : ObservableCollection<T>, ISupportIncrementalLoading
{
private bool hasMoreItems;
private int currentPage;
private string filter;
private Func<string, int, Task<IList<T>>> func;
Action onLoadingStarts;
Action onLoadingEnds;
public IncrementalCollection(Func<string, int, Task<IList<T>>> func, Action onLoadingStarts, Action onLoadingEnds)
{
this.func = func;
this.hasMoreItems = true;
this.onLoadingStarts = onLoadingStarts;
this.onLoadingEnds = onLoadingEnds;
}
public void ResetCollection(string filter)
{
currentPage = 0;
this.filter = filter;
this.Clear();
}
public bool HasMoreItems
{
get { return hasMoreItems; }
}
public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
{
return DoLoadMoreItemsAsync(count).AsAsyncOperation<LoadMoreItemsResult>();
}
private async Task<LoadMoreItemsResult> DoLoadMoreItemsAsync(uint count)
{
onLoadingStarts();
var result = await func(this.filter, ++this.currentPage);
if (result == null || result.Count == 0)
{
hasMoreItems = false;
}
else
{
foreach (T item in result)
this.Add(item);
}
onLoadingEnds();
return new LoadMoreItemsResult() { Count = result == null ? 0 : (uint)result.Count };
}
}
The first strange behavior occurs when page loads, LoadMoreItemsAsync function is sometimes called once, generally twice and sometimes more than twice. This is strange as one call is enough to add enough items to the collection. I even tried to pull more data (2-3 times) but the behavior continues. There might be problem about the place of initialization of the IncrementalCollection object. As it seems the longer it takes to load the page the more calls are made to LoadMoreItemsAsync function. I am creating the collection in NavigationHelper_LoadState function like this.
_users = new IncrementalCollection<User>((filter, page) => _dataService.GetUserList(url, filter, null, page), onLoadingStarts, onLoadingEnds);
Second strange behavior is about caching, although I have added
this.NavigationCacheMode = NavigationCacheMode.Disabled;
to every page constructor and also changed NavigationHelper not to save pageState on back navigation. It feels like web requests are cached as it is very hard to return a response in that amount of time.
public void OnNavigatedFrom(NavigationEventArgs e)
{
if (e.NavigationMode == NavigationMode.Back)
return;
var frameState = SuspensionManager.SessionStateForFrame(this.Frame);
var pageState = new Dictionary<String, Object>();
if (this.SaveState != null)
{
this.SaveState(this, new SaveStateEventArgs(pageState));
}
frameState[_pageKey] = pageState;
}
Any help about these strange behaviors is appreciated.
Also is there any good tutorial about ISupportIncrementalLoading interface that explains LoadMoreItemsAsync firing conditions. I am trying to modify a WrapPanel implementation but don't know where to start as I don't know what it looks for. This is probably about ItemHeight but still concrete information is better.
Thanks in advance.
There seems to be a bug in ISupportIncrementalLoading interface. Solved the multiple request problem by applying the solution here Create a ListView with LoadMoreItemsAsync on end of scroll.
I wrapped the foreach loop inside Task.WhenAll call.
await Task.WhenAll(Task.Delay(50), Window.Current.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
foreach (T item in result)
this.Add(item);
}).AsTask());
In my first view model (renamed to MainViewModel) I have a list of ActionViewModels.
In my xaml i have a listbox which is bound to the list, in the listbox i have a template which binds to properties from the ActionViewModel.
So far so good and everything works.
When selecting one of the listitems i navigate to an ActionViewModel and pass the id with it.
The ActionViewModel retrieves information from a static list in memory from which the MainViewModel also retrieved the information to create the list of actionviewmodels.
So far still so good, i can edit the properties, all the bindings do work fine and i'm all happy.
By clicking the save button the information is gathered and stored in the static list.
When i hit the back button i go back to the list, but unfortunately the values showing there are still the same, is there some way to send a command to reload the items in the list? To pass a complete viewmodel as reference to a new ActionViewModel? Or some property which tells the parent 'this viewmodel in your list has been updated'?
I am sure the above text is a bit confusing, so here is some code to clarify it a bit (hopefully)
MainViewModel.cs
private List<ActionViewModel> _actionViewModels;
public List<ActionViewModel> ActionViewModels
{
get { return _actionViewModels; }
set { _actionViewModels = value; RaisePropertyChanged(() => ActionViewModels); }
}
private Cirrious.MvvmCross.ViewModels.MvxCommand<int> _navigateToAction;
public System.Windows.Input.ICommand NavigateToAction
{
get
{
_navigateToAction = _navigateToAction ?? new Cirrious.MvvmCross.ViewModels.MvxCommand<int>((action) => NavigateToTheDesiredAction(action));
return _navigateToAction;
}
}
private void NavigateToTheDesiredAction(int action)
{
ShowViewModel<ActionViewModel>(new { id = action });
}
// Get DTOs from server or from cache and fill the list of ActionViewModels
public async Task Load()
{
ActionService actionService = new ActionService();
List<ActionViewModel> actionViewModels = new List<ActionViewModel>();
MyActions = await actionService.GetMyActions();
foreach (ActionDTO action in MyActions)
{
ActionViewModel actionViewModel = new ActionViewModel();
await actionViewModel.Load(action.id);
actionViewModels.Add(actionViewModel);
}
ActionViewModels = actionViewModels;
}
ActionViewModel.cs
public int ID
{
get { return TheAction.id; }
set { TheAction.id = value; RaisePropertyChanged(() => ID); }
}
public string Title
{
get { return TheAction.Title; }
set { TheAction.Title = value; RaisePropertyChanged(() => Title); }
}
public async Task Load(int actionId)
{
ActionDTO TheAction = await actionService.GetAction(actionId);
this.ID = TheAction.id;
this.Title = TheAction.Title;
}
private Cirrious.MvvmCross.ViewModels.MvxCommand _save;
public System.Windows.Input.ICommand Save
{
get
{
_save = _save ?? new Cirrious.MvvmCross.ViewModels.MvxCommand(PreSaveModel);
return _save;
}
}
private void PreSaveModel()
{
SaveModel();
}
private async Task SaveModel()
{
ValidationDTO result = await actionService.SaveAction(TheAction);
}
ActionService.cs
public static List<ActionDTO> AllActions = new List<ActionDTO>();
public async Task<ActionDTO> GetAction(int actionId)
{
ActionDTO action = AllActions.FirstOrDefault(a => a.id == actionId);
if (action == null)
{
int tempActionId = await LoadAction(actionId);
if (tempActionId > 0)
return await GetAction(actionId);
else
return new ActionDTO() { Error = new ValidationDTO(false, "Failed to load the action with id " + actionId, ErrorCode.InvalidActionId) };
}
return action;
}
private async Task<int> LoadAction(int actionId)
{
ActionDTO action = await webservice.GetAction(actionId);
AllActions.Add(action);
return action.id;
}
public async Task<ValidationDTO> SaveAction(ActionDTO action)
{
List<ActionDTO> currentList = AllActions;
ActionDTO removeActionFromList = currentList.FirstOrDefault(a => a.id == action.id);
if (removeActionFromList != null)
currentList.Remove(removeActionFromList);
currentList.Add(action);
AllActions = currentList;
return await webservice.SaveAction(action);
}
There are 3 ways I can think of that would allow you to do this.
The ActionService could send out some sort of notification when data changes. One easy way to do this is to use the MvvmCross Messenger plugin. This is the way the CollectABull service works in CollectionService.cs in the N+1 days of mvvmcross videos (for more info watch N=13 in http://mvvmcross.wordpress.com)
This is the approach I generally use. It has low overhead, uses WeakReferences (so doesn't leak memory), it is easily extensible (any object can listen for changes), and it encourages loose coupling of the ViewModel and Model objects
You could implement some kind of Refresh API on the list ViewModel and could call this from appropriate View events (e.g. ViewDidAppear, OnNavigatedTo and OnResume).
I don't generally use this approach for Refreshing known data, but I have used it for enabling/disabling resource intensive objects - e.g. timers
For certain shape of model data (and especially how often it changes), then I can imagine scenarios where this approach might be more efficient than the messenger approach.
You could extend the use of INotifyPropertyChanged and INotifyCollectionChanged back into your model layer.
I've done this a few times and it's worked well for me.
If you do choose this approach, be careful to ensure that all Views do subscribe to change events using WeakReference subscriptions such as those used in MvvmCross binding - see WeakSubscription. If you didn't do this, then it could be possible for the Model to cause Views to persist in memory even after the UI itself has removed them.
I have the following code, which basically takes values from a database and populates a listview.
using (IDataReader reader = cmd.ExecuteReader())
{
lvwMyList.Items.Clear();
while (reader.Read())
{
ListViewItem lvi = lvwMyList.Items.Add(reader["Value1"].ToString());
lvi.SubItems.Add(reader["Value2"].ToString());
}
}
The problem that I have is that this is repeatedly executed at short intervals (every second) and results in the items in the listview continually disappearing and re-appearing. Is there some way to stop the listview from refreshing until it’s done with the updates? Something like below:
using (IDataReader reader = cmd.ExecuteReader())
{
lvwMyList.Items.Freeze(); // Stop the listview updating
lvwMyList.Items.Clear();
while (reader.Read())
{
ListViewItem lvi = lvwMyList.Items.Add(reader["Value1"].ToString());
lvi.SubItems.Add(reader["Value2"].ToString());
}
lvwMyList.Items.UnFreeze(); // Refresh the listview
}
Like this:
try
{
lvwMyList.BeginUpdate();
//bla bla bla
}
finally
{
lvwMyList.EndUpdate();
}
Make sure that you invoke lvwMyList.Items.Clear() after BeginUpdate if you want to clear the list before filling it.
This is my first time posting on StackOverflow, so pardon the messy code formatting below.
To prevent locking up the form while updating the ListView, you can use the method below that I've written to solve this issue.
Note: This method should not be used if you expect to populate the ListView with more than about 20,000 items. If you need to add more than 20k items to the ListView, consider running the ListView in virtual mode.
public static async void PopulateListView<T>(ListView listView, Func<T, ListViewItem> func,
IEnumerable<T> objects, IProgress<int> progress) where T : class, new()
{
if (listView != null && listView.IsHandleCreated)
{
var conQue = new ConcurrentQueue<ListViewItem>();
// Clear the list view and refresh it
if (listView.InvokeRequired)
{
listView.BeginInvoke(new MethodInvoker(() =>
{
listView.BeginUpdate();
listView.Items.Clear();
listView.Refresh();
listView.EndUpdate();
}));
}
else
{
listView.BeginUpdate();
listView.Items.Clear();
listView.Refresh();
listView.EndUpdate();
}
// Loop over the objects and call the function to generate the list view items
if (objects != null)
{
int objTotalCount = objects.Count();
foreach (T obj in objects)
{
await Task.Run(() =>
{
ListViewItem item = func.Invoke(obj);
if (item != null)
conQue.Enqueue(item);
if (progress != null)
{
double dProgress = ((double)conQue.Count / objTotalCount) * 100.0;
if(dProgress > 0)
progress.Report(dProgress > int.MaxValue ? int.MaxValue : (int)dProgress);
}
});
}
// Perform a mass-add of all the list view items we created
if (listView.InvokeRequired)
{
listView.BeginInvoke(new MethodInvoker(() =>
{
listView.BeginUpdate();
listView.Items.AddRange(conQue.ToArray());
listView.Sort();
listView.EndUpdate();
}));
}
else
{
listView.BeginUpdate();
listView.Items.AddRange(conQue.ToArray());
listView.Sort();
listView.EndUpdate();
}
}
}
if (progress != null)
progress.Report(100);
}
You don't have to provide an IProgress object, just use null and the method will work just as well.
Below is an example usage of the method.
First, define a class that contains the data for the ListViewItem.
public class TestListViewItemClass
{
public int TestInt { get; set; }
public string TestString { get; set; }
public DateTime TestDateTime { get; set; }
public TimeSpan TestTimeSpan { get; set; }
public decimal TestDecimal { get; set; }
}
Then, create a method that returns your data items. This method could query a database, call a web service API, or whatever, as long as it returns an IEnumerable of your class type.
public IEnumerable<TestListViewItemClass> GetItems()
{
for (int x = 0; x < 15000; x++)
{
yield return new TestListViewItemClass()
{
TestDateTime = DateTime.Now,
TestTimeSpan = TimeSpan.FromDays(x),
TestInt = new Random(DateTime.Now.Millisecond).Next(),
TestDecimal = (decimal)x + new Random(DateTime.Now.Millisecond).Next(),
TestString = "Test string " + x,
};
}
}
Finally, on the form where your ListView resides, you can populate the ListView. For demonstration purposes, I'm using the form's Load event to populate the ListView. More than likely, you'll want to do this elsewhere on the form.
I've included the function that generates a ListViewItem from an instance of my class, TestListViewItemClass. In a production scenario, you will likely want to define the function elsewhere.
private async void TestListViewForm_Load(object sender, EventArgs e)
{
var function = new Func<TestListViewItemClass, ListViewItem>((TestListViewItemClass x) =>
{
var item = new ListViewItem();
if (x != null)
{
item.Text = x.TestString;
item.SubItems.Add(x.TestDecimal.ToString("F4"));
item.SubItems.Add(x.TestDateTime.ToString("G"));
item.SubItems.Add(x.TestTimeSpan.ToString());
item.SubItems.Add(x.TestInt.ToString());
item.Tag = x;
return item;
}
return null;
});
PopulateListView<TestListViewItemClass>(this.listView1, function, GetItems(), progress);
}
In the above example, I created an IProgress object in the form's constructor like this:
progress = new Progress<int>(value =>
{
toolStripProgressBar1.Visible = true;
if (value >= 100)
{
toolStripProgressBar1.Visible = false;
toolStripProgressBar1.Value = 0;
}
else if (value > 0)
{
toolStripProgressBar1.Value = value;
}
});
I've used this method of populating a ListView many times in projects where we were populating up to 12,000 items in the ListView, and it is extremely fast. The main thing is you need to have your object fully built from the database before you even touch the ListView for updates.
Hopefully this is helpful.
I've included below an async version of the method, which calls the main method shown at the top of this post.
public static Task PopulateListViewAsync<T>(ListView listView, Func<T, ListViewItem> func,
IEnumerable<T> objects, IProgress<int> progress) where T : class, new()
{
return Task.Run(() => PopulateListView<T>(listView, func, objects, progress));
}
You can also try setting the visible or enabled properties to false during the update and see if you like those results any better.
Of course, reset the values to true when the update is done.
Another approach is to create a panel to overlay the listbox. Set it's left, right, height, and width properties the same as your listbox and set it's visible property to true during the update, false after you're done.
I have a WinForms application which uses a list box to display list of items. My application hangs whent the number of items in the listbox exceeds some 150 items. Is this the property of ListBox control that it can hold only so many items? If so, I would request you to provide a solution to this problem.
Thanks,
Rakesh.
It all depends on what you are binding, if you are binding simple key value pairs you can instantly bind 10k easy. You might want to try adding the items in a loop instead of binding to see if there is a certain item it hangs on.
for (int i = 0; i < 10000; i++)
{
listBox1.Items.Add("item:" + i.ToString());
}
You can back your listbox by a larger dataset and use a paging mechanism or you can add an event listener for SizeChanged and disable adding when it reaches your max.
First tip, always..
SuspendLayout();
// fill your lists
ResumeLayout();
Second tip, use AddRange when possible.
Third, and it may be overkill, create your own ListBox...
public class LimitedListBox : ListBox
{
private int _maxItems = 100;
public LimitedListBox()
{
SetItems(new LimitedObjectCollection(this, _maxItems));
}
public int MaxItems
{
get { return _maxItems; }
set { _maxItems = value; }
}
/// <summary>
/// This is the only 'bug' - no design time support for Items unless
/// you create an editor.
/// </summary>
public new LimitedObjectCollection Items
{
get
{
if (base.Items == null)
{
SetItems(new LimitedObjectCollection(this, _maxItems));
}
return (LimitedObjectCollection) base.Items;
}
}
private void SetItems(ObjectCollection items)
{
FieldInfo info = typeof (ListBox).GetField("itemsCollection",
BindingFlags.NonPublic | BindingFlags.Instance |
BindingFlags.GetField);
info.SetValue(this, items);
}
#region Nested type: LimitedObjectCollection
public class LimitedObjectCollection : ObjectCollection
{
private int _maxItems;
public LimitedObjectCollection(ListBox owner, int maxItems)
: base(owner)
{
_maxItems = maxItems;
}
public LimitedObjectCollection(ListBox owner, ObjectCollection value, int maxItems)
: base(owner)
{
_maxItems = maxItems;
AddRange(value);
}
public LimitedObjectCollection(ListBox owner, object[] value, int maxItems)
: base(owner)
{
_maxItems = maxItems;
AddRange(value);
}
public int MaxItems
{
get { return _maxItems; }
set { _maxItems = value; }
}
public new int Add(object item)
{
if (base.Count >= _maxItems)
{
return -1;
}
return base.Add(item);
}
public new void AddRange(object[] items)
{
int allowed = _maxItems - Count;
if (allowed < 1)
{
return;
}
int length = allowed <= items.Length ? allowed : items.Length;
var toAdd = new object[length];
Array.Copy(items, 0, toAdd, 0, length);
base.AddRange(toAdd);
}
public new void AddRange(ObjectCollection value)
{
var items = new object[value.Count];
value.CopyTo(items, 0);
base.AddRange(items);
}
}
#endregion
}
Why don't you just parse your database something like this.
int Total = yourarray.GetLength(0);
Then all you have to do is this in your new array.
double [] new = double[Total];
array.copy(Total,new);
Now you have an array thats dynamic. Anytime your database grows it automatically populates
the new array.
Or if you can do a select count statement on your database you can get the total number or rows and then pipe that to a string. Then you use the string to make control the array. Hope this helps
I just got an "out of memory" error message when filling a list box. The problem was not anything to do with too many items. There was a bug in my code and the items in the list box were returning null in the ToString() method. So the Dot Net error message was wrong and confusing.