visual studio 延迟更新绘图装饰

visual studio drawing adornment with lazy update

我正在制作 Visual Studio 装饰扩展。如果至少 2 秒没有用户输入,我想更新装饰。所以我构建了一个 worker 并尝试删除和添加装饰,但 VS 说它无法更新,因为非 ui 线程调用了它。所以我在没有线程的情况下等待然后我的编辑器变得非常滞后(因为 ui 线程等待)

我想知道有没有办法用惰性更新来更新装饰品。 绘制装饰是通过调用 AddAdornment() 完成的,我找不到如何调用 ui 线程来绘制。

下面是我的代码

    internal async void OnLayoutChanged(object sender, TextViewLayoutChangedEventArgs e)
    {
        Print("OnLayoutChanged Called");

        task = Task.Factory.StartNew(() =>
        {
            Print("task Started");
            if (e.NewSnapshot != e.OldSnapshot)
            {
                parseStopwatch.Restart();
                shouldParse = true;
            }

            ParseWork(e);
        });
        await task;


    }


    private async void ParseWork(object param)
    {
        var e = (TextViewLayoutChangedEventArgs)param;
        if (e == null)
        {
            shouldParse = false;
            parseStopwatch.Stop();
            CsharpRegionParser.ParseCs(this.view.TextSnapshot);
            DrawRegionBox();
            return;
        }

        while (shouldParse)
        {
            Task.Delay(10);
            if ((shouldParse && parseStopwatch.ElapsedMilliseconds > 2000) || parseStopwatch.ElapsedMilliseconds > 5000)
            {
                break;
            }

        }
        shouldParse = false;
        parseStopwatch.Stop();
        CsharpRegionParser.ParseCs(this.view.TextSnapshot);
        DrawRequest(e);

        return;

    }

Task.Delay 在您的代码中使用 returns 一项在您延迟时完成的任务。如果你这样称呼它并忽略结果,它并没有按照你的想法去做。你可能的意思是不是像你那样调用 Task.Factory.StartNew,你想要:

var cancellationTokenSource = new CancellationTokenSource();
Task.Delay(2000, cancellationTokenSource.Token).ContinueWith(() => DoWork(), cancellationTokenSource.Token, TaskScheduler.Current).

这有效地表示“启动一个等待 2 秒的计时器,然后一旦它完成 运行 UI 线程上的 DoWork 方法。如果发生更多输入,那么您可以调用 cancellationTokenSource.Cancel() 并再次调用 运行。

另外,我不得不问一下你的类型"CSharpRegionParser"。如果您需要区域信息并且您使用的是 Visual Studio 2015,那么您可以从 Roslyn 获取语法树并且您应该观察工作区更改事件而不是挂钩 LayoutChanged。你也最好将你的系统构建为 tagger/adornment 管理器对,因为它可能更清楚地编写......我不清楚为什么你会在 LayoutChanged 中进行解析逻辑,因为 LayoutChanged 是在期间发生的事情视觉布局,包括滚动、调整大小等

我不确定你为什么被否决,特别是因为在处理扩展时这是一个有趣的问题。

因此,对于您的第一个问题:Visual Studio 具有与 WPF 相同的要求(由于其 COM 依赖性而增加了一些复杂性)。当您不在主 (UI) 线程上时,您无法更新 UI 元素。不幸的是,如果您直接投入并使用用于 WPF 的策略来处理它,您将遇到完全不同的问题(主要是死锁)。

首先,温习一下如何处理 Visual Studio 扩展区中从后台到 UI 线程的切换。我发现 Asynchronous and Multithreaded programming within VS using JoinableTaskFactory 有助于解释。

我不得不用昂贵的解析操作做类似的事情。这很简单。

我的解析器作为 IViewModelTagger 实例的一部分执行并使用以下序列(大致):

  1. 它使用 async void 事件处理程序订阅 ITextBuffer.ChangedLowPriority 事件。
  2. 立即着火,它通过 CancellationToken.Cancel() 调用取消任何正在进行的解析操作。取消令牌被传递到支持它的所有内容(在 Roslyn 中,它在您希望的任何地方都受到支持)。
  3. 它开始解析操作,但在开始之前,我有一个 Task.Delay(200, m_cancellationToken) 调用。我是 200 毫秒,基于我的打字速度以及 Roslyn 的操作对任何昂贵的东西都有 CancellationToken 过载这一事实(我的解析工作也很轻量级)。 YMMV.

我使用的 WPF 组件非常需要 UI 线程,并且它们混合在 IViewModelTaggerIWpfTextViewListener 中。它们足够轻巧,我可以跳过异步它们,但在非常大的 类 上它们可以挂起 UI.

为了处理这个问题,我做了以下事情:

  1. 在 TextViewLayoutChanged 上,我订阅了一个 async void 事件处理程序。
  2. 我先 Task.Run() 昂贵的操作,防止 UI 被阻塞。
  3. 当我最终创建 WPF UI 元素并将它们添加为最终装饰(以及 SDK 中需要它的几个操作)时,我 await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync() 获得UI 线程。

我提到了 "other SDK operations",这很重要。在 SDK 中,除了主线程之外,您不能做几件事(内存现在让我失望,但是如果在后台线程上访问它们,特别是 TextView 的某些部分会失败,而且不会始终如一)。

有更多选项可用于执行 UI 线程的工作(普通 Task.Run 工作,以及 ThreadHelper.JoinableTaskFactory.Run)。我的回答前面链接的 Andrew Arnott post 解释了所有的选择。你会想要完全理解这一点,因为有理由根据任务使用一些而不是其他的。

希望对您有所帮助!