2

When user resizes window some long text should be updated, but if the thread is already running it should be stopped and started over with new width parameter.

int myWidth;
private CancellationTokenSource tokenSource2 = new CancellationTokenSource();
private CancellationToken ct = new CancellationToken();

void container_Loaded(object sender, RoutedEventArgs e)
{
  ct = tokenSource2.Token;
  MyFunction();
}

        void container_SizeChanged(object sender, SizeChangedEventArgs e)
        {
          if (tokenSource2.Token.IsCancellationRequested)
            MyFunction();
          else
            tokenSource2.Cancel();
        }

        void MyFunction()            
        {
           myWidth = GetWidth();
           Task.Factory.StartNew(() =>
           {  
              string s;    
              for (int i=0;i<1000,i++){
                  s=s+Functionx(myWidth);
                  ct.ThrowIfCancellationRequested();
              }
              this.Dispatcher.BeginInvoke(new Action(() => { 
                   ShowText(s); 
              }));
           },tokenSource2.Token)
           .ContinueWith(t => {
              if (t.IsCanceled)
              {
                tokenSource2 = new CancellationTokenSource(); //reset token
                MyFunction(); //restart
              };
           });
        }

What now is happening is when I resize window I see text iteratively updating next several seconds as if old threads were not canceled. What am I doing wrong?

Daniel
  • 1,064
  • 2
  • 13
  • 30
  • 3
    You never actually cancel the thread. Every resize increment starts another task. – Hans Passant Feb 06 '14 at 19:01
  • You are right. It seems to me the only thing I can do is have each of these many objects one global Tasks which I can check on resize if running and then task=null, task = new Task.Factory... What do you think about it @HansPassant – Daniel Feb 06 '14 at 19:52
  • `if (tokenSource2.Token.IsCancellationRequested) tokenSource2.Cancel();` - this calls `Cancel()` only if `IsCancellationRequested` **is already `true`**, which makes no sense. Did you mean `if (!tokenSource2.Token.IsCancellationRequested) tokenSource2.Cancel();` ? – noseratio Feb 07 '14 at 05:14
  • 1
    There is no reason to write: `if (ct.IsCancellationRequested) ct.ThrowIfCancellationRequested();` The whole point of `ThrowIfCancellationRequested` is that it does the check to see if it is cancelled. Just write `ct.ThrowIfCancellationRequested();` and omit the `if`. – Servy Feb 07 '14 at 15:09
  • @Noseratio yes, I switched those two lines, but it is still wrong. Would it make sense to have one global Width variable which Task would check if different and then restart? If completed without restarting it would change global variable isFinished=true. Would it be a problem if accessed from two threads at the same time? – Daniel Feb 07 '14 at 15:12

2 Answers2

1

