7
\$\begingroup\$

I'm playing with async/await, but I have yet to discover a standard method to safely cancel an intensive task. I have tested the following, and it works exactly as intended, though I remain unsure if a standard method exists.

abstract class PageLoader<TPage, TElement> where TPage : Page
{
    private TPage page;
    private Task loader;
    private volatile bool mode;
    private volatile int token;

    public PageLoader(TPage page)
    {
        if (page == null)
        {
            throw new ArgumentNullException("page");
        }

        this.page = page;
        this.mode = true;
    }

    protected TPage Page => page;
    protected bool Mode => mode;

    public void Load(TElement[] items)
    {
        if (!page.Dispatcher.HasThreadAccess)
        {
            throw new InvalidOperationException("The current thread is not authorized.");
        }

        if (loader != null)
        {
            throw new InvalidOperationException("The loader requires reset.");
        }

        if (items == null)
        {
            throw new ArgumentNullException("items");
        }

        int value = token;

        mode = true;
        (loader = new Task(() => AutoLoad(value, items))).Start();
    }

    public async Task<bool> Set(bool retainMode)
    {
        if (!page.Dispatcher.HasThreadAccess)
        {
            throw new InvalidOperationException("The current thread is not authorized.");
        }

        if (mode)
        {
            mode = false;

            if (loader != null)
            {
                await loader;
                loader = null;
                token++;
            }

            if (retainMode)
            {
                mode = true;
            }

            return true;
        }

        return false;
    }

    protected void Commit(Entry entry)
    {
        if (entry == null)
        {
            throw new ArgumentNullException("entry");
        }

        DispatchedHandler handler = () =>
        {
            if (entry.Token == token)
            {
                AutoCommit(entry);
            }
        };

        var action = page.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, handler);
    }

    protected abstract void AutoLoad(int token, TElement[] items);
    protected abstract void AutoCommit(Entry entry);

    protected class Entry
    {
        public readonly int Token;
        public readonly TElement Element;
        public readonly object Data;

        public Entry(int token, TElement element, object data)
        {
            Token = token;
            Element = element;
            Data = data;
        }
    }
}

Tested with:

class GridViewLoader : PageLoader<MainPage, int>
{
    public GridViewLoader(MainPage page) : base(page) { }

    protected override void AutoLoad(int token, int[] items)
    {
        foreach (int item in items)
        {
            if (!Mode)
            {
                break;
            }

            // (Intensive process per item..)

            Commit(new Entry(token, item, null));
        }
    }

    protected override void AutoCommit(Entry entry)
    {
        GridViewItem item = new GridViewItem()
        {
            Content = entry.Element.ToString()
        };

        Page.gridView.Items.Add(item);
    }
}

Usage:

private GridViewLoader gridViewLoader;

private async void queryBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
{
    if (!await gridViewLoader.Set(false))
    {
        return;
    }

    gridView.Items.Clear();

    int[] selection;

    // (Do stuff..)
    selection = new int[] { 1, 2, 3 };

    if (selection != null)
    {
        gridViewLoader.Load(selection);
    }
}
\$\endgroup\$
  • \$\begingroup\$ To be clear, you're asking if there's a better way to do this, right? \$\endgroup\$ – Nic Hartley Feb 1 '16 at 23:45
  • \$\begingroup\$ @QPaysTaxes - Not explicitly, but if there is a standard or better way, I'd sure like to know. I may have improvised a little because I do not know Windows Runtime very well. \$\endgroup\$ – Avenicci Feb 2 '16 at 0:16
6
\$\begingroup\$

You should be using a CancellationToken, which can be generated like:

CancellationTokenSource source = new CancellationTokenSource();
CancellationToken token = source.Token;

Pass this token to the method you wish to potentially cancel and keep a reference outside the method. Then, from the calling method you can check token.IsCancellationRequested at periodic intervals and return, or if you wish to throw, you can call token.ThrowIfCancellationRequested(). To signal a cancellation, you call source.Cancel(), which propagates through to the token and signals a cancellation request. You can also set it up to automatically cancel after a certain amount of time when you create the CancellationTokenSource.

\$\endgroup\$
  • \$\begingroup\$ I'm curious, what does CancellationToken offer besides helpful semantics? \$\endgroup\$ – Avenicci Feb 1 '16 at 21:52
  • \$\begingroup\$ It is essentially a class that handles all the semantics of sharing the information so you just need to handle the actual cancelling and checking if it is canceled. \$\endgroup\$ – 23fc9a62-56de-47fb-97b4-737890 Feb 1 '16 at 21:53
  • \$\begingroup\$ It could replace the mode flag. However, for a real-time UI loading task, dispatching is required to ensure that updates occur on the authorized thread. Any dispatch request is not certain to occur before any kind of cancellation. If they do not, then the results of distinct loading sessions may overlap. The code in OP solves this with an incrementing version named token. \$\endgroup\$ – Avenicci Feb 1 '16 at 22:36
  • \$\begingroup\$ @Avenicci In this case, you can use separate tokens - one generated for each call. \$\endgroup\$ – 23fc9a62-56de-47fb-97b4-737890 Feb 2 '16 at 0:09
  • 1
    \$\begingroup\$ @Avenicci instead of trying to shoehorn a CancelationToken into your current code, it would be best to start from scratch with a simple example that uses it. Might be the best way to break out of the "I already have this code" trap. Using the CancelationToken is the "right way" to do this when using async-await. \$\endgroup\$ – RubberDuck Feb 2 '16 at 10:33
0
\$\begingroup\$

A couple of comments:

  • if (!page.Dispatcher.HasThreadAccess) use: page.Dispatcher.VerifyAccess(); instead, does the same thing but throws the framework exception.

  • Without understanding what problem it solves I think the design is wrong.

    protected override void AutoCommit(Entry entry)
    {
        GridViewItem item = new GridViewItem()
        {
            Content = entry.Element.ToString()
        };
    
        Page.gridView.Items.Add(item); 
        // this can potentially be very expensive to do in a loop 
        // as it forces a layout on each item added
    }
    

Why not do:

<ListView ItemsSource="{Binding Rows}">
    <ListView.View>
        <GridView>
            ...
        </GridView>
    </ListView.View>
</ListView>

And:

public class ViewModel : INotifyPropertyChanged
{
    private ObservableCollection<Row> _rows = new ObservableCollection<Row> ();

    public ObservableCollection<Row> Rows
    {
        get { return _rows; }
        set
        {
            if (Equals(value, _rows))
            {
                return;
            }

            _rows = value;
            OnPropertyChanged();
        }
    }

    private void BulkLoad()
    {
        var newRows = new ObservableCollection<Row>();
        for (int i = 0; i < 1000; i++)
        {
            // pseudo code here
            var item = FetchRow(i);
            newRows.Add(item);
        }

        // swapping out the collection here when done
        Rows = newRows;
    }
  • Regarding cancellation: I agree with answer and comments that you should use CancellationToken. This is the idiomatic way of canceling tasks in .net.
  • Unless fecthing/creating row items is very expensive I would expect the code above to be very fast eliminating the need for task in the first place.

Sorry if I did not understand the code at all :)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.