高级可视化工具方案

如果你正在编写自己的自定义数据可视化工具,本文提供的信息可能会很有用,尤其是在被可视化的对象或可视化工具 UI 本身很复杂的情况下。

以下示例基于具有两个项目的 Visual Studio 解决方案。 第一个对应于 .NET Framework 4.7.2 项目,该项目将作为 UI 逻辑的调试器端组件。 第二个是 .NET Standard 2.0 项目,它将成为调试对象端组件,以便可以在 .NET Core 应用程序中使用。

调试程序端包含 WPF 窗口,其可能包含一个在加载时可见的不确定 ProgressBar 控件以及两个名为 DataLabelErrorLabel 的标签(这两个标签在加载时都会折叠)。 进度栏从它试图可视化的对象中获取全部数据后,它将被折叠,可视化工具随即显示带有相关信息的数据标签。 如果出现错误,则进度栏也会隐藏,但使用错误标签显示错误消息。 下面是一个简单的示例:

<Window x:Class="AdvancedVisualizer.DebuggerSide.VisualizerDialog"
        xmlns:local="clr-namespace:AdvancedVisualizer.DebuggerSide">

    <Grid>
        <StackPanel x:Name="progressControl" Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
            <ProgressBar IsIndeterminate="True" Width="200" Height="10"/>
            <Label HorizontalAlignment="Center">Loading...</Label>
        </StackPanel>
        <Label x:Name="DataLabel" Visibility="Collapsed" HorizontalAlignment="Center" VerticalAlignment="Center" />
        <Label x:Name="ErrorLabel" Visibility="Collapsed" HorizontalAlignment="Center" VerticalAlignment="Center" />
    </Grid>

</Window>

为了简化示例,VisualizerDialog 交互逻辑具有一个简单的构造函数,用于向其 Loaded 事件注册事件处理程序。 该事件处理程序负责提取数据并根据每个示例进行更改,因此在每个部分中单独显示。

public partial class VisualizerDialog : Window
{
    private AdvancedVisualizerViewModel ViewModel => (AdvancedVisualizerViewModel)this.DataContext;

    public VisualizerDialog()
    {
        InitializeComponent();

        this.Loaded += VisualizerLoaded;
    }

    public void VisualizerLoaded(object sender, RoutedEventArgs e)
    {
        // Logic to fetch and show the data in the UI.
    }
}

调试器端有一个称为 AdvancedVisualizerViewModel 的视图模型,用于处理可视化工具的逻辑,以便从调试对象端提取其数据。 这将根据每个示例的不同而变化,因此将在每个部分中单独显示。 最后,可视化工具入口点如下所示:

[assembly: DebuggerVisualizer(typeof(AdvancedVisualizer.DebuggerSide.AdvancedVisualizer), typeof(AdvancedVisualizer.DebuggeeSide.CustomVisualizerObjectSource), Target = typeof(VerySlowObject), Description = "Very Slow Object Visualizer")]
namespace AdvancedVisualizer.DebuggerSide
{
    public class AdvancedVisualizer : DialogDebuggerVisualizer
    {
        protected override void Show(IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider)
        {
            IAsyncVisualizerObjectProvider asyncObjectProvider = (IAsyncVisualizerObjectProvider)objectProvider;

            AdvancedVisualizerViewModel viewModel = new AdvancedVisualizerViewModel(asyncObjectProvider);
            Window advancedVisualizerWindow = new VisualizerDialog() { DataContext = viewModel };

            advancedVisualizerWindow.ShowDialog();
        }
    }
}

注意

在前面的代码中,我们正在对 objectProvider 执行强制转换。 这种强制转换背后的原因在使用新的异步 API 部分中进行了说明。

调试对象端因示例而异,因此在每个部分中单独显示。

使用新的异步 API

出于兼容性原因,被 DialogDebuggerVisualizer 覆盖的 Show 方法仍会接收类型为 IVisualizerObjectProvider 的对象提供程序实例。 但是,此类型还实现 IAsyncVisualizerObjectProvider 接口。 因此,使用 VS 2022 17.2 及更高版本可以安全地强制转换它。 该提供程序添加了 IVisualizerObjectProvider2 中存在的方法的异步实现。

处理较长的序列化时间

在某些情况下,调用 IAsyncVisualizerObjectProvider 上的默认 GetDeserializableObjectAsync 方法将导致可视化工具引发超时异常。 自定义数据可视化器操作最多仅允许五秒,以确保 Visual Studio 保持响应。 也就是说,每次调用 GetDeserializableObjectAsyncReplaceDataAsyncTransferDeserializableObjectAsync 等都必须在达到时间限制之前完成执行,否则 VS 会引发异常。 由于没有计划为更改此时间约束提供支持,因此可视化工具实现必须处理对象序列化时间超过 5 秒的情况。 为了正确处理这种情况,建议可视化工具按区块或分段处理从调试对象端组件到调试程序端组件的数据传递。

注意

可以从 VSSDK-Extensibility-Samples 存储库下载用于获取这些代码片段的项目。

例如,假设你有一个名为 VerySlowObject 的复杂对象,其中包含许多必须处理并复制到调试器端可视化工具组件的字段和属性。 在这些属性中,你有 VeryLongList,根据 VerySlowObject 的实例,它可能会在五秒内完成序列化,或者需要更多时间。

