Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ObservationCollection that implements ISupportIncrementalLoading within ViewModel inside PCL using MVVM architecture for WinRT & WP8/WinPRT support

I have my ViewModels inside a PCL, because I'm developing a Windows 8.1 and Windows Phone app in parallel. I have a list of things inside my ViewModel as an ObservableCollection.

I have a GridView inside a Page inside a Windows 8.1 project. I wish to incrementally load items from my list of things in my ViewModel. Normally I would implement ISupportIncrementalLoading inside a custom subclass of ObservableCollection, however, given that my ViewModel is inside a PCL, ISupportIncrementalLoading is not available (it's not supported by WP8).

So my question is, does anyone have any suggestions as to how I can create some kind of converter, adapter or abstraction layer between the ItemsSource binding of the GridView and the Observable Things property of my ViewModel which would implement ISupportIncrementalLoading and then call the ViewModel's LoadMoreThings method and pass the items to the GridView.

I feel like there is some solution, such as creating a custom ISupportIncrementalLoading inside my View Models PCL and then have the View layer delegate to it.

thanks

like image 858
krisdyson Avatar asked Nov 27 '13 12:11

krisdyson


1 Answers

In the end, I used the abstract factory pattern. The facts are:

  • You cannot reference the View layer from the PCL ViewModel layer, as the VM layer should not be concerned with the View layer. One of the benefits of this is that you can create another consumer of the ViewModel layer without any dependencies on the target platform. e.g. create a Windows 8 and Windows Phone 8 app off the back of one ViewModel library PCL project.

  • The GridView is a WinRT component which can bind to an ObservableCollection<T>. ObservableCollection<T> is available inside both the View layer and ViewModel layer. If you want to support incremental loading inside your app (which is a must for large datasets), then you need to create a special subclass of ObservableCollection<T> that implements ISupportIncrementalLoading. What we want to do is just create that subclass inside the ViewModel project and you're done. But we cannot do this because ISupportIncrementalLoading is only available inside a WinRT project.

This problem can be resolved by using the abstract factory pattern. All the ViewModel really wants is an ObservableCollection<T>, but the View layer demands an ObservableCollection that implements ISupportIncrementalLoading. So the answer is to define an interface in the ViewModel layer that gives the ViewModel exact what it wants; let's call it IPortabilityFactory. Then in the View layer define a concrete implementation of IPortabilityFactory called PortabilityFactory. Use an IoC in the View layer to map IPortabilityFactory (ViewModel interface) to PortabilityFactory (View layer concrete impl.).

On the constructor of the ViewModel class, have an IPortabilityFactory instance injected. Now the ViewModel has a factory that will give it an ObservableCollection<T> instance.

Now instead of calling new ObservableCollection<Thing>() in the ViewModel you call factory.GetIncrementalCollection<Thing>(...).

OK, so we're done with the ViewModel layer; now we need that custom implementation of the ObservableCollection<T>. It's called IncrementalLoadingCollection and it's defined in the View layer. It implements ISupportIncrementalLoading.

Here's the code and explanations, together with an implementation of ISupportIncrementalLoading.

In the ViewModel layer (PCL) I have an abstract factory interface.

public interface IPortabilityFactory
{
    ObservableCollection<T> GetIncrementalCollection<T>(int take, Func<int, Task<List<T>>> loadMoreItems, Action onBatchStart, Action<List<T>> onBatchComplete);
}

In the View layer (Windows 8 app, in this case) I implement a concrete factory like this:

public class PortabilityFactory : IPortabilityFactory 
{
    public ObservableCollection<T> GetIncrementalCollection<T>(int take, Func<int, Task<List<T>>> loadMoreItems, Action onBatchStart, Action<List<T>> onBatchComplete)
    {
        return new IncrementalLoadingCollection<T>(take, loadMoreItems, onBatchStart, onBatchComplete);
    }
}

Again, inside the View layer, I happen to use Unity for my IoC. When the IoC is created, I map IPortabilityFactory (in PCL) to PortabilityFactory (in View layer; the app project).

Container.RegisterType<IPortabilityFactory, PortabilityFactory>(new ContainerControlledLifetimeManager());

We now need to create a subclass of ObservableCollection and here's the code:

public class IncrementalLoadingCollection<T> 
        : ObservableCollection<T>, ISupportIncrementalLoading
    {
        private Func<int, Task<List<T>>> _loadMoreItems = null;
        private Action<List<T>> _onBatchComplete = null;
        private Action _onBatchStart = null;


        /// <summary>
        /// How many records to currently skip
        /// </summary>
        private int Skip { get; set; }

        /// <summary>
        /// The max number of items to get per batch
        /// </summary>
        private int Take { get; set; }

        /// <summary>
        /// The number of items in the last batch retrieved
        /// </summary>
        private int VirtualCount { get; set; }

        /// <summary>
        /// .ctor
        /// </summary>
        /// <param name="take">How many items to take per batch</param>
        /// <param name="loadMoreItems">The load more items function</param>
        public IncrementalLoadingCollection(int take, Func<int, Task<List<T>>> loadMoreItems, Action onBatchStart, Action<List<T>> onBatchComplete)
        {
            Take = take;
            _loadMoreItems = loadMoreItems;
            _onBatchStart = onBatchStart;
            _onBatchComplete = onBatchComplete;
            VirtualCount = take;
        }

        /// <summary>
        /// Returns whether there are more items (if the current batch size is equal to the amount retrieved then YES)
        /// </summary>
        public bool HasMoreItems
        {
            get { return this.VirtualCount >= Take; }
        }

        public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
        {
           CoreDispatcher dispatcher = Window.Current.Dispatcher;
           _onBatchStart(); // This is the UI thread

           return Task.Run<LoadMoreItemsResult>(
                async () =>
                {
                    var result = await _loadMoreItems(Skip);
                    this.VirtualCount = result.Count;
                    Skip += Take;

                    await dispatcher.RunAsync(
                        CoreDispatcherPriority.Normal,
                        () =>
                        {
                            foreach (T item in result) this.Add(item);
                            _onBatchComplete(result); // This is the UI thread
                        });

                    return new LoadMoreItemsResult() { Count = (uint)result.Count };

                }).AsAsyncOperation<LoadMoreItemsResult>();
        }
    }

IncrementalLoadingCollection's constructor asks for four parameters which will be supplied by the ViewModel via the factory:

  • take - this is the page size

  • loadMoreItems - this is a delegate reference to a function inside the ViewModel that will retrieve the next batch of items (importantly, this function will not run inside the UI thread)

  • onBatchStart - this will be invoked just before the loadMoreItems method is called. This allows me to make changes to properties on the ViewModel that may impact the View. e.g., have an observable IsProcessing property which is bound to the Visibility property of a progress bar.

  • onBatchComplete - this will be invoked just after the retrieval of the latest batch and pass the items in. Crucially, this function will invoke on the UI thread.

In the ViewModel layer, my ViewModel has a constructor on it which accepts a IPortabilityFactory object:

public const string IsProcessingPropertyName = "IsProcessing";

private bool _isProcessing = false;
public bool IsProcessing
{
    get
    {
        return _isProcessing;
    }
    set
    {
        if (_isProcessing == value)
        {
            return;
        }
        RaisePropertyChanging(IsProcessingPropertyName);
        _isProcessing = value;
        RaisePropertyChanged(IsProcessingPropertyName);
        }
}

    private IPortabilityFactory _factory = null;
    public ViewModel(IPortabilityFactory factory)
    {
        _factory = factory;
        Initialize();
    }


    private async void Initialize()
    {
        Things = _factory.GetIncrementalCollection<Thing>(10, LoadThings, 
           () => IsProcessing = true, BatchLoaded);
    }

    private void BatchLoaded(List<Thing> batch)
    {
        IsProcessing = false;
    }

    private async Task<List<Thing>> LoadThings(int skip)
    {
        var items = await _service.GetThings(skip, 10 /*page size*/);
        return items;
    }

I hope this helps someone.

like image 147
krisdyson Avatar answered Nov 15 '22 11:11

krisdyson