VirtualQueryableCollectionView concurrency and scroll loading same object in multiple rows

6 posts, 0 answers
  1. Remco
    Remco avatar
    13 posts
    Member since:
    Jan 2012

    Posted 28 Feb 2012 Link to this post

    Hello,

    I have a RadGridView bound to a VirtualQueryableCollectionView and the grid is sorted descending by a DateTime column. Given a load size of 30 rows, when a user A has the first 30 rows loaded, and subsequently a user B inserts a new row at the top, effectively pushing all other rows down, and following that user A scrolls down resulting in the next 30 rows being loaded, then the entity previously at row index 29 will be loaded again and displayed again at row index 30, while it is also still displayed at row index 29.

    Since I am using WCF RIA Services the entity set will only contain the entity in memory once, but the RadGridView displays it twice. I want to prevent that from happening. Could Telerik provide guidance on the recommended way to achieve this?

    I have tried to detect when my load operation completes but before I load the entities into the VirtualQueryableCollectionView whether any of the returned entities has already been loaded into the VirtualQueryableCollectionView using the IndexOf method and if so call the Refresh method on the VirtualQueryableCollectionView, but this quickly becomes a mess with the view continually being refreshed.

    I also want the Refresh to avoid loading the rows 30 to 59 again that allowed me to detect the issue, as I know those 30 rows are ok: it's all other rows that need to be refreshed. I say 'refresh all other rows' as that is what is required when user B insert a row at the top that effectiely pushes all other rows down, as in my example. But another example is user B makes an update of the DateTime column at say row index 10, moving that entity to say row index 40, in which case for user A only the rows from 10 to 29 need to be refreshed.

    Any help with this would be much appreciated.

    cheers

    Remco
  2. Vlad
    Admin
    Vlad avatar
    11100 posts

    Posted 28 Feb 2012 Link to this post

    Hi,

     Generally if the server-side count is different to the client count (VirtuaItemCount) you should equalize them (similar to our demos) which will recreate the virtual collection. 

    Kind regards,
    Vlad
    the Telerik team
    Sharpen your .NET Ninja skills! Attend Q1 webinar week and get a chance to win a license! Book your seat now >>
  3. Remco
    Remco avatar
    13 posts
    Member since:
    Jan 2012

    Posted 29 Feb 2012 Link to this post

    Hi Vlad,

    Equalizing the server-side count with the client count does resolve the issue when the server-side count has changed. Although when the server-side count has changed and the client count is equalized recreating the virtual collection then the current scroll position is lost and reset to the top. Also, requesting the total item count every time the user scrolls the RadGridView has a large impact on performance, so therefore I only do this the first time the RadGridView loads or when the user explicitly clicks a refresh button or automatically after a (configurable) 10 minute timeout. But even though a refresh is triggered automatically after 10 minutes or can be triggered explicitly by clicking the refresh button, I do want to avoid the RadGridView showing the same entity twice on different rows when the user scrolls the RadGridView. I was therefore hoping to be able to detect the same entity being loaded in the VirtualQueryableCollectionView twice and then perform an 'intelligent' refresh that also maintains the scroll position (sorry, I don't mean to call the current refresh method the opposite of intelligent).

    Equalizing the server-side count with the client count does not resolve the issue when the server-side count has not changed. As in my second example, when the grid is sorted descending by a DateTime column, if user A has loaded the first 30 rows, then user B updates the DateTime value of the entity at row index 10 which moves it to row index 40, then when user A scrolls down to load the next 30 rows he will see that updated entity at row index 40, but it will also still be displayed at row index 10, all be it with the updated DateTime value. Also, the entity that was previously at row index 30, but has now been pushed up to row index 29, will not have been loaded at all. How can I detect this case and perform that 'intelligent' refresh that also maintains the scroll position.

    cheers

    Remco
  4. Remco
    Remco avatar
    13 posts
    Member since:
    Jan 2012

    Posted 29 Feb 2012 Link to this post

    Hi Vlad,

    This is how I could currently detect whether an entity has already been loaded. Unfortunately I cannot call the Refresh method on the VirtualQueryableCollectionView as this will result in a false positive detection of an entity already loaded triggering another Refresh, and so on. The only thing I can do is recreating the VirtualQueryableCollectionView, which is not as nice a user experience as when you call the Refresh method on the VirtualQueryableCollectionView: it appears to recreate the RadGridView and resets the scroll position to the top and it makes unneccesary calls to the web service. I would like to make the refresh more intelligent than that.

    private void OnEntriesItemsLoading(object sender, VirtualQueryableCollectionViewItemsLoadingEventArgs e)
    {
        var queryBuilder = new QueryBuilder<EntryPresentationModel>(this.requestTotalItemCount).SortBy(this.Entries).Skip(e.StartIndex).Take(e.ItemCount);
     
        this.context.LoadEntriesByLogbookNameAsync(this.appService.LogbookName, queryBuilder, e.StartIndex).ContinueWith(
            this.OnLoadEntriesComplete, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.FromCurrentSynchronizationContext());
     
        this.requestTotalItemCount = false;
    }
     
    private void OnLoadEntriesComplete(Task<ServiceLoadResult<EntryPresentationModel>> task)
    {
        if (task.IsFaulted)
        {
            Exception exceptionToThrow;
            var rethrow = this.exceptionManager.HandleException(task.Exception.InnerException, ExceptionHandlingPolicies.ServiceLayer, out exceptionToThrow);
            if (rethrow)
            {
                throw exceptionToThrow ?? task.Exception.InnerException;
            }
        }
        else if (!task.IsCanceled)
        {
            var needRefresh = task.Result.Entities.Any(entry => this.Entries.IndexOf(entry) != -1);
            if (needRefresh)
            {
                this.requestTotalItemCount = true;
                this.Entries = new VirtualQueryableCollectionView<EntryPresentationModel> { LoadSize = this.appService.LoadSize, VirtualItemCount = this.appService.LoadSize };
                this.Entries.ItemsLoading += this.OnEntriesItemsLoading;
            }
            else
            {
                if (task.Result.TotalEntityCount != -1 && task.Result.TotalEntityCount != this.Entries.VirtualItemCount)
                {
                    this.Entries.VirtualItemCount = task.Result.TotalEntityCount;
                }
     
                var startIndex = (int)task.AsyncState;
                this.Entries.Load(startIndex, task.Result.Entities);
            }
        }
    }
     
    private void OnRefresh()
    {
        this.refreshDateTime = DateTime.UtcNow.Add(this.appService.RefreshInterval);
        this.RefreshTimeSpan = this.appService.RefreshInterval;
     
        this.requestTotalItemCount = true;
        ////this.Entries.Refresh();
        this.Entries = new VirtualQueryableCollectionView<EntryPresentationModel> { LoadSize = this.appService.LoadSize, VirtualItemCount = this.appService.LoadSize };
        this.Entries.ItemsLoading += this.OnEntriesItemsLoading;
    }
     
    private void OnTimerTick()
    {
        this.RefreshTimeSpan = this.refreshDateTime - DateTime.UtcNow;
     
        if (this.RefreshTimeSpan <= TimeSpan.Zero)
        {
            this.OnRefresh();
        }
    }


    cheers

    Remco
  5. Vlad
    Admin
    Vlad avatar
    11100 posts

    Posted 29 Feb 2012 Link to this post

    Hello,

     I believe that after setting 

    this
    .Entries.VirtualItemCount = task.Result.TotalEntityCount; 

    you do not need to call Load() for the collection unfortunately I'm afraid that it is not possible to keep the scroll during such changes. 

    All the best,
    Vlad
    the Telerik team
    Sharpen your .NET Ninja skills! Attend Q1 webinar week and get a chance to win a license! Book your seat now >>
  6. Remco
    Remco avatar
    13 posts
    Member since:
    Jan 2012

    Posted 29 Feb 2012 Link to this post

    Hi Vlad,

    this.Entries.Refresh();

    does preserve the scroll position. However, because using this method results in these false positives when I do

    var needRefresh = task.Result.Entities.Any(entry => this
    .Entries.IndexOf(entry) != -1);

    I end up in a never ending cycle of refreshing. I want to avoid having to call

    this.Entries = new VirtualQueryableCollectionView<EntryPresentationModel> { LoadSize = this.appService.LoadSize, VirtualItemCount = this.appService.LoadSize };
    this.Entries.ItemsLoading += this.OnEntriesItemsLoading;

    when I want to do a refresh.Hence my request for help.

    I suspect the solution lies somewhere in the VirtualQueryableCollectionView code where the appropriate rows need to be marked as needing a refresh, but I cannot change that code. As it is, I suspect the VirtualQueryableCollectionView is more appropriate for use with more static data, and less appropriate for use with more frequently and concurrently changing data. I don't mind if the data is not bang up to date (the user can click the Refresh button or there is the automatic refresh after 10 mins), but the data must be consistent when scrolling, and any inconsistency must be resolved automatically without the user noticing, without a big delay and without loosing the scroll position.

    cheers

    Remco

Back to Top