Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions documentation/Using-the-SDK/Advanced/Adding-Column-Commands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# Adding Column Commands

Column commands let a plugin advertise operations that a host can perform on individual column values. A host may expose these operations in its user interface, such as in a context menu. Support is host-dependent, so a table must remain usable when a host does not expose column commands.

Commands are collected in a `DataColumnCommands` instance and attached to a `DataColumn<T>`, a `HierarchicalDataColumn<T>`, or an individual [column variant](./Adding-Column-Variants.md). Columns without commands expose `DataColumnCommands.Empty` through `IDataColumnWithCommands`.

## Downloading Source Code

`DownloadSourceCodeCommand` is a column command for retrieving the source code represented by a row value. Implement it for the value type projected by the column:

```cs
using System;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Performance.SDK.ColumnCommands;

public sealed class DownloadSourceCommand
: DownloadSourceCodeCommand
{
private static readonly HttpClient httpClient = new HttpClient();

public DownloadSourceCommand()
: base("Download source code")
{
}

public override bool CanExecute(object value, string downloadPath)
{
return value is Uri sourceUri
&& (sourceUri.Scheme == Uri.UriSchemeHttp || sourceUri.Scheme == Uri.UriSchemeHttps)
&& !string.IsNullOrWhiteSpace(downloadPath);
}

public override async Task<DownloadSourceCodeResult[]> ExecuteAsync(
object value,
string downloadPath,
CancellationToken cancellationToken)
{
if (!CanExecute(value, downloadPath))
{
return new[]
{
new DownloadSourceCodeResult(
"The selected value does not identify downloadable source code.",
value as Uri),
};
}

var sourceUri = (Uri)value;
var fileName = Path.GetFileName(sourceUri.LocalPath);
var destinationPath = Path.Combine(downloadPath, fileName);

try
{
Directory.CreateDirectory(downloadPath);

using (HttpResponseMessage response =
await httpClient.GetAsync(sourceUri, cancellationToken).ConfigureAwait(false))
{
response.EnsureSuccessStatusCode();

using (Stream source = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
using (var destination = File.Create(destinationPath))
{
await source.CopyToAsync(destination, 81920, cancellationToken)
.ConfigureAwait(false);
}
}

return new[]
{
new DownloadSourceCodeResult(new Uri(destinationPath)),
};
}
catch (Exception error) when (!(error is OperationCanceledException))
{
return new[]
{
new DownloadSourceCodeResult(error.Message, sourceUri),
};
}
}
}
```

The host passes the projected row value and a local download directory to `CanExecute`. Return `false` for values the command cannot resolve or paths it cannot use. A host should call `CanExecute` before invoking `ExecuteAsync`, but implementations should still validate or safely reject their inputs.

`ExecuteAsync` controls the file layout beneath `downloadPath`. A row value may resolve to multiple source files, such as when it represents a stack frame containing inlined functions. Return one `DownloadSourceCodeResult` for each attempted download. The returned array may contain both successful and failed results: each success contains the URI of a downloaded file, while each failure contains its own error message and, optionally, the corresponding remote source URI. Allow cancellation to propagate as an `OperationCanceledException`.

Command implementations can be invoked on an arbitrary thread. They must be thread-safe, avoid accessing UI-thread state, honor the cancellation token, and use asynchronous I/O for downloads.

## Attaching Commands to a Column

Create the command collection once and pass it to the column:

```cs
var commands = new DataColumnCommands(new DownloadSourceCommand());

tableBuilderWithRowCount.AddColumn(
new DataColumn<Uri>(sourceColumnConfiguration, sourceProjection, commands));
```

The strongly typed `ColumnBuilder<T>` can be used with `ITableBuilderWithRowCount` to add a column to a table:

```cs
tableBuilderWithRowCount.AddColumn(
new ColumnBuilder<Uri>(sourceColumnConfiguration, sourceProjection)
.WithCommands(commands));
```

`ColumnBuilder<T>` is mutable: `WithCommands` returns the same builder instance and may be chained as shown above. This differs from the functional builders used inside `AddColumnWithVariants` callbacks, where every method returns a new builder that must be returned or chained.

## Commands on Column Variants

Commands belong to the specific base column or variant to which they are attached. They are not inherited by related variants. To attach commands to a variant, use the builder overloads that supply a *variant builder* and call `WithCommands` on it. `WithToggleableBuilder` configures a toggle through a `ToggleableVariantBuilder`, and `WithModalBuilder` configures a mode through a `ModalVariantBuilder`:

```cs
tableBuilderWithRowCount.AddColumnWithVariants(
sourceColumnConfiguration,
sourceProjection,
builder => builder.WithToggleableBuilder(
localSourceDescriptor,
localSourceProjection,
variantBuilder => variantBuilder.WithCommands(commands)));
```

For a mode with child variants, chain `WithCommands` with `WithBuilder` on the `ModalVariantBuilder`. `WithBuilder` adds the nested toggles; `WithCommands` attaches the commands to the mode itself:

