使用调度程序仅运行一次代码

时间:2014-10-15 14:43:45

标签: c# dispatcher ref

我有一个简单的模式只能运行一次代码。它主要用于在UI上更新某些内容,而在后台可能会经常更改。

private bool _updating;
private void UpdateSomething()
{
    if (!_updating)
    {
        _updating = true;

        Application.Current.Dispatcher.BeginInvoke(new Action(() =>
            {
                _updating = false;
                DoSomething();
            }), DispatcherPriority.Background);
    }
}

我更喜欢将样板代码放在一个简单的方法中:

public static void RunOnce(Action action, ref bool guard)
{
    if (!guard)
    {
        guard = true;

        Application.Current.Dispatcher.BeginInvoke(new Action(() =>
            {
                guard = false;
                action();
            }), DispatcherPriority.Background);
    }
}

并称之为:

void UpdateSomething()
{
    RunOnce(DoSomething, ref _updating);
}

但是,这不起作用,因为您无法在匿名方法中使用ref参数。 有没有解决方法,例如固定ref参数并在执行方法时释放它?

2 个答案:

答案 0 :(得分:2)

你可以这样做:

public static void RunOnce(Action action, ref RunOnceToken token)
{
    if (token == null || token.IsCompleted)
    {
        token = new RunOnceToken(
            Application.Current.Dispatcher.BeginInvoke(
                action,
                DispatcherPriority.Background));
    }
}

public sealed class RunOnceToken : IDisposable
{
    private DispatcherOperation _operation;

    public RunOnceToken(DispatcherOperation operation)
    {
        if (operation != null &&
            operation.Status != DispatcherOperationStatus.Completed &&
            operation.Status != DispatcherOperationStatus.Aborted)
        {
            _operation = operation;
            _operation.Completed += OnCompletedOrAborted;
            _operation.Aborted += OnCompletedOrAborted;
        }
    }

    private void OnCompletedOrAborted(object sender, EventArgs e)
    {
        this.Dispose();
    }

    public bool IsCompleted
    {
        get { return _operation == null; }
    }

    public void Dispose()
    {
        var operation = _operation;
        if (operation == null)
            return;

        _operation = null;

        operation.Completed -= OnCompletedOrAborted;
        operation.Aborted -= OnCompletedOrAborted;
    }
}

您的示例用法将更改为:

private RunOnceToken _updateToken;

private void UpdateSomething()
{
    RunOnce(DoSomething, ref _updateToken);
}

如果你永远不清除你的令牌副本,那真的不重要,因为包装后的DispatcherOperation会在完成时被清除,以避免泄漏action或它捕获的任何值。

如果不明显,这些都不是并发安全的;我假设上面的所有内容都只能从UI线程访问。

一个有用的增强功能可能是为DispatcherPriority添加一个可选的RunOnce参数,以便您可以控制用于计划action的优先级(如果它可能取消已经安排的操作)被安排在较低的优先级。)

答案 1 :(得分:0)

我不知道DispatcherOperation的存在,但是看到Mike Strobel回答了我写的以下代码。我不是100%知道它,但似乎不需要太多样板。

public static class DispatcherExtensions {
    public static int clearInterval = 10_000;

    private static long time => DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond;
    private static long lastClearTime = time;
    private static Dictionary<int, DispatcherOperation> currOperations = new Dictionary<int, DispatcherOperation>();
    private static object sync = new object();

    public static void invokeLastAsync(this Dispatcher d,  Action a, DispatcherPriority p = DispatcherPriority.Background, [CallerFilePath]object key1 = null, [CallerLineNumber]object key2 = null) {
        lock (sync) {
            DispatcherOperation dop;
            var k = key1.GetHashCode() ^ key2.GetHashCode();
            if (currOperations.ContainsKey(k)) {
                dop = currOperations[k];
                currOperations.Remove(k);
                dop.Abort();
            }
            dop = d.BeginInvoke(a, p);
            clearOperations(false);
            currOperations.Add(k, dop);
        }
    }

    public static void clearOperations(bool force = true) {
        var ct = time;
        if (!force && ct - lastClearTime < clearInterval) return;
        var nd = new Dictionary<int, DispatcherOperation>();
        foreach (var ao in currOperations) {
            var s = ao.Value.Status;
            if (s == DispatcherOperationStatus.Completed
                || s == DispatcherOperationStatus.Aborted)
            nd.Add(ao.Key, ao.Value);
        }
        currOperations = nd;
        lastClearTime = ct;
    }

}

基本上,扩展方法将文件路径和行号作为关键字,以将DispacherOperation实例存储在字典中,如果该键已经有一个操作,则它被中止并被新操作替换。定期从不再调用的已完成/已终止的操作中清除字典。

用法很简单:

private int initCount = 0;
private int invokeCount = 0;
private void updateSomething() {
    initCount++;
    view.Dispatcher.invokeLastAsync(() => {
        Console.WriteLine($@"invoked {++invokeCount}/{initCount}");
    });
}

到目前为止,我还没有遇到任何问题。也许其他人可能会看到一些薄弱环节。