计算应该等待Dispatcher

时间:2014-09-27 14:17:48

标签: wpf timer wait dispatcher

我使用深度优先搜索(数独求解器)进行算法计算,我希望能够控制ui的更新速率,以显示在计算过程中计算的所有/大多数数字。

我知道这会减慢计算时间,但视觉反馈对我来说更重要。

目前,ui元素(每个元素代表一个数独游戏中的一个字段)通过绑定到绑定模式“oneway”绑定到数据源。

我遇到Dispatcher.Invoke并在数据源发送值更改事件后以低优先级调用该方法,现在可以看到计算进度的一部分至少如何显示,但它仍然是太快。 所以我的问题是,如何管理它,我的计算等待Dispatcher线程在计算下一个(更改的)值之前显示更改的值?

2 个答案:

答案 0 :(得分:2)

您似乎想要使用故事板动画或计时器。

看看 DispatcherTimer ,这听起来像是真正追求的。使用队列/列表,然后以所需的速率逐步将已解决的项目添加到视图中的演示文稿中。在下面的示例中,我假设您有一个名为SudokuItem的clas。 timer_Tick方法将每隔一秒调用一次,由Interval属性控制。我抓住并删除queue中的第一项。您可以在Dispatcher上设置DispatcherTimer属性。那里有一个虚构的SudokuService,模拟项目的生成。

简单示例 - 可以重构。没有异步 - 等待这里等。

protected ISudokuService SudokuService; // Fictional TDD Service for handling generation of Sudoku boards
private List<SudokuItem> queue = new List<SudokuItem>(); 
private DispatcherTimer timer;
private ObservableCollection<SudokuItem> sudokuItems = new ObservableCollection<SudokuItem>();

public ObservableCollection<SudokuItem> SudokuItems
{
    get { return sudokuItems; }
    set
    {
        if (Equals(value, sudokuItems)) return;
        sudokuItems = value;
        OnPropertyChanged();
    }
}

public void StartGeneration()
{
    new Thread(DoYourSudokuMagic).Start(); // Start heavy lifting in a separate thread
    timer = new DispatcherTimer {Interval = new TimeSpan(0, 0, 0, 1), IsEnabled = true};
    timer.Tick += timer_Tick;
    timer.Start();
}

private void DoYourSudokuMagic()
{
    // fetch/generate new SudokuItems somehow... replace this with your code
    SudokoService.StartGeneratingBoard();
    while( SudokuService.HasMoreItems ){
        var items = SudokoService.GetNextItems();
        if (items != null && items.Any())
        {
            lock (queue)
            {
                queue.AddRange(items);
            }
        }
    }
}

// Always invoked on the Dispatcher thread
void timer_Tick(object sender, EventArgs e)
{
    // note that you must call timer.Stop(); when you are done
    lock (queue)
    {
        if (queue.Any())
        {
            SudokuItems.Add(queue.FirstOrDefault());
            queue.RemoveAt(0);
        }
    }
}

您可以使用的另一个计时器,实际上是表单,是 BackgroundWorker 类,在您的情况下它可能是实用的。

Dispatcher.Invoke 阻止,直至获得访问权限。 Dispatcher.BeginInvoke 会继续您的代码,并允许您在访问时更新您的Properties或DependencyProperties。您还应首先检查访问权限,我通常将其放在基类中。

// Note that Dispatcher is an interface in this baseclass for TDD, you have to set it or replace it with your dispatcher.
protected void BeginInvoke(Action action, DispatcherPriority priority)
{
    if (!Dispatcher.CheckAccess())
        Dispatcher.BeginInvoke(action, priority); // doesn't block
    else
        action(); // hey we are on the gui thread already :)
}

protected void Invoke(Action action, DispatcherPriority priority)
{
    if (!Dispatcher.CheckAccess())
        Dispatcher.Invoke(action, priority); // blocks
    else
        action(); // hey we are on the gui thread already :)
}

我希望这有一些帮助,而且我没有理解你的问题。

干杯

答案 1 :(得分:0)

  

所以我的问题是,如何管理它,我的计算等待Dispatcher线程在计算下一个(更改的)值之前显示更改的值?

这不是你想要做的。你真的只想限制更新。

使用UI更新执行CPU绑定操作的标准现代方法是使用Task.RunIProgress<T>。所以,我假设你的CPU绑定代码看起来像这样:

struct SolveProgress { ... }
void Solve(IProgress<SolveProgress> progress)
{
  while (...)
  {
    ...
    if (progress != null)
      progress.Report(new SolveProgress(...));
  }
}
async void Button_Click()
{
  var progress = new Progress<SolveProgress>(value =>
  {
    // Update UI
  });
  await Task.Run(() => Solve(progress));
}

问题是后台工作正在发布大量更新。您想要的是跨各种计算机功能的一致UI体验,每秒不超过几次。就个人而言,我真的很喜欢100ms的更新 - 这足以传达“这个软件真的很难”的感觉,同时仍然是“可见”。如果你想让它更具可读性,那么250ms会更好。

限制是一种基于时间的操作,我喜欢将Rx用于任何基于时间的操作。首先,使用ObserverProgress<T> that converts progress updates into an observable stream替换Progress<T>实现(仅执行每个进度的委托)。这种类型非常简单:

public sealed class ObservableProgress<T> : IObservable<T>, IProgress<T>, IDisposable
{
    private readonly Subject<T> _subject = new Subject<T>();

    void IProgress<T>.Report(T value)
    {
        _subject.OnNext(value);
    }

    public void Dispose()
    {
        _subject.Dispose();
    }

    IDisposable IObservable<T>.Subscribe(IObserver<T> observer)
    {
        return _subject.Subscribe(observer);
    }
}

然后消费代码看起来像这样(在安装NuGet软件包Rx和Rx-WPF之后):

using (var progress = new ObservableProgress<SolveProgress>())
using (progress.ObserveOn(this)
    .Subscribe(value =>
    {
        // Update UI
    }))
{
    await Task.Run(() => Solve(progress));
}

ObservableProgress<T>默认情况下会同步到Progress<T>这样的UI线程,这就是ObserveOn所必需的原因。有了这个,就可以轻松控制进度更新:

using (var progress = new ObservableProgress<SolveProgress>())
using (progress.Sample(TimeSpan.FromMilliseconds(100))
    .ObserveOn(this)
    .Subscribe(value =>
    {
        // Update UI
    }))
{
    await Task.Run(() => Solve(progress));
}

Rx方法的一个好处是限制逻辑是在后台线程上完成的;它只有在有一些UI工作要做时才切换到UI线程。这意味着基于时间的限制实际上是在源头完成的,从而节省了CPU周期。由于它是基于时间的(而不是,例如,使用计数器),无论是在较旧的机器上运行还是在较新的机器上运行,都可以获得可靠,流畅的用户体验。