I don't think using global variables is a good idea in this case. Here's how I would do it by adding cancellation logic to my AsyncOp class from a related question. This code also implements the IProgress pattern and throttles the ViewModel updates.

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace Wpf_21611292
{
    /// <summary>
    /// Cancel and restarts an asynchronous operation
    /// </summary>
    public class AsyncOp<T>
    {
        readonly object _lock = new object();
        Task<T> _pendingTask = null;
        CancellationTokenSource _pendingCts = null;

        public Task<T> CurrentTask
        {
            get { lock (_lock) return _pendingTask; }
        }

        public bool IsPending
        {
            get { lock (_lock) return _pendingTask != null && !_pendingTask.IsCompleted; }
        }

        public bool IsCancellationRequested
        {
            get { lock (_lock) return _pendingCts != null && _pendingCts.IsCancellationRequested; }
        }

        public void Cancel()
        {
            lock (_lock)
            {
                if (_pendingTask != null && !_pendingTask.IsCompleted && !_pendingCts.IsCancellationRequested)
                    _pendingCts.Cancel();
            }
        }

        public Task<T> Run(
            Func<CancellationToken, Task<T>> routine,
            CancellationToken token = default,
            bool startAsync = false,
            bool continueAsync = false,
            TaskScheduler taskScheduler = null)
        {
            Task<T> previousTask = null;
            CancellationTokenSource previousCts = null;

            Task<T> thisTask = null;
            CancellationTokenSource thisCts = null;

            async Task<T> routineWrapper()
            {
                // await the old task
                if (previousTask != null)
                {
                    if (!previousTask.IsCompleted && !previousCts.IsCancellationRequested)
                    {
                        previousCts.Cancel();
                    }
                    try
                    {
                        await previousTask;
                    }
                    catch (Exception ex)
                    {
                        if (!(previousTask.IsCanceled || ex is OperationCanceledException))
                            throw;
                    }
                }

                // run and await this task
                return await routine(thisCts.Token);
            };

            Task<Task<T>> outerTask;

            lock (_lock)
            {
                previousTask = _pendingTask;
                previousCts = _pendingCts;

                thisCts = CancellationTokenSource.CreateLinkedTokenSource(token);

                outerTask = new Task<Task<T>>(
                    routineWrapper,
                    thisCts.Token,
                    continueAsync ?
                        TaskCreationOptions.RunContinuationsAsynchronously :
                        TaskCreationOptions.None);

                thisTask = outerTask.Unwrap();

                _pendingTask = thisTask;
                _pendingCts = thisCts;
            }

            var scheduler = taskScheduler;
            if (scheduler == null)
            {
                scheduler = SynchronizationContext.Current != null ?
                    TaskScheduler.FromCurrentSynchronizationContext() :
                    TaskScheduler.Default;
            }

            if (startAsync)
                outerTask.Start(scheduler);
            else
                outerTask.RunSynchronously(scheduler);

            return thisTask;
        }
    }

    /// <summary>
    /// ViewModel
    /// </summary>
    public class ViewModel : INotifyPropertyChanged
    {
        string _width;

        string _text;

        public string Width
        {
            get
            {
                return _width;
            }
            set
            {
                if (_width != value)
                {
                    _width = value;
                    this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Width)));
                }
            }
        }

        public string Text
        {
            get
            {
                return _text;
            }
            set
            {
                if (_text != value)
                {
                    _text = value;
                    this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Text)));
                }
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }

    /// <summary>
    /// MainWindow
    /// </summary>
    public partial class MainWindow : Window
    {
        ViewModel _model = new ViewModel { Text = "Starting..." };

        AsyncOp<DBNull> _asyncOp = new AsyncOp<DBNull>();

        CancellationTokenSource _workCts = new CancellationTokenSource();

        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = _model;

            this.Loaded += MainWindow_Loaded;
            this.SizeChanged += MainWindow_SizeChanged;
        }

        void MainWindow_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            _asyncOp.Run(WorkAsync, _workCts.Token);
        }

        void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            _asyncOp.Run(WorkAsync, _workCts.Token);
        }

        async Task<DBNull> WorkAsync(CancellationToken token)
        {
            const int limit = 200000000;
            var throttle = TimeSpan.FromMilliseconds(200);

            // update ViewModel's Width
            _model.Width = $"Width: {this.Width:#.##}";

            // update ViewModel's Text using IProgress pattern 
            // and throttling updates
            IProgress<int> progress = new Progress<int>(i =>
            {
                _model.Text = $"{(double)i / (limit - 1)* 100:0.}%";
            });

            var stopwatch = new Stopwatch();
            stopwatch.Start();

            // do some CPU-intensive work
            await Task.Run(() =>
            {
                int i;
                for (i = 0; i < limit; i++)
                {
                    if (stopwatch.Elapsed > throttle)
                    {
                        progress.Report(i);
                        stopwatch.Restart();
                    }
                    if (token.IsCancellationRequested)
                        break;
                }
                progress.Report(i);
            }, token);

            return DBNull.Value;
        }
    }
}

XAML:

<Window x:Class="Wpf_21611292.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <TextBox Width="200" Height="30" Text="{Binding Path=Width}"/>
        <TextBox Width="200" Height="30" Text="{Binding Path=Text}"/>
    </StackPanel>
</Window>

It uses async/await, so if you target .NET 4.0, you'd need Microsoft.Bcl.Async and VS2012+. Alternatively, you can convert async/await to ContinueWith, which is a bit tedious, but always possible (that's more or less what the C# 5.0 compiler does behind the scene).

