This project is read-only.

Asynchronous loading of entries

FileExplorer1

v1's loading code is done in CurrentDirectoryViewModel, a very simple linq statement.
private List<FileListViewItemViewModel> getEntries()
{
    var retVal = from entry in 
            EmbeddedDirectoryModel.EmbeddedDirectoryEntry.EnumerateFileSystemInfos()
            where (entry is DirectoryInfoEx && ListDirectories) || (entry is FileInfoEx && ListFiles)
            select new FileListViewItemViewModel(_rootModel, ExModel.FromExEntry(entry)); ;
    _cachedSubEntries = retVal.ToArray();

    return new List<FileListViewItemViewModel>(_cachedSubEntries);
}

FileExplorer2

v2 introduce asynchronous loading by using the AsyncObservableCollection, which load the entries (in a linq statement) in background thread, and notify UI thread to add or remove an item in UI thread. This is required because UI thread shouldn't be blocked by a slow operation, and the UI code fails if an UI object is changed by a non-UI thread. v2 uses the same loading mechanism.

As shown, AsyncObservableCollection take an IEnumerable<T> taskFunc and loop it. Whenever an item is returned, newItemAction is called in UI thread, and add the item immediately.

public class AsyncBackgroundTaskManager<T>
{
   
    public AsyncBackgroundTaskManager(DispatcherPriority priority,
        Func<IEnumerable<T>> taskFunc, 
        Action beginAction, Action<T> newItemAction, 
        Action<IList<T>, Exception> completeAction)
    {
        _dispatcher = Dispatcher.CurrentDispatcher;
        ...
        _newItemAction = (item) =>
            {
                InvokeInDispatcher(() => newItemAction(item)); //Invoke in UI thread.
            };
        ...
    }
   
    ...
    public virtual void RunBackgroundTask()
    {
        if (_taskFunc != null && !IsWorking)
        {
            //Task wasn't available at that time.
            _workingThread = new Thread(new ThreadStart(() =>
                {
                    try
                    {
                        _beginAction();
                        List<T> retList = new List<T>();
                        IEnumerator<T> enumerator = _taskFunc().GetEnumerator();
                        while (enumerator.MoveNext())
                        {
                            _newItemAction(enumerator.Current);
                            retList.Add(enumerator.Current);
                        }

                        _completeAction(retList, null);
                        if (BackgroundTaskCompleted != null)
                            InvokeInDispatcher(() => 
                                BackgroundTaskCompleted(this, new EventArgs()));
                    }
                   ...
                    finally { ... }
                }));
            _workingThread.Start();
        }

    }
    ...
}

public class AsyncObservableCollection<T> : ObservableCollection<T>
{
    ...
    public AsyncObservableCollection(
        DispatcherPriority priority,
        IEnumerable<T> taskFunc,
        Action beginAction,
        Action<T> newItemAction,
        Action<T> removeItemAction,
        Action<IList<T>, Exception> completeAction)
        : base()
    {
        ....
        _bgWorker = new AsyncBackgroundTaskManager<T>(priority, taskFunc,
            () => { beginAction(); },
            (item) => { this.Add(item); newItemAction(item); },
            (item) => { this.Remove(item); removeItemAction(item); },
            (list, ex) => { completeAction(list, ex); });
    }
    ....
}

Using AsyncObservableCollection, UI tries to display entries as soon as it's created, this loading method is suitable if the time of returning each item is slow, which is not the case nowadays, the FileSystem is cached by the Operating system, and web storage returns json data, which returns the whole file list one at a time.

FileExplorer3

In v3, because of availability of await, EntriesHelper class is written, EntriesHelper is wrote to allow code reuse as much as possible, it's used in FileExplorer3 to list all kinds of ViewModels (e.g. EntryViewModels, CommandViewModels).

EntriesHelper basically encapsulate the loading code in LoadAsync() method, unlike AsyncObservableCollection, it returns only after LoadAsync() is completed.

EntriesHelper maintain two lists. All, which is an ObservableCollection, which returns items for UI thread, and AllNonBindable, which is used by viewmodel to access item in non-UI thread.

EntriesHelper support Update or Replace mode, the first only update the changes, the another replace the entries list completely. It's useful because if there's an UIElement that represent the VM, using Update mode will not re-create the UIElement, but on the another side, Replace is faster because Update requires a lot of Equality comparsion.
public class EntriesHelper<VM> : NotifyPropertyChanged, IEntriesHelper<VM>
{
   ...
    public EntriesHelper(Func<bool, object, Task<IEnumerable<VM>>> loadSubEntryFunc)
    {
        _loadSubEntryFunc = loadSubEntryFunc;

        All = new FastObservableCollection<VM>();
        All.Add(default(VM));
    }
    ...
    
    public async Task<IEnumerable<VM>> LoadAsync(
           UpdateMode updateMode = UpdateMode.Replace, 
           bool force = false, object parameter = null)
    {
        if (_loadSubEntryFunc != null)
        {
            _lastCancellationToken.Cancel(); //Cancel previous load.                
            using (var releaser = await _loadingLock.LockAsync())
            {
                _lastCancellationToken = new CancellationTokenSource();
                if (!_isLoaded || force)
                {
                    if (_clearBeforeLoad)
                        All.Clear();

                    try
                    {                      
                        IsLoading = true;
                        await _loadSubEntryFunc(_isLoaded, parameter)
                        .ContinueWith((prevTask, _) =>
                        {
                            IsLoaded = true;
                            IsLoading = false;
                            if (!prevTask.IsCanceled && !prevTask.IsFaulted)
                            {
                                SetEntries(updateMode, prevTask.Result.ToArray());
                                _lastRefreshTimeUtc = DateTime.UtcNow;
                            }
                        }, _lastCancellationToken,
                        //Code inside ContinueWith() is run in UI thread
                       TaskScheduler.FromCurrentSynchronizationContext());
                    }
                    ...
                }
            }
        }
        return _subItemList;
    }

    public void SetEntries(UpdateMode updateMode = UpdateMode.Replace, 
         params VM[] viewModels)
    {
        switch (updateMode)
        {
            //only update changes.
            case UpdateMode.Update: updateEntries(viewModels); break; 
            //completely reset entries.
            case UpdateMode.Replace: setEntries(viewModels); break;  
            default: throw new NotSupportedException("UpdateMode");
        }
    }
 }

Last edited May 24, 2014 at 10:53 AM by lycj, version 8

Comments

No comments yet.