```cs
return modesBuilder.WithModalBuilder(
sourceModeDescriptor,
sourceProjection,
variantBuilder => variantBuilder
.WithCommands(commands)
.WithBuilder(modeBuilder => modeBuilder.WithToggle(
alternateSourceDescriptor,
alternateSourceProjection)));
```

Hierarchical variants use the same pattern through `WithHierarchicalToggleableBuilder` and `WithHierarchicalModalBuilder`, which additionally take an `ICollectionInfoProvider<T>`. Attach commands only to variants whose projected values the command understands.

## Hierarchical Columns

For a `HierarchicalDataColumn<T>`, the value supplied to `CanExecute` and `ExecuteAsync` is the value displayed for the selected row. When the column uses an `ICollectionAccessProvider<T, TElement>`, this may be a `TElement` rather than the column's declared `T`. A command for a hierarchical column should therefore handle every displayed value type on which it can operate and return `false` from `CanExecute` for unsupported values.

## Host Discovery

A host discovers commands by testing whether an `IDataColumn` implements `IDataColumnWithCommands`, then querying its `Commands` property:

```cs
if (column is IDataColumnWithCommands columnWithCommands &&
columnWithCommands.Commands.TryGetDownloadSourceCodeCommand(out var command) &&
command.CanExecute(value, downloadPath))
{
DownloadSourceCodeResult[] results =
await command.ExecuteAsync(value, downloadPath, cancellationToken);

foreach (DownloadSourceCodeResult result in results)
{
if (result.Success)
{
Open(result.Uri);
}
else
{
ShowError(result.ErrorMessage);
}
}
}
```

Hosts should use `CommandName` as the user-facing action name and process every returned result independently. A failed result's `Uri` may identify the remote source, but it is not a successfully downloaded resource and should not be opened as one.
27 changes: 27 additions & 0 deletions documentation/Using-the-SDK/Advanced/Adding-Column-Variants.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,31 @@ where "With DST" is a child of "Local" but not "UTC." Depending on the SDK drive

If desired, it is also possible to define new sub-modes of a given mode using `WithToggledModes` in the callback.

## Variant Builders

The `WithMode` and `WithToggle` overloads shown above are the simplest way to add a variant when that variant has no commands of its own. When a variant needs to advertise [column commands](./Adding-Column-Commands.md), use the *variant builder* overloads instead. These overloads take a callback that receives a variant builder and returns it after configuration:

* `WithToggleableBuilder` supplies a `ToggleableVariantBuilder`, whose `WithCommands` method attaches commands to the toggle.
* `WithModalBuilder` supplies a `ModalVariantBuilder`, whose `WithCommands` method attaches commands to the mode and whose `WithBuilder` method nests additional toggleable variants underneath the mode (equivalent to the `Func<ToggleableColumnBuilder, ColumnBuilder>` callback taken by `WithMode`).

For example, this adds a "Local" mode that both advertises commands and nests a "With DST" toggle:

```cs
return builder
.WithModes("UTC")
.WithModalBuilder(
new ColumnVariantDescriptor(new Guid("..."), "Local"),
asLocal,
variantBuilder => variantBuilder
.WithCommands(commands)
.WithBuilder(modeBuilder => modeBuilder
.WithToggle(
new ColumnVariantDescriptor(new Guid("..."), "With DST"),
asLocal.Compose(local => FixDST(local)))));
```