noseratio
  • 59,932
  • 34
  • 208
  • 486
  • Can you explain me how does it work? I thought if you send token while starting task it will not start at all. I don't understand where it cancels previous task and starts new one. Also, it should iterate until 1000 anyway, just passing width to another function. – Daniel Feb 09 '14 at 19:04
  • @MiloS, note the `for` loop inside `MyFunctionAsync`, that's where it iterates and that's where you can call your `MyFunctionX` instead. It works by canceling the previous instance of the tasks and asynchronously waiting for the cancelation to complete. The best way to see how it happens is to step it through in the debugger. – noseratio Feb 09 '14 at 20:30
  • Installing Microsoft.Bcl.Async it said it is not required/supported for .net 4.5 and also AsyncOp is not supported. By the way, is your solution creating two tasks for each of the controls? – Daniel Feb 10 '14 at 15:07
  • @MiloS, you don't need `Microsoft.Bcl.Async` if you target .NET 4.5, I said you'd need it for .NET 4.0. Copy `AsyncOp` code [from here](http://stackoverflow.com/a/21392669/1768303). *By the way, is your solution creating two tasks for each of the controls?* Not sure what you're asking. `AsyncOp` is used per *operaton* (like your `MyFunction`). You can use as many per control as needed. – noseratio Feb 10 '14 at 20:17
  • It does restart function on resize, but UI is completely unresponsive during resizing. (Each of these is a TextBlock within ListBox and it is fine if I have less than 10 TextBlocks) – Daniel Feb 10 '14 at 21:20
  • Note how I use `await Task.Run(()` inside `MyFunctionAsync` in my answer, that's what keeps the UI responsive. Your're probably not doing that. Anyway, this is as much as can help with this. – noseratio Feb 10 '14 at 21:23
  • I did copy/paste but still blocks UI. Tried also var text = await Task.Run(() => { for (var i = 0; i < 100000000; i++) {if (token.IsCancellationRequested) return i.ToString(); } return "Finished" + result.ToString();} Even with 5 items UI jumps while resizing – Daniel Feb 10 '14 at 21:41
  • @MiloS, I've updated the answer with complete and functional WPF app code, showing that the UI is perfectly fluent during re-sizes. I hope it will help you to fix your app, but I can't spend any more time on this. – noseratio Feb 10 '14 at 22:14
  • 1
    Thank you very much for your time and help. It does work great in your test app! I will share in the next comment why it did not work in my app. Cheers! – Daniel Feb 10 '14 at 22:47
1

You should use Microsoft's Reactive Framework (aka Rx) - NuGet System.Reactive.Windows.Threading (for WPF) and add using System.Reactive.Linq; - then you can do this:

    public MainWindow()
    {
        InitializeComponent();

        _subscription =
            Observable
                .FromEventPattern<SizeChangedEventHandler, SizeChangedEventArgs>(
                    h => container.SizeChanged += h,
                    h => container.SizeChanged -= h)
                .Select(e => GetWidth())
                .Select(w => Observable.Start(
                        () => String.Concat(Enumerable.Range(0, 1000).Select(n => Functionx(w)))))
                .Switch()
                .ObserveOnDispatcher()
                .Subscribe(t => ShowText(t));
    }

    private IDisposable _subscription = null;

That's all the code needed.

This responds to the SizeChanged event, calls GetWidth and then pushes the Functionx to another thread. It uses Switch() to always switch to the latest SizeChanged and then ignores any in-flight code. It pushes the result to the dispatcher and then calls ShowText.

If you need to close the form or stop the subscription running just call _subscription.Dispose().

Simple.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • It's really beautiful and concise. Rx.NET is something I've always wanted to use in a production project, but yet haven't gotten to. – noseratio Jul 13 '20 at 02:35
  • 1
    @noseratio - It's beautiful, concise, and **powerful**. I prefer it to Tasks for almost everything. Observables are also awaitable. They are awesome! – Enigmativity Jul 13 '20 at 02:58
  • I hope I'll get to use it more in my future projects, especially now that Microsoft is embracing the MVU pattern for XAML-based UI. – noseratio Jul 13 '20 at 03:04
  • @noseratio - Model/View/Update? How does that differ from MVC? – Enigmativity Jul 13 '20 at 04:26
  • Basically this is a variation of React pattern (i.e, one way bindings and immutable state) done in WPF or the future MAUI .NET, see [Introducing .NET Multi-platform App UI](https://devblogs.microsoft.com/dotnet/introducing-net-multi-platform-app-ui/), you could scroll down to "MVU". – noseratio Jul 13 '20 at 06:05
  • Also, [this discussion](https://github.com/dotnet/maui/issues/118) is an interesting read :) – noseratio Jul 13 '20 at 06:20