public class VerySlowObject
{
    // More properties...

    // One of the properties we want to visualize.
    public List<SomeRandomObject> VeryLongList { get; }

    // More properties...
}

这就是为什么你需要创建自己的调试对象端组件,它是一个派生自 VisualizerObjectSource 并替代 TransferData 方法的类。

public class CustomVisualizerObjectSource : VisualizerObjectSource
{
    public override void TransferData(object obj, Stream fromVisualizer, Stream toVisualizer)
    {
        // Serialize `obj` into the `toVisualizer` stream...
    }
}

此时你有两种选择;可以添加自定义“命令”和“响应”类型,让可视化工具在两个组件之间协调数据传输的状态;或者让 VisualizerObjectSource 自行处理。 如果对象只有一个相同类型的简单集合(并且你希望将每个元素发送到 UI),则建议采用第二种方法,因为调试对象端仅返回集合段,直到到达末尾。 如果有多个不同的部分,或者可能只想返回整个对象的一部分,则第一种方法可能更容易。 考虑到你决定采用第二种方法,你将在调试对象端项目中创建以下类。

[Serializable]
public class GetVeryLongListCommand
{
    public int StartOffset { get; }

    // Constructor...
}

[Serializable]
public class GetVeryLongListResponse
{
    public string[] Values { get; }
    public bool IsComplete { get; }

    // Constructor...
}

有了帮助程序类,你的视图模型就可以获取一个异步方法来提取数据并进行处理,以便在 UI 中显示。 在此示例中,将其命名为 GetDataAsync

public async Task<string> GetDataAsync()
{
    List<string> verySlowObjectList = new List<string>();

    // Consider the possibility that we might timeout when fetching the data.
    bool isRequestComplete;

    do
    {
        // Send the command requesting more elements from the collection and process the response.
        IDeserializableObject deserializableObject = await m_asyncObjectProvider.TransferDeserializableObjectAsync(new GetVeryLongListCommand(verySlowObjectList.Count), CancellationToken.None);
        GetVeryLongListResponse response = deserializableObject.ToObject<GetVeryLongListResponse>();

        // Check if a timeout occurred. If it did we try fetching more data again.
        isRequestComplete = response.IsComplete;

        // If no timeout occurred and we did not get all the elements we asked for, then we reached the end
        // of the collection and we can safely exit the loop.
        verySlowObjectList.AddRange(response.Values);
    }
    while (!isRequestComplete);

    // Do some processing of the data before showing it to the user.
    string valuesToBeShown = ProcessList(verySlowObjectList);
    return valuesToBeShown;
}

private string ProcessList(List<string> verySlowObjectList)
{
    // Do some processing of the data before showing it to the user...
}

GetDataAsync 方法在循环中创建 GetVeryLongListCommand 实例,将其发送到调试对象端进行处理,并根据响应重新发送该实例以获取其余数据,或者结束循环,因为它已经提取了所有数据。 调试对象端的 TransferData 方法可以按如下方式处理请求。

public override void TransferData(object obj, Stream fromVisualizer, Stream toVisualizer)
{
    // Serialize `obj` into the `toVisualizer` stream...

    // Start the timer so that we can stop processing the request if it's are taking too long.
    long startTime = Environment.TickCount;

    if (obj is VerySlowObject slowObject)
    {
        bool isComplete = true;

        // Read the supplied command
        fromVisualizer.Seek(0, SeekOrigin.Begin);
        IDeserializableObject deserializableObject = GetDeserializableObject(fromVisualizer);
        GetVeryLongListCommand command = deserializableObject.ToObject<GetVeryLongListCommand>();

        List<string> returnValues = new List<string>();

        for (int i = (int)command.StartOffset; i < slowObject.VeryLongList?.Count; i++)
        {
            // If the call takes more than 3 seconds, just return what we have received so far and fetch the remaining data on a posterior call.
            if ((Environment.TickCount - startTime) > 3_000)
            {
                isComplete = false;
                break;
            }

            // This call takes a considerable amount of time...
            returnValues.Add(slowObject.VeryLongList[i].ToString());
        }

        GetVeryLongListResponse response = new GetVeryLongListResponse(returnValues.ToArray(), isComplete);
        Serialize(toVisualizer, response);
    }
    else
    {
        // Handle failure case...
    }
}

视图模型拥有所有数据后,可视化工具的 VisualizerLoaded 事件处理程序将调用该视图模型,以便它可以请求数据。

public void VisualizerLoaded(object sender, RoutedEventArgs e)
{
    _ = Dispatcher.InvokeAsync(async () =>
    {
        try
        {
            string data = await this.ViewModel.GetDataAsync();

            this.DataLabel.Visibility = Visibility.Visible;
            this.DataLabel.Content = data;
        }
        catch
        {
            this.ErrorLabel.Content = "Error getting data.";
        }
        finally
        {
            this.progressControl.Visibility = Visibility.Collapsed;
        }
    });
}

注意

请务必处理请求可能发生的错误,并在此处通知用户。

通过这些更改,可视化工具便应该能处理需要很长时间才能从调试对象端序列化到调试器端的对象。