Both APIs have hierarchical counterparts, `WithHierarchicalToggleableBuilder` and `WithHierarchicalModalBuilder`, that additionally accept an `ICollectionInfoProvider<T>`. See [Adding Column Commands](./Adding-Column-Commands.md#commands-on-column-variants) for more command examples.

> ❗ The ability to recursively define column variants within a mode makes it possible to define arbitrarily complex trees of column variants. For a better user experience, it is recommended to limit the number of levels of column variants; **if your column has a complex tree of variants, you should consider creating new columns instead**.

# Defining Default Column Variants
Expand Down Expand Up @@ -332,4 +357,6 @@ Registered column variants are exposed as `IDataColumn` instances where

For information on how to obtain `IDataColumn`s for column variants via the SDK Engine, please refer to the "Using Column Variants" section of the [Using the Engine](../Using-the-engine.md#using-column-variants) documentation.

Individual variants can also advertise commands by using the variant builder overloads `WithToggleableBuilder` and `WithModalBuilder` (and their hierarchical counterparts) and calling `WithCommands` on the supplied variant builder. Commands are associated only with the variant to which they are supplied and are not inherited by related variants. See [Adding Column Commands](./Adding-Column-Commands.md#commands-on-column-variants) for examples.


1 change: 1 addition & 0 deletions documentation/Using-the-SDK/Advanced/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ The following collection of documents outline more advanced usages of the SDK.
- [Making your Extensions Disposable](Disposable-Extensions.md)
- [Specifying Compatible DataSources](Specifying-Compatible-DataSources.md)
- [Adding Column Variants](Adding-Column-Variants.md)
- [Adding Column Commands](Adding-Column-Commands.md)
- [Using Plugin Options](Using-Plugin-Options.md)
6 changes: 6 additions & 0 deletions documentation/Using-the-SDK/Building-a-table.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ tableBuilderWithRowCount.AddColumn(this.wordCountColumn, wordCountProjection);

Note that _every_ column a table provides must be added through a call to `ITableBuilderWithRowCount.AddColumn`, even if they're not used in a `TableConfiguration` (see below).

### Adding commands to a column

A column can advertise commands that a supporting host may invoke for a row value. For example, a source-location column can provide a command that downloads the source code represented by the selected value. Commands can be attached when constructing a `DataColumn<T>` or by using `ColumnBuilder<T>.WithCommands`.

See [Adding Column Commands](./Advanced/Adding-Column-Commands.md) for the command contract, download implementation, column variants, and host integration.

### Deprecating a column

Removing a column can break existing saved configurations that reference it: they will silently lose the column, and filters depending on it will become invalid.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using Microsoft.Performance.SDK.Processing;
using Microsoft.Performance.SDK.Processing.ColumnBuilding;
using Microsoft.Performance.SDK.Runtime.ColumnBuilding.Builders;
using Microsoft.Performance.SDK.Runtime.Tests.Fixtures;
using Microsoft.Performance.Testing;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using ColumnConfiguration = Microsoft.Performance.SDK.Processing.ColumnConfiguration;
using ColumnMetadata = Microsoft.Performance.SDK.Processing.ColumnMetadata;
using Projection = Microsoft.Performance.SDK.Processing.Projection;
Expand Down Expand Up @@ -114,6 +114,105 @@ public void WithModes_NullBuilderDoesNotThrow()
Assert.IsTrue(true);
}

[TestMethod]
public void WithToggleableBuilder_NullIdentifierThrows()
{
var builder = CreateSut();

Assert.ThrowsExactly<ArgumentNullException>(() =>
{
builder.WithToggleableBuilder(null, Projection.Constant(1f), variantBuilder => variantBuilder);
});
}

[TestMethod]
public void WithToggleableBuilder_NullProjectionThrows()
{
var builder = CreateSut();

Assert.ThrowsExactly<ArgumentNullException>(() =>
{
builder.WithToggleableBuilder<int>(
new ColumnVariantDescriptor(Guid.NewGuid(), new ColumnVariantProperties { Label = "Foo" }),
null,
variantBuilder => variantBuilder);
});
}

[TestMethod]
public void WithToggleableBuilder_NullBuildVariantThrows()
{
var builder = CreateSut();

Assert.ThrowsExactly<ArgumentNullException>(() =>
{
builder.WithToggleableBuilder(
new ColumnVariantDescriptor(Guid.NewGuid(), new ColumnVariantProperties { Label = "Foo" }),
Projection.Constant(1f),
null);
});
}

[TestMethod]
public void WithHierarchicalToggleableBuilder_NullIdentifierThrows()
{
var builder = CreateSut();

Assert.ThrowsExactly<ArgumentNullException>(() =>
{
builder.WithHierarchicalToggleableBuilder(
null,
Projection.Constant(1f),
new StubCollectionAccessProvider<float>(),
variantBuilder => variantBuilder);
});
}

[TestMethod]
public void WithHierarchicalToggleableBuilder_NullProjectionThrows()
{
var builder = CreateSut();

Assert.ThrowsExactly<ArgumentNullException>(() =>
{
builder.WithHierarchicalToggleableBuilder<float>(
new ColumnVariantDescriptor(Guid.NewGuid(), new ColumnVariantProperties { Label = "Foo" }),
null,
new StubCollectionAccessProvider<float>(),
variantBuilder => variantBuilder);
});
}

[TestMethod]
public void WithHierarchicalToggleableBuilder_NullCollectionInfoThrows()
{
var builder = CreateSut();

Assert.ThrowsExactly<ArgumentNullException>(() =>
{
builder.WithHierarchicalToggleableBuilder<float>(
new ColumnVariantDescriptor(Guid.NewGuid(), new ColumnVariantProperties { Label = "Foo" }),
Projection.Constant(1f),
null,
variantBuilder => variantBuilder);
});
}

[TestMethod]
public void WithHierarchicalToggleableBuilder_NullBuildVariantThrows()
{
var builder = CreateSut();

Assert.ThrowsExactly<ArgumentNullException>(() =>
{
builder.WithHierarchicalToggleableBuilder<float>(
new ColumnVariantDescriptor(Guid.NewGuid(), new ColumnVariantProperties { Label = "Foo" }),
Projection.Constant(1f),
new StubCollectionAccessProvider<float>(),
null);
});
}

private EmptyColumnBuilder CreateSut()
{
return new EmptyColumnBuilder(
Expand Down
Loading
Loading