diff --git a/documentation/Using-the-SDK/Advanced/Adding-Column-Commands.md b/documentation/Using-the-SDK/Advanced/Adding-Column-Commands.md new file mode 100644 index 000000000..b85d68272 --- /dev/null +++ b/documentation/Using-the-SDK/Advanced/Adding-Column-Commands.md @@ -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`, a `HierarchicalDataColumn`, 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 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(sourceColumnConfiguration, sourceProjection, commands)); +``` + +The strongly typed `ColumnBuilder` can be used with `ITableBuilderWithRowCount` to add a column to a table: + +```cs +tableBuilderWithRowCount.AddColumn( + new ColumnBuilder(sourceColumnConfiguration, sourceProjection) + .WithCommands(commands)); +``` + +`ColumnBuilder` 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`. Attach commands only to variants whose projected values the command understands. + +## Hierarchical Columns + +For a `HierarchicalDataColumn`, the value supplied to `CanExecute` and `ExecuteAsync` is the value displayed for the selected row. When the column uses an `ICollectionAccessProvider`, 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. \ No newline at end of file diff --git a/documentation/Using-the-SDK/Advanced/Adding-Column-Variants.md b/documentation/Using-the-SDK/Advanced/Adding-Column-Variants.md index af66e8955..f40b3cf3e 100644 --- a/documentation/Using-the-SDK/Advanced/Adding-Column-Variants.md +++ b/documentation/Using-the-SDK/Advanced/Adding-Column-Variants.md @@ -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` 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`. 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 @@ -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. + diff --git a/documentation/Using-the-SDK/Advanced/README.md b/documentation/Using-the-SDK/Advanced/README.md index b50e75873..a0300f8c2 100644 --- a/documentation/Using-the-SDK/Advanced/README.md +++ b/documentation/Using-the-SDK/Advanced/README.md @@ -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) \ No newline at end of file diff --git a/documentation/Using-the-SDK/Building-a-table.md b/documentation/Using-the-SDK/Building-a-table.md index 8ed78fbe0..61926b8a0 100644 --- a/documentation/Using-the-SDK/Building-a-table.md +++ b/documentation/Using-the-SDK/Building-a-table.md @@ -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` or by using `ColumnBuilder.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. diff --git a/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/EmptyColumnBuilderTests.cs b/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/EmptyColumnBuilderTests.cs index 94ec8c397..66a8af567 100644 --- a/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/EmptyColumnBuilderTests.cs +++ b/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/EmptyColumnBuilderTests.cs @@ -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; @@ -114,6 +114,105 @@ public void WithModes_NullBuilderDoesNotThrow() Assert.IsTrue(true); } + [TestMethod] + public void WithToggleableBuilder_NullIdentifierThrows() + { + var builder = CreateSut(); + + Assert.ThrowsExactly(() => + { + builder.WithToggleableBuilder(null, Projection.Constant(1f), variantBuilder => variantBuilder); + }); + } + + [TestMethod] + public void WithToggleableBuilder_NullProjectionThrows() + { + var builder = CreateSut(); + + Assert.ThrowsExactly(() => + { + builder.WithToggleableBuilder( + new ColumnVariantDescriptor(Guid.NewGuid(), new ColumnVariantProperties { Label = "Foo" }), + null, + variantBuilder => variantBuilder); + }); + } + + [TestMethod] + public void WithToggleableBuilder_NullBuildVariantThrows() + { + var builder = CreateSut(); + + Assert.ThrowsExactly(() => + { + builder.WithToggleableBuilder( + new ColumnVariantDescriptor(Guid.NewGuid(), new ColumnVariantProperties { Label = "Foo" }), + Projection.Constant(1f), + null); + }); + } + + [TestMethod] + public void WithHierarchicalToggleableBuilder_NullIdentifierThrows() + { + var builder = CreateSut(); + + Assert.ThrowsExactly(() => + { + builder.WithHierarchicalToggleableBuilder( + null, + Projection.Constant(1f), + new StubCollectionAccessProvider(), + variantBuilder => variantBuilder); + }); + } + + [TestMethod] + public void WithHierarchicalToggleableBuilder_NullProjectionThrows() + { + var builder = CreateSut(); + + Assert.ThrowsExactly(() => + { + builder.WithHierarchicalToggleableBuilder( + new ColumnVariantDescriptor(Guid.NewGuid(), new ColumnVariantProperties { Label = "Foo" }), + null, + new StubCollectionAccessProvider(), + variantBuilder => variantBuilder); + }); + } + + [TestMethod] + public void WithHierarchicalToggleableBuilder_NullCollectionInfoThrows() + { + var builder = CreateSut(); + + Assert.ThrowsExactly(() => + { + builder.WithHierarchicalToggleableBuilder( + new ColumnVariantDescriptor(Guid.NewGuid(), new ColumnVariantProperties { Label = "Foo" }), + Projection.Constant(1f), + null, + variantBuilder => variantBuilder); + }); + } + + [TestMethod] + public void WithHierarchicalToggleableBuilder_NullBuildVariantThrows() + { + var builder = CreateSut(); + + Assert.ThrowsExactly(() => + { + builder.WithHierarchicalToggleableBuilder( + new ColumnVariantDescriptor(Guid.NewGuid(), new ColumnVariantProperties { Label = "Foo" }), + Projection.Constant(1f), + new StubCollectionAccessProvider(), + null); + }); + } + private EmptyColumnBuilder CreateSut() { return new EmptyColumnBuilder( diff --git a/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ModalColumnBuilderTests.cs b/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ModalColumnBuilderTests.cs index fa7835da0..b9363a6dd 100644 --- a/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ModalColumnBuilderTests.cs +++ b/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ModalColumnBuilderTests.cs @@ -1,14 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Collections.Generic; 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 System.Collections.Generic; using ColumnConfiguration = Microsoft.Performance.SDK.Processing.ColumnConfiguration; using ColumnMetadata = Microsoft.Performance.SDK.Processing.ColumnMetadata; using Projection = Microsoft.Performance.SDK.Processing.Projection; @@ -117,11 +117,104 @@ public void WithDefaultMode_UnregisteredGuidThrows() }); } + [TestMethod] + public void WithModalBuilder_NullIdentifierThrows() + { + var builder = CreateSut(); + + Assert.ThrowsExactly(() => + { + builder.WithModalBuilder(null, modeProjection, variantBuilder => variantBuilder); + }); + } + + [TestMethod] + public void WithModalBuilder_NullProjectionThrows() + { + var builder = CreateSut(); + + Assert.ThrowsExactly(() => + { + builder.WithModalBuilder(modeDescriptor, null, variantBuilder => variantBuilder); + }); + } + + [TestMethod] + public void WithModalBuilder_NullBuildVariantThrows() + { + var builder = CreateSut(); + + Assert.ThrowsExactly(() => + { + builder.WithModalBuilder(modeDescriptor, modeProjection, null); + }); + } + + [TestMethod] + public void WithHierarchicalModalBuilder_NullIdentifierThrows() + { + var builder = CreateSut(); + + Assert.ThrowsExactly(() => + { + builder.WithHierarchicalModalBuilder( + null, + modeProjection, + new StubCollectionAccessProvider(), + variantBuilder => variantBuilder); + }); + } + + [TestMethod] + public void WithHierarchicalModalBuilder_NullProjectionThrows() + { + var builder = CreateSut(); + + Assert.ThrowsExactly(() => + { + builder.WithHierarchicalModalBuilder( + modeDescriptor, + null, + new StubCollectionAccessProvider(), + variantBuilder => variantBuilder); + }); + } + + [TestMethod] + public void WithHierarchicalModalBuilder_NullCollectionInfoThrows() + { + var builder = CreateSut(); + + Assert.ThrowsExactly(() => + { + builder.WithHierarchicalModalBuilder( + modeDescriptor, + modeProjection, + null, + variantBuilder => variantBuilder); + }); + } + + [TestMethod] + public void WithHierarchicalModalBuilder_NullBuildVariantThrows() + { + var builder = CreateSut(); + + Assert.ThrowsExactly(() => + { + builder.WithHierarchicalModalBuilder( + modeDescriptor, + modeProjection, + new StubCollectionAccessProvider(), + null); + }); + } + private ModalColumnBuilder CreateSut() { return new ModalColumnWithModesBuilder( new TestColumnVariantsProcessor(), - new List(), + new List(), new DataColumn( new ColumnConfiguration( new ColumnMetadata(Guid.NewGuid(), "foo")), Projection.Constant(1)), diff --git a/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ModalVariantBuilderTests.cs b/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ModalVariantBuilderTests.cs new file mode 100644 index 000000000..67ab33936 --- /dev/null +++ b/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ModalVariantBuilderTests.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Performance.SDK.ColumnCommands; +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 ColumnConfiguration = Microsoft.Performance.SDK.Processing.ColumnConfiguration; +using ColumnMetadata = Microsoft.Performance.SDK.Processing.ColumnMetadata; +using Projection = Microsoft.Performance.SDK.Processing.Projection; + +namespace Microsoft.Performance.SDK.Runtime.Tests.ColumnBuilding; + +[TestClass] +[UnitTest] +public class ModalVariantBuilderTests +{ + private static readonly ColumnVariantDescriptor modeDescriptor = new(Guid.NewGuid(), new ColumnVariantProperties { Label = "Mode" }); + private static readonly IProjection modeProjection = Projection.Constant(1); + + [TestMethod] + public void WithCommands_ReturnsSameBuilderInstance() + { + var builder = new ModalVariantBuilder(modeDescriptor, modeProjection); + var result = builder.WithCommands(new DataColumnCommands(new StubDownloadCommand())); + Assert.AreSame(builder, result); + } + + [TestMethod] + public void WithCommands_CommandsPreservedInVariant() + { + var commands = new DataColumnCommands(new StubDownloadCommand()); + + var variant = new ModalVariantBuilder(modeDescriptor, modeProjection) + .WithCommands(commands) + .CreateVariant(CreateBaseColumn()); + + var columnWithCommands = variant.Column as IDataColumnWithCommands; + Assert.IsNotNull(columnWithCommands); + Assert.AreSame(commands, columnWithCommands.Commands); + } + + [TestMethod] + public void NoCommands_ExposesEmptyCommands() + { + var variant = new ModalVariantBuilder(modeDescriptor, modeProjection) + .CreateVariant(CreateBaseColumn()); + + var columnWithCommands = variant.Column as IDataColumnWithCommands; + Assert.IsNotNull(columnWithCommands); + Assert.AreSame(DataColumnCommands.Empty, columnWithCommands.Commands); + } + + [TestMethod] + public void WithBuilder_BuilderPreservedInVariant() + { + Func nested = b => b; + + var variant = new ModalVariantBuilder(modeDescriptor, modeProjection) + .WithBuilder(nested) + .CreateVariant(CreateBaseColumn()); + + Assert.AreSame(nested, variant.Builder); + } + + [TestMethod] + public void WithCommandsThenWithBuilder_BothApplied() + { + var commands = new DataColumnCommands(new StubDownloadCommand()); + Func nested = b => b; + + var variant = new ModalVariantBuilder(modeDescriptor, modeProjection) + .WithCommands(commands) + .WithBuilder(nested) + .CreateVariant(CreateBaseColumn()); + + var columnWithCommands = variant.Column as IDataColumnWithCommands; + Assert.IsNotNull(columnWithCommands); + Assert.AreSame(commands, columnWithCommands.Commands); + Assert.AreSame(nested, variant.Builder); + } + + [TestMethod] + public void Hierarchical_WithCommands_CommandsPreservedInVariant() + { + var commands = new DataColumnCommands(new StubDownloadCommand()); + + var variant = new ModalVariantBuilder(modeDescriptor, modeProjection, new StubCollectionAccessProvider()) + .WithCommands(commands) + .CreateVariant(CreateBaseColumn()); + + Assert.IsInstanceOfType(variant.Column, typeof(HierarchicalDataColumn)); + + var columnWithCommands = variant.Column as IDataColumnWithCommands; + Assert.IsNotNull(columnWithCommands); + Assert.AreSame(commands, columnWithCommands.Commands); + } + + private static IDataColumn CreateBaseColumn() + { + return new DataColumn( + new ColumnConfiguration(new ColumnMetadata(Guid.NewGuid(), "base")), + Projection.Constant(0)); + } + + private sealed class StubDownloadCommand + : DownloadSourceCodeCommand + { + public StubDownloadCommand() + : base("Stub") + { + } + + public override bool CanExecute(object value, string downloadPath) + { + return false; + } + + public override Task ExecuteAsync( + object value, + string downloadPath, + CancellationToken cancellationToken) + { + return Task.FromResult(Array.Empty()); + } + } +} diff --git a/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ToggleableVariantBuilderTests.cs b/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ToggleableVariantBuilderTests.cs new file mode 100644 index 000000000..80e67c6c2 --- /dev/null +++ b/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ToggleableVariantBuilderTests.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Performance.SDK.ColumnCommands; +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 ColumnConfiguration = Microsoft.Performance.SDK.Processing.ColumnConfiguration; +using ColumnMetadata = Microsoft.Performance.SDK.Processing.ColumnMetadata; +using Projection = Microsoft.Performance.SDK.Processing.Projection; + +namespace Microsoft.Performance.SDK.Runtime.Tests.ColumnBuilding; + +[TestClass] +[UnitTest] +public class ToggleableVariantBuilderTests +{ + private static readonly ColumnVariantDescriptor toggleDescriptor = new(Guid.NewGuid(), new ColumnVariantProperties { Label = "Toggle" }); + private static readonly IProjection toggleProjection = Projection.Constant(1); + + [TestMethod] + public void WithCommands_ReturnsSameBuilderInstance() + { + var builder = new ToggledVariantBuilder(toggleDescriptor, toggleProjection); + var result = builder.WithCommands(new DataColumnCommands(new StubDownloadCommand())); + Assert.AreSame(builder, result); + } + + [TestMethod] + public void WithCommands_CommandsPreservedInVariant() + { + var commands = new DataColumnCommands(new StubDownloadCommand()); + + var variant = new ToggledVariantBuilder(toggleDescriptor, toggleProjection) + .WithCommands(commands) + .CreateVariant(CreateBaseColumn()); + + var columnWithCommands = variant.Column as IDataColumnWithCommands; + Assert.IsNotNull(columnWithCommands); + Assert.AreSame(commands, columnWithCommands.Commands); + } + + [TestMethod] + public void NoCommands_ExposesEmptyCommands() + { + var variant = new ToggledVariantBuilder(toggleDescriptor, toggleProjection) + .CreateVariant(CreateBaseColumn()); + + var columnWithCommands = variant.Column as IDataColumnWithCommands; + Assert.IsNotNull(columnWithCommands); + Assert.AreSame(DataColumnCommands.Empty, columnWithCommands.Commands); + } + + [TestMethod] + public void Hierarchical_WithCommands_CommandsPreservedInVariant() + { + var commands = new DataColumnCommands(new StubDownloadCommand()); + + var variant = new ToggledVariantBuilder(toggleDescriptor, toggleProjection, new StubCollectionAccessProvider()) + .WithCommands(commands) + .CreateVariant(CreateBaseColumn()); + + Assert.IsInstanceOfType(variant.Column, typeof(HierarchicalDataColumn)); + + var columnWithCommands = variant.Column as IDataColumnWithCommands; + Assert.IsNotNull(columnWithCommands); + Assert.AreSame(commands, columnWithCommands.Commands); + } + + private static IDataColumn CreateBaseColumn() + { + return new DataColumn( + new ColumnConfiguration(new ColumnMetadata(Guid.NewGuid(), "base")), + Projection.Constant(0)); + } + + private sealed class StubDownloadCommand + : DownloadSourceCodeCommand + { + public StubDownloadCommand() + : base("Stub") + { + } + + public override bool CanExecute(object value, string downloadPath) + { + return false; + } + + public override Task ExecuteAsync( + object value, + string downloadPath, + CancellationToken cancellationToken) + { + return Task.FromResult(Array.Empty()); + } + } +} diff --git a/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ToggledColumnBuilderTests.cs b/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ToggledColumnBuilderTests.cs index 7b7dc7dbd..ebd94d2a2 100644 --- a/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ToggledColumnBuilderTests.cs +++ b/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ToggledColumnBuilderTests.cs @@ -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; @@ -98,6 +98,105 @@ public void WithToggledModes_NullBuilderDoesNotThrow() Assert.IsTrue(true); } + [TestMethod] + public void WithToggleableBuilder_NullIdentifierThrows() + { + var builder = CreateSut(); + + Assert.ThrowsExactly(() => + { + builder.WithToggleableBuilder(null, Projection.Constant(1f), variantBuilder => variantBuilder); + }); + } + + [TestMethod] + public void WithToggleableBuilder_NullProjectionThrows() + { + var builder = CreateSut(); + + Assert.ThrowsExactly(() => + { + builder.WithToggleableBuilder( + new ColumnVariantDescriptor(Guid.NewGuid(), new ColumnVariantProperties { Label = "Foo" }), + null, + variantBuilder => variantBuilder); + }); + } + + [TestMethod] + public void WithToggleableBuilder_NullBuildVariantThrows() + { + var builder = CreateSut(); + + Assert.ThrowsExactly(() => + { + builder.WithToggleableBuilder( + new ColumnVariantDescriptor(Guid.NewGuid(), new ColumnVariantProperties { Label = "Foo" }), + Projection.Constant(1f), + null); + }); + } + + [TestMethod] + public void WithHierarchicalToggleableBuilder_NullIdentifierThrows() + { + var builder = CreateSut(); + + Assert.ThrowsExactly(() => + { + builder.WithHierarchicalToggleableBuilder( + null, + Projection.Constant(1f), + new StubCollectionAccessProvider(), + variantBuilder => variantBuilder); + }); + } + + [TestMethod] + public void WithHierarchicalToggleableBuilder_NullProjectionThrows() + { + var builder = CreateSut(); + + Assert.ThrowsExactly(() => + { + builder.WithHierarchicalToggleableBuilder( + new ColumnVariantDescriptor(Guid.NewGuid(), new ColumnVariantProperties { Label = "Foo" }), + null, + new StubCollectionAccessProvider(), + variantBuilder => variantBuilder); + }); + } + + [TestMethod] + public void WithHierarchicalToggleableBuilder_NullCollectionInfoThrows() + { + var builder = CreateSut(); + + Assert.ThrowsExactly(() => + { + builder.WithHierarchicalToggleableBuilder( + new ColumnVariantDescriptor(Guid.NewGuid(), new ColumnVariantProperties { Label = "Foo" }), + Projection.Constant(1f), + null, + variantBuilder => variantBuilder); + }); + } + + [TestMethod] + public void WithHierarchicalToggleableBuilder_NullBuildVariantThrows() + { + var builder = CreateSut(); + + Assert.ThrowsExactly(() => + { + builder.WithHierarchicalToggleableBuilder( + new ColumnVariantDescriptor(Guid.NewGuid(), new ColumnVariantProperties { Label = "Foo" }), + Projection.Constant(1f), + new StubCollectionAccessProvider(), + null); + }); + } + protected virtual ToggleableColumnBuilder CreateSut() { var baseColumn = new DataColumn( @@ -109,7 +208,7 @@ protected virtual ToggleableColumnBuilder CreateSut() new ColumnMetadata(Guid.NewGuid(), "toggle")), Projection.Constant(1)); return new ToggledColumnBuilder( - new []{ new ToggledColumnBuilder.AddedToggle(new ColumnVariantDescriptor(Guid.NewGuid(), new ColumnVariantProperties { Label = "Foo" }), initialToggle) }, + new[] { new ToggleableVariant(new ColumnVariantDescriptor(Guid.NewGuid(), new ColumnVariantProperties { Label = "Foo" }), initialToggle) }, baseColumn, new TestColumnVariantsProcessor()); } diff --git a/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ToggledColumnWithToggledModesBuilderTests.cs b/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ToggledColumnWithToggledModesBuilderTests.cs index f1fa68f99..7fe1b9e2c 100644 --- a/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ToggledColumnWithToggledModesBuilderTests.cs +++ b/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ToggledColumnWithToggledModesBuilderTests.cs @@ -1,7 +1,6 @@ // 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; @@ -9,6 +8,7 @@ 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; @@ -31,7 +31,7 @@ protected override ToggleableColumnBuilder CreateSut() new ColumnMetadata(Guid.NewGuid(), "toggle")), Projection.Constant(1)); return new ToggledColumnWithToggledModesBuilder( - new []{ new ToggledColumnBuilder.AddedToggle(new ColumnVariantDescriptor(Guid.NewGuid(), new ColumnVariantProperties { Label = "Foo" }), initialToggle) }, + new[] { new ToggleableVariant(new ColumnVariantDescriptor(Guid.NewGuid(), new ColumnVariantProperties { Label = "Foo" }), initialToggle) }, baseColumn, new TestColumnVariantsProcessor(), new ModesBuilderCallbackInvoker((modesBuilder) => modesBuilder, baseColumn), diff --git a/src/Microsoft.Performance.SDK.Runtime.Tests/TableBuilderTests.cs b/src/Microsoft.Performance.SDK.Runtime.Tests/TableBuilderTests.cs index 148cba848..b5b04d3eb 100644 --- a/src/Microsoft.Performance.SDK.Runtime.Tests/TableBuilderTests.cs +++ b/src/Microsoft.Performance.SDK.Runtime.Tests/TableBuilderTests.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Performance.SDK.Processing; +using Microsoft.Performance.SDK.Processing.ColumnBuilding; using Microsoft.Performance.Testing; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -87,6 +88,29 @@ public void AddColumnReturnsBuilder() Assert.AreEqual(this.Sut, this.Sut.AddColumn(column)); } + [TestMethod] + [UnitTest] + public void AddColumnBuilderBuildsColumnAndReturnsBuilder() + { + var columnBuilder = new ColumnBuilder( + new ColumnMetadata(Guid.NewGuid(), "name"), + new UIHints { Width = 200, }, + Projection.CreateUsingFuncAdaptor(i => "test")); + + var result = this.Sut.AddColumn(columnBuilder); + + Assert.AreEqual(this.Sut, result); + Assert.AreEqual("test", ((DataColumn)this.Sut.Columns.Single()).Project(0)); + } + + [TestMethod] + [UnitTest] + public void AddColumnBuilderDoesNotAllowNulls() + { + Assert.ThrowsExactly( + () => this.Sut.AddColumn((ColumnBuilder)null)); + } + [TestMethod] [UnitTest] public void ReplaceColumnOldNullThrows() diff --git a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/CallbackInvokers/ModesBuilderCallbackInvoker.cs b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/CallbackInvokers/ModesBuilderCallbackInvoker.cs index d36ac590d..b757980a9 100644 --- a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/CallbackInvokers/ModesBuilderCallbackInvoker.cs +++ b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/CallbackInvokers/ModesBuilderCallbackInvoker.cs @@ -1,12 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Collections.Generic; using Microsoft.Performance.SDK.Processing; using Microsoft.Performance.SDK.Processing.ColumnBuilding; using Microsoft.Performance.SDK.Runtime.ColumnBuilding.Processors; using Microsoft.Performance.SDK.Runtime.ColumnVariants.TreeNodes; +using System; +using System.Collections.Generic; namespace Microsoft.Performance.SDK.Runtime.ColumnBuilding.Builders.CallbackInvokers; @@ -47,7 +47,7 @@ public bool TryGet(out IColumnVariantsTreeNode builtVariantsTreeNode) var processor = new BuiltColumnVariantReflector(); var builder = new ModalColumnWithModesBuilder( processor, - new List(), + new List(), this.baseColumn, null); diff --git a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/EmptyColumnBuilder.cs b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/EmptyColumnBuilder.cs index 59bb8c962..a9c6cec71 100644 --- a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/EmptyColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/EmptyColumnBuilder.cs @@ -1,13 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Collections.Generic; using Microsoft.Performance.SDK.Processing; using Microsoft.Performance.SDK.Processing.ColumnBuilding; using Microsoft.Performance.SDK.Runtime.ColumnBuilding.Builders.CallbackInvokers; using Microsoft.Performance.SDK.Runtime.ColumnBuilding.Processors; using Microsoft.Performance.SDK.Runtime.ColumnVariants.TreeNodes; +using System; +using System.Collections.Generic; namespace Microsoft.Performance.SDK.Runtime.ColumnBuilding.Builders; @@ -53,7 +53,7 @@ public override ToggleableColumnBuilder WithToggle( return new ToggledColumnBuilder( [ - new ToggledColumnBuilder.AddedToggle(toggleDescriptor, + new ToggleableVariant(toggleDescriptor, new DataColumn( new ColumnConfiguration(this.baseColumn.Configuration) { @@ -77,7 +77,7 @@ public override ToggleableColumnBuilder WithHierarchicalToggle( return new ToggledColumnBuilder( [ - new ToggledColumnBuilder.AddedToggle(toggleDescriptor, + new ToggleableVariant(toggleDescriptor, new HierarchicalDataColumn( new ColumnConfiguration(this.baseColumn.Configuration) { @@ -90,6 +90,34 @@ public override ToggleableColumnBuilder WithHierarchicalToggle( processor); } + /// + public override ToggleableColumnBuilder WithToggleableBuilder( + ColumnVariantDescriptor toggleDescriptor, + IProjection projection, + Func buildVariant) + { + Guard.NotNull(toggleDescriptor, nameof(toggleDescriptor)); + Guard.NotNull(projection, nameof(projection)); + Guard.NotNull(buildVariant, nameof(buildVariant)); + + return CreateFromBuilder(toggleDescriptor, projection, null, buildVariant); + } + + /// + public override ToggleableColumnBuilder WithHierarchicalToggleableBuilder( + ColumnVariantDescriptor toggleDescriptor, + IProjection projection, + ICollectionInfoProvider collectionProvider, + Func buildVariant) + { + Guard.NotNull(toggleDescriptor, nameof(toggleDescriptor)); + Guard.NotNull(projection, nameof(projection)); + Guard.NotNull(collectionProvider, nameof(collectionProvider)); + Guard.NotNull(buildVariant, nameof(buildVariant)); + + return CreateFromBuilder(toggleDescriptor, projection, collectionProvider, buildVariant); + } + /// public override ColumnBuilder WithToggledModes( string toggleText, @@ -98,7 +126,7 @@ public override ColumnBuilder WithToggledModes( Guard.NotNull(toggleText, nameof(toggleText)); return new ToggledColumnWithToggledModesBuilder( - new List(), + new List(), baseColumn, processor, new ModesBuilderCallbackInvoker(builder, baseColumn), @@ -122,7 +150,7 @@ public override ModalColumnBuilder WithModes( return new ModalColumnWithModesBuilder( processor, [ - new ModalColumnWithModesBuilder.AddedMode( + new ModalVariant( new ColumnVariantDescriptor(baseColumn.Configuration.Metadata.Guid, baseProjectionProperties), baseColumn, builder), @@ -130,4 +158,21 @@ public override ModalColumnBuilder WithModes( baseColumn, null); } + + private ToggleableColumnBuilder CreateFromBuilder( + ColumnVariantDescriptor toggleDescriptor, + IProjection projection, + ICollectionInfoProvider collectionProvider, + Func buildVariant) + { + ToggleableVariantBuilder variantBuilder = new ToggledVariantBuilder(toggleDescriptor, projection, collectionProvider); + variantBuilder = buildVariant(variantBuilder); + + return new ToggledColumnBuilder( + [ + variantBuilder.CreateVariant(this.baseColumn), + ], + baseColumn, + processor); + } } \ No newline at end of file diff --git a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs index 4b02b17f3..9e6d19d12 100644 --- a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs +++ b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs @@ -1,15 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; using Microsoft.Performance.SDK.Processing; using Microsoft.Performance.SDK.Processing.ColumnBuilding; using Microsoft.Performance.SDK.Runtime.ColumnBuilding.Builders.CallbackInvokers; using Microsoft.Performance.SDK.Runtime.ColumnBuilding.Processors; using Microsoft.Performance.SDK.Runtime.ColumnVariants.TreeNodes; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; namespace Microsoft.Performance.SDK.Runtime.ColumnBuilding.Builders; @@ -21,14 +21,9 @@ internal class ModalColumnWithModesBuilder { private readonly IDataColumn baseColumn; private readonly IColumnVariantsProcessor processor; - private readonly List addedModes; + private readonly List addedModes; private readonly int? defaultModeIndex; - internal record AddedMode( - ColumnVariantDescriptor Descriptor, - IDataColumn column, - Func builder); - /// /// Initializes a new instance of the /// @@ -46,7 +41,7 @@ internal record AddedMode( /// public ModalColumnWithModesBuilder( IColumnVariantsProcessor processor, - List addedModes, + List addedModes, IDataColumn baseColumn, int? defaultModeIndex) { @@ -66,16 +61,16 @@ internal override void Commit() } List modeVariants = new(); - foreach (AddedMode mode in this.addedModes) + foreach (ModalVariant mode in this.addedModes) { - var callbackInvoker = new ModeBuilderCallbackInvoker(mode.builder, this.baseColumn); + var callbackInvoker = new ModeBuilderCallbackInvoker(mode.Builder, this.baseColumn); IColumnVariantsTreeNode subVariantsTreeNode = NullColumnVariantsTreeNode.Instance; if (callbackInvoker.TryGet(out var builtVariant)) { subVariantsTreeNode = builtVariant; } - modeVariants.Add(new ModeColumnVariantsTreeNode(mode.Descriptor, mode.column, subVariantsTreeNode)); + modeVariants.Add(new ModeColumnVariantsTreeNode(mode.Descriptor, mode.Column, subVariantsTreeNode)); } var variant = new ModesColumnVariantsTreeNode(modeVariants, this.defaultModeIndex ?? 0); @@ -99,12 +94,12 @@ public override ModalColumnBuilder WithMode( Guard.NotNull(modeDescriptor, nameof(modeDescriptor)); Guard.NotNull(projection, nameof(projection)); - AddedMode newMode = new( + ModalVariant newMode = new( modeDescriptor, new DataColumn( new ColumnConfiguration(this.baseColumn.Configuration) { - Metadata = new ColumnMetadata(this.baseColumn.Configuration.Metadata) { Name = modeDescriptor.Properties.ColumnName ?? this.baseColumn.Configuration.Metadata.Name}, + Metadata = new ColumnMetadata(this.baseColumn.Configuration.Metadata) { Name = modeDescriptor.Properties.ColumnName ?? this.baseColumn.Configuration.Metadata.Name }, }, projection), builder); @@ -112,6 +107,7 @@ public override ModalColumnBuilder WithMode( return WithMode(newMode); } + /// public override ModalColumnBuilder WithHierarchicalMode( ColumnVariantDescriptor modeDescriptor, IProjection projection, @@ -120,6 +116,7 @@ public override ModalColumnBuilder WithHierarchicalMode( return WithHierarchicalMode(modeDescriptor, projection, collectionProvider, null); } + /// public override ModalColumnBuilder WithHierarchicalMode( ColumnVariantDescriptor modeDescriptor, IProjection projection, @@ -130,12 +127,12 @@ public override ModalColumnBuilder WithHierarchicalMode( Guard.NotNull(projection, nameof(projection)); Guard.NotNull(collectionProvider, nameof(collectionProvider)); - AddedMode newMode = new( + ModalVariant newMode = new( modeDescriptor, new HierarchicalDataColumn( new ColumnConfiguration(this.baseColumn.Configuration) { - Metadata = new ColumnMetadata(this.baseColumn.Configuration.Metadata) { Name = modeDescriptor.Properties.ColumnName ?? this.baseColumn.Configuration.Metadata.Name}, + Metadata = new ColumnMetadata(this.baseColumn.Configuration.Metadata) { Name = modeDescriptor.Properties.ColumnName ?? this.baseColumn.Configuration.Metadata.Name }, }, projection, collectionProvider), @@ -144,6 +141,38 @@ public override ModalColumnBuilder WithHierarchicalMode( return WithMode(newMode); } + public override ModalColumnBuilder WithModalBuilder( + ColumnVariantDescriptor modeDescriptor, + IProjection projection, + Func buildVariant) + { + Guard.NotNull(modeDescriptor, nameof(modeDescriptor)); + Guard.NotNull(projection, nameof(projection)); + Guard.NotNull(buildVariant, nameof(buildVariant)); + + ModalVariantBuilder variantBuilder = new ModalVariantBuilder(modeDescriptor, projection); + variantBuilder = buildVariant(variantBuilder); + + return WithMode(variantBuilder.CreateVariant(this.baseColumn)); + } + + public override ModalColumnBuilder WithHierarchicalModalBuilder( + ColumnVariantDescriptor modeDescriptor, + IProjection projection, + ICollectionInfoProvider collectionProvider, + Func buildVariant) + { + Guard.NotNull(modeDescriptor, nameof(modeDescriptor)); + Guard.NotNull(projection, nameof(projection)); + Guard.NotNull(collectionProvider, nameof(collectionProvider)); + Guard.NotNull(buildVariant, nameof(buildVariant)); + + ModalVariantBuilder variantBuilder = new ModalVariantBuilder(modeDescriptor, projection, collectionProvider); + variantBuilder = buildVariant(variantBuilder); + + return WithMode(variantBuilder.CreateVariant(this.baseColumn)); + } + /// public override ColumnBuilder WithDefaultMode(Guid modeIdentifierGuid) { @@ -176,7 +205,7 @@ public override ColumnBuilder WithDefaultMode(Guid modeIdentifierGuid) index); } - private ModalColumnBuilder WithMode(AddedMode newMode) + private ModalColumnBuilder WithMode(ModalVariant newMode) { return new ModalColumnWithModesBuilder( this.processor, @@ -184,4 +213,4 @@ private ModalColumnBuilder WithMode(AddedMode newMode) this.baseColumn, this.defaultModeIndex); } -} \ No newline at end of file +} diff --git a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalVariantBuilder`1.cs b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalVariantBuilder`1.cs new file mode 100644 index 000000000..af7427b7d --- /dev/null +++ b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalVariantBuilder`1.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using Microsoft.Performance.SDK.ColumnCommands; +using Microsoft.Performance.SDK.Processing; +using Microsoft.Performance.SDK.Processing.ColumnBuilding; +using System; + +namespace Microsoft.Performance.SDK.Runtime.ColumnBuilding.Builders; + +internal class ModalVariantBuilder + : ModalVariantBuilder +{ + private readonly ColumnVariantDescriptor modeDescriptor; + private readonly IProjection projection; + private readonly ICollectionInfoProvider? collectionProvider; + + private DataColumnCommands? commands = null; + private Func? builder = null; + + public ModalVariantBuilder( + ColumnVariantDescriptor modeDescriptor, + IProjection projection) + : this(modeDescriptor, projection, null) + { + } + + public ModalVariantBuilder( + ColumnVariantDescriptor modeDescriptor, + IProjection projection, + ICollectionInfoProvider? collectionProvider) + { + this.modeDescriptor = modeDescriptor; + this.projection = projection; + this.collectionProvider = collectionProvider; + } + + public override ModalVariantBuilder WithCommands(DataColumnCommands commands) + { + this.commands = commands; + return this; + } + + public override ModalVariantBuilder WithBuilder(Func builder) + { + this.builder = builder; + return this; + } + + internal override ModalVariant CreateVariant(IDataColumn baseColumn) + { + if (this.collectionProvider is null) + { + return CreateModalVariant(baseColumn); + } + + return CreateHierarchicalModalVariant(baseColumn); + } + + private ModalVariant CreateModalVariant(IDataColumn baseColumn) + { + ModalVariant newMode = new( + this.modeDescriptor, + new DataColumn( + new ColumnConfiguration(baseColumn.Configuration) + { + Metadata = new ColumnMetadata(baseColumn.Configuration.Metadata) { Name = this.modeDescriptor.Properties.ColumnName ?? baseColumn.Configuration.Metadata.Name }, + }, + this.projection, + this.commands), + this.builder); + + return newMode; + } + + private ModalVariant CreateHierarchicalModalVariant(IDataColumn baseColumn) + { + ModalVariant newMode = new( + this.modeDescriptor, + new HierarchicalDataColumn( + new ColumnConfiguration(baseColumn.Configuration) + { + Metadata = new ColumnMetadata(baseColumn.Configuration.Metadata) { Name = this.modeDescriptor.Properties.ColumnName ?? baseColumn.Configuration.Metadata.Name }, + }, + this.projection, + this.collectionProvider, + this.commands), + builder); + + return newMode; + } +} \ No newline at end of file diff --git a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnBuilder.cs b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnBuilder.cs index 41869eb2b..ae1cab66a 100644 --- a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnBuilder.cs @@ -1,14 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Performance.SDK.Processing; using Microsoft.Performance.SDK.Processing.ColumnBuilding; using Microsoft.Performance.SDK.Runtime.ColumnBuilding.Builders.CallbackInvokers; using Microsoft.Performance.SDK.Runtime.ColumnBuilding.Processors; using Microsoft.Performance.SDK.Runtime.ColumnVariants.TreeNodes; +using System; +using System.Collections.Generic; +using System.Linq; namespace Microsoft.Performance.SDK.Runtime.ColumnBuilding.Builders; @@ -18,11 +18,8 @@ namespace Microsoft.Performance.SDK.Runtime.ColumnBuilding.Builders; internal class ToggledColumnBuilder : ToggleableColumnBuilder { - internal record AddedToggle( - ColumnVariantDescriptor ToggleDescriptor, - IDataColumn column); - private readonly IReadOnlyCollection toggles; + private readonly IReadOnlyCollection toggles; private readonly IDataColumn baseColumn; private readonly IColumnVariantsProcessor processor; @@ -39,7 +36,7 @@ internal record AddedToggle( /// The to invoke once the column variants are built. /// public ToggledColumnBuilder( - IReadOnlyCollection toggles, + IReadOnlyCollection toggles, IDataColumn baseColumn, IColumnVariantsProcessor processor) { @@ -64,7 +61,7 @@ public override ToggleableColumnBuilder WithToggle( return new ToggledColumnBuilder( this.toggles.Append( - new AddedToggle( + new ToggleableVariant( toggleDescriptor, new DataColumn( new ColumnConfiguration(this.baseColumn.Configuration) @@ -89,7 +86,7 @@ public override ToggleableColumnBuilder WithHierarchicalToggle( return new ToggledColumnBuilder( this.toggles.Append( - new AddedToggle( + new ToggleableVariant( toggleDescriptor, new HierarchicalDataColumn( new ColumnConfiguration(this.baseColumn.Configuration) @@ -103,6 +100,34 @@ public override ToggleableColumnBuilder WithHierarchicalToggle( processor); } + /// + public override ToggleableColumnBuilder WithToggleableBuilder( + ColumnVariantDescriptor toggleDescriptor, + IProjection projection, + Func buildVariant) + { + Guard.NotNull(toggleDescriptor, nameof(toggleDescriptor)); + Guard.NotNull(projection, nameof(projection)); + Guard.NotNull(buildVariant, nameof(buildVariant)); + + return CreateFromBuilder(toggleDescriptor, projection, null, buildVariant); + } + + /// + public override ToggleableColumnBuilder WithHierarchicalToggleableBuilder( + ColumnVariantDescriptor toggleDescriptor, + IProjection projection, + ICollectionInfoProvider collectionProvider, + Func buildVariant) + { + Guard.NotNull(toggleDescriptor, nameof(toggleDescriptor)); + Guard.NotNull(projection, nameof(projection)); + Guard.NotNull(collectionProvider, nameof(collectionProvider)); + Guard.NotNull(buildVariant, nameof(buildVariant)); + + return CreateFromBuilder(toggleDescriptor, projection, collectionProvider, buildVariant); + } + /// public override ColumnBuilder WithToggledModes( string toggleText, @@ -124,7 +149,7 @@ private IColumnVariantsTreeNode BuildVariant() foreach (var toggle in this.toggles.Reverse()) { - variantsTreeNode = new ToggleableColumnVariantsTreeNode(toggle.ToggleDescriptor, toggle.column, variantsTreeNode); + variantsTreeNode = new ToggleableColumnVariantsTreeNode(toggle.ToggleDescriptor, toggle.Column, variantsTreeNode); } return variantsTreeNode; @@ -134,4 +159,19 @@ protected virtual IColumnVariantsTreeNode GetRootVariant() { return NullColumnVariantsTreeNode.Instance; } -} \ No newline at end of file + + private ToggleableColumnBuilder CreateFromBuilder( + ColumnVariantDescriptor toggleDescriptor, + IProjection projection, + ICollectionInfoProvider collectionProvider, + Func buildVariant) + { + ToggleableVariantBuilder variantBuilder = new ToggledVariantBuilder(toggleDescriptor, projection, collectionProvider); + variantBuilder = buildVariant(variantBuilder); + + return new ToggledColumnBuilder( + this.toggles.Append(variantBuilder.CreateVariant(this.baseColumn)).ToList(), + baseColumn, + processor); + } +} diff --git a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnWithToggledModesBuilder.cs b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnWithToggledModesBuilder.cs index 1b663161a..effe90295 100644 --- a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnWithToggledModesBuilder.cs +++ b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnWithToggledModesBuilder.cs @@ -1,11 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Collections.Generic; using Microsoft.Performance.SDK.Processing; +using Microsoft.Performance.SDK.Processing.ColumnBuilding; using Microsoft.Performance.SDK.Runtime.ColumnBuilding.Builders.CallbackInvokers; using Microsoft.Performance.SDK.Runtime.ColumnBuilding.Processors; using Microsoft.Performance.SDK.Runtime.ColumnVariants.TreeNodes; +using System.Collections.Generic; namespace Microsoft.Performance.SDK.Runtime.ColumnBuilding.Builders; @@ -38,7 +39,7 @@ internal sealed class ToggledColumnWithToggledModesBuilder /// The text to display for the final toggle for the modes. /// public ToggledColumnWithToggledModesBuilder( - IReadOnlyCollection toggles, + IReadOnlyCollection toggles, IDataColumn baseColumn, IColumnVariantsProcessor processor, ModesBuilderCallbackInvoker modesBuilderCallbackActionInvoker, diff --git a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledVariantBuilder`1.cs b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledVariantBuilder`1.cs new file mode 100644 index 000000000..a1f1917f0 --- /dev/null +++ b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledVariantBuilder`1.cs @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using Microsoft.Performance.SDK.ColumnCommands; +using Microsoft.Performance.SDK.Processing; +using Microsoft.Performance.SDK.Processing.ColumnBuilding; + +namespace Microsoft.Performance.SDK.Runtime.ColumnBuilding.Builders; + +/// +/// A concrete that builds a single toggleable +/// column variant, optionally hierarchical, from a projection of type . +/// +/// +/// The type of data that the variant's projection produces. +/// +internal sealed class ToggledVariantBuilder + : ToggleableVariantBuilder +{ + private readonly ColumnVariantDescriptor toggleDescriptor; + private readonly IProjection projection; + private readonly ICollectionInfoProvider? collectionProvider = null; + + private DataColumnCommands? commands = null; + + /// + /// Initializes a new instance of the class + /// for a non-hierarchical toggleable variant. + /// + /// + /// The for the toggle. + /// + /// + /// The projection used to generate the column when this toggle is on. + /// + public ToggledVariantBuilder( + ColumnVariantDescriptor toggleDescriptor, + IProjection projection) + { + this.toggleDescriptor = toggleDescriptor; + this.projection = projection; + } + + /// + /// Initializes a new instance of the class + /// for a hierarchical toggleable variant. + /// + /// + /// The for the toggle. + /// + /// + /// The projection used to generate the column when this toggle is on. + /// + /// + /// The collection provider used to build a hierarchical column. + /// + public ToggledVariantBuilder( + ColumnVariantDescriptor toggleDescriptor, + IProjection projection, + ICollectionInfoProvider collectionProvider) + : this(toggleDescriptor, projection) + { + this.collectionProvider = collectionProvider; + } + + /// + public override ToggleableVariantBuilder WithCommands(DataColumnCommands commands) + { + this.commands = commands; + return this; + } + + /// + internal override ToggleableVariant CreateVariant(IDataColumn baseColumn) + { + if (this.collectionProvider is null) + { + return CreateToggleableVariant(baseColumn); + } + + return CreateHierarchicalToggleableVariant(baseColumn); + } + + /// + /// Creates a non-hierarchical backed by a + /// . + /// + /// + /// The base column whose configuration and metadata the variant derives from. + /// + /// + /// The created . + /// + private ToggleableVariant CreateToggleableVariant(IDataColumn baseColumn) + { + return new ToggleableVariant( + this.toggleDescriptor, + new DataColumn( + new ColumnConfiguration(baseColumn.Configuration) + { + Metadata = new ColumnMetadata(baseColumn.Configuration.Metadata) { Name = this.toggleDescriptor.Properties.ColumnName ?? baseColumn.Configuration.Metadata.Name }, + }, + this.projection, + this.commands)); + } + + /// + /// Creates a hierarchical backed by a + /// . + /// + /// + /// The base column whose configuration and metadata the variant derives from. + /// + /// + /// The created . + /// + private ToggleableVariant CreateHierarchicalToggleableVariant(IDataColumn baseColumn) + { + return new ToggleableVariant( + this.toggleDescriptor, + new HierarchicalDataColumn( + new ColumnConfiguration(baseColumn.Configuration) + { + Metadata = new ColumnMetadata(baseColumn.Configuration.Metadata) { Name = this.toggleDescriptor.Properties.ColumnName ?? baseColumn.Configuration.Metadata.Name }, + }, + this.projection, + this.collectionProvider, + this.commands)); + } +} \ No newline at end of file diff --git a/src/Microsoft.Performance.SDK.Runtime/LanguageFeatures.cs b/src/Microsoft.Performance.SDK.Runtime/LanguageFeatures.cs index 84bb04ad9..deb232d89 100644 --- a/src/Microsoft.Performance.SDK.Runtime/LanguageFeatures.cs +++ b/src/Microsoft.Performance.SDK.Runtime/LanguageFeatures.cs @@ -4,7 +4,7 @@ // See https://stackoverflow.com/a/64749403 namespace System.Runtime.CompilerServices { - internal static class IsExternalInit {} + internal static class IsExternalInit { } internal class ExtensionAttribute : Attribute { } @@ -18,5 +18,19 @@ public CompilerFeatureRequiredAttribute(string name) { } namespace System.Diagnostics.CodeAnalysis { - internal class SetsRequiredMembersAttribute : Attribute {} + internal class SetsRequiredMembersAttribute : Attribute { } +} + +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class NotNullWhenAttribute : Attribute + { + public NotNullWhenAttribute(bool returnValue) + { + this.ReturnValue = returnValue; + } + + public bool ReturnValue { get; } + } } \ No newline at end of file diff --git a/src/Microsoft.Performance.SDK.Runtime/TableBuilder.cs b/src/Microsoft.Performance.SDK.Runtime/TableBuilder.cs index b305653ff..f1c0a2dcb 100644 --- a/src/Microsoft.Performance.SDK.Runtime/TableBuilder.cs +++ b/src/Microsoft.Performance.SDK.Runtime/TableBuilder.cs @@ -171,6 +171,14 @@ public ITableBuilderWithRowCount AddColumn(IDataColumn column) return this.AddColumnWithVariants(column, null); } + /// + public ITableBuilderWithRowCount AddColumn(ColumnBuilder columnBuilder) + { + Guard.NotNull(columnBuilder, nameof(columnBuilder)); + + return columnBuilder.AddColumnToTable(this); + } + /// public ITableBuilderWithRowCount AddColumnWithVariants( IDataColumn column, diff --git a/src/Microsoft.Performance.SDK.Tests/ColumnVariantsTests.cs b/src/Microsoft.Performance.SDK.Tests/ColumnVariantsTests.cs index b8fb6fc4d..be4754336 100644 --- a/src/Microsoft.Performance.SDK.Tests/ColumnVariantsTests.cs +++ b/src/Microsoft.Performance.SDK.Tests/ColumnVariantsTests.cs @@ -3,6 +3,9 @@ using System; using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Performance.SDK.ColumnCommands; using Microsoft.Performance.SDK.Processing; using Microsoft.Performance.SDK.Processing.ColumnBuilding; using Microsoft.Performance.SDK.Runtime; @@ -379,6 +382,81 @@ public void ModeWithColumnName_HasCorrectColumnName() Assert.AreEqual(local.Properties.ColumnName, localMode.ModeColumn.Configuration.Metadata.Name); } + [TestMethod] + public void ToggleableBuilder_WithCommands_CommandsAttachedToToggle() + { + var commands = new DataColumnCommands(new StubDownloadCommand()); + + var tableBuilder = new TableBuilder(); + tableBuilder + .SetRowCount(1) + .AddColumnWithVariants(baseConfig, baseProj, builder => + { + return builder + .WithToggleableBuilder(projectAsDateTime, utcProj, variantBuilder => variantBuilder.WithCommands(commands)); + }); + + var expected = Toggle(projectAsDateTime); + AssertCorrectColumnVariants(expected, tableBuilder); + + var toggleNode = GetTreeNode(tableBuilder) as ToggleableColumnVariantsTreeNode; + Assert.IsNotNull(toggleNode); + var columnWithCommands = toggleNode.ToggledColumn as IDataColumnWithCommands; + Assert.IsNotNull(columnWithCommands); + Assert.AreSame(commands, columnWithCommands.Commands); + } + + [TestMethod] + public void ToggleableBuilder_AfterToggle_PreservesExistingToggle() + { + var tableBuilder = new TableBuilder(); + tableBuilder + .SetRowCount(1) + .AddColumnWithVariants(baseConfig, baseProj, builder => + { + return builder + .WithToggle(projectAsDateTime, utcProj) + .WithToggleableBuilder(utc, utcProj, variantBuilder => variantBuilder); + }); + + var expected = Toggle( + projectAsDateTime, + Toggle(utc)); + AssertCorrectColumnVariants(expected, tableBuilder); + } + + [TestMethod] + public void ModalBuilder_WithCommandsAndNestedToggle_AttachesBoth() + { + var commands = new DataColumnCommands(new StubDownloadCommand()); + + var tableBuilder = new TableBuilder(); + tableBuilder + .SetRowCount(1) + .AddColumnWithVariants(baseConfig, utcProj, builder => + { + return builder + .WithModes(utc.Properties) + .WithModalBuilder(local, localProj, variantBuilder => variantBuilder + .WithCommands(commands) + .WithBuilder(modeBuilder => modeBuilder.WithToggle(showFloat, floatProj))); + }); + + var expected = Modes( + 0, + Mode(utc), + Mode(local, + Toggle(showFloat))); + AssertCorrectColumnVariants(expected, tableBuilder); + + var modesNode = GetTreeNode(tableBuilder) as ModesColumnVariantsTreeNode; + Assert.IsNotNull(modesNode); + var localMode = modesNode.Modes.Cast().Single(m => m.ModeDescriptor.Guid == local.Guid); + var columnWithCommands = localMode.ModeColumn as IDataColumnWithCommands; + Assert.IsNotNull(columnWithCommands); + Assert.AreSame(commands, columnWithCommands.Commands); + } + private void AssertCorrectColumnVariants( IColumnVariantsTreeNode expectedRoot, TableBuilder builtTable) { @@ -412,4 +490,26 @@ private ToggleableColumnVariantsTreeNode Toggle(ColumnVariantDescriptor descript { return new ToggleableColumnVariantsTreeNode(descriptor, null, subVariantsTreeNode ?? NullColumnVariantsTreeNode.Instance); } + + private sealed class StubDownloadCommand + : DownloadSourceCodeCommand + { + public StubDownloadCommand() + : base("Stub") + { + } + + public override bool CanExecute(object value, string downloadPath) + { + return false; + } + + public override Task ExecuteAsync( + object value, + string downloadPath, + CancellationToken cancellationToken) + { + return Task.FromResult(Array.Empty()); + } + } } \ No newline at end of file diff --git a/src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs b/src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs new file mode 100644 index 000000000..e261cd7ea --- /dev/null +++ b/src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Performance.SDK.ColumnCommands; + +/// +/// Exposes the set of host-invokable commands that a plugin has associated +/// with a given column. Plugins construct an instance of this type and +/// attach it to a column (for example via +/// ColumnBuilder<T>.WithCommands) to advertise the operations +/// that a host may perform against the column's row values. +/// +/// +/// This class works with both and +/// . Note that with the latter, +/// the column's row value might be different than T because of an +/// on the column. For +/// this reason, individual command APIs operate on +/// rather than a generic value type. +/// +public sealed class DataColumnCommands +{ + /// + /// Gets a shared instance that + /// exposes no commands. Use this when a column has no commands to + /// advertise, rather than allocating a new empty instance. + /// + public static readonly DataColumnCommands Empty = new(null); + + private readonly DownloadSourceCodeCommand? downloadSourceCodeCommand; + + /// + /// Initializes a new instance of the + /// class with the specified commands. Pass null for any + /// command that is not supported by the column. + /// + /// + /// The to expose on the + /// column, or null if the column does not support downloading + /// source code. + /// + public DataColumnCommands(DownloadSourceCodeCommand? downloadSourceCodeCommand) + { + this.downloadSourceCodeCommand = downloadSourceCodeCommand; + } + + /// + /// Attempts to get the + /// advertised by the column. + /// + /// + /// When this method returns true, contains the + /// associated with the + /// column; otherwise, null. + /// + /// + /// true if the column exposes a + /// ; otherwise, false. + /// + public bool TryGetDownloadSourceCodeCommand([NotNullWhen(true)] out DownloadSourceCodeCommand? command) + { + command = this.downloadSourceCodeCommand; + return command is not null; + } +} diff --git a/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeCommand.cs b/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeCommand.cs new file mode 100644 index 000000000..e08023425 --- /dev/null +++ b/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeCommand.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Performance.SDK.ColumnCommands; + +/// +/// Represents a command that a plugin advertises on a column to download +/// the source code associated with a given row value. Hosts discover this +/// command via +/// and invoke to obtain the results of the +/// source code download attempts. +/// +/// +/// Implementations must be safe to call from a host on an arbitrary +/// thread. The value passed to and +/// is the row value produced by the column, +/// which for hierarchical columns may not be the same type as the +/// column's declared projection type. +/// +public abstract class DownloadSourceCodeCommand +{ + /// + /// Initializes a new instance of the + /// class with a display name + /// that hosts may surface to users (for example, on a context-menu + /// item). + /// + /// + /// A human-readable name for this command. + /// + protected DownloadSourceCodeCommand(string commandName) + { + CommandName = commandName; + } + + /// + /// Gets the human-readable name of this command. Hosts may display + /// this value in UI when offering the command to the user. + /// + public string CommandName { get; } + + /// + /// Determines whether this command can be executed for the specified + /// row value and download path. Hosts should call this before + /// surfacing the command to the user, and skip or disable the + /// command when this method returns false. + /// + /// + /// The row value from the column for which the command may be + /// executed. + /// + /// + /// The local path under which the source code would be downloaded + /// if the command were executed. + /// + /// + /// true if may be called with the + /// given arguments; otherwise, false. + /// + public abstract bool CanExecute(object value, string downloadPath); + + /// + /// Asynchronously downloads the source code files associated with the + /// specified row value to the specified location. + /// + /// + /// The row value from the column for which source code should be + /// downloaded. + /// + /// + /// The local path under which the source code should be + /// downloaded. Implementations decide the exact file layout beneath + /// this path and return the resulting URIs via the + /// properties. + /// + /// + /// A token that may be used to cancel the download operation. + /// + /// + /// A task that produces one for + /// each attempted source code download. The returned array may contain + /// both successful and failed results. Hosts should process each result + /// independently, opening the + /// of each successful result and surfacing the + /// of each failed + /// result. + /// + public abstract System.Threading.Tasks.Task ExecuteAsync( + object value, + string downloadPath, + System.Threading.CancellationToken cancellationToken); +} diff --git a/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeResult.cs b/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeResult.cs new file mode 100644 index 000000000..515d8d104 --- /dev/null +++ b/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeResult.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; + +namespace Microsoft.Performance.SDK.ColumnCommands; + +/// +/// The result of one source code download attempted by a +/// . On success, exposes a +/// that points to the downloaded source code (typically +/// a local file URI) that the host can open using the appropriate platform +/// mechanism. +/// +public class DownloadSourceCodeResult +{ + /// + /// Initializes a new instance of the + /// class representing a + /// successful download. + /// + /// + /// The URI pointing to the downloaded source code that the host + /// should open. + /// + /// + /// is null. + /// + public DownloadSourceCodeResult(Uri uri) + { + Guard.NotNull(uri, nameof(uri)); + + this.Uri = uri; + this.Success = true; + } + + /// + /// Initializes a new instance of the + /// class representing a + /// failure. will be false. + /// + /// + /// A human-readable message describing why the source code could + /// not be downloaded. + /// + /// + /// An optional URI associated with the failure. Because this + /// constructor represents a failure case, this URI does not refer + /// to a successfully downloaded local resource. When not + /// null, it typically represents the remote URI that + /// corresponds to the row value (for example, the source location + /// the command attempted to download from), which the host may + /// choose to surface to the user as a fallback. + /// + /// + /// is null. + /// + public DownloadSourceCodeResult(string errorMessage, Uri? uri) + { + Guard.NotNull(errorMessage, nameof(errorMessage)); + + this.ErrorMessage = errorMessage; + Uri = uri; + this.Success = false; + } + + /// + /// Gets a value indicating whether this download attempt completed + /// successfully and is safe to open. When + /// false, hosts should not attempt to open + /// and should surface instead. + /// + public bool Success { get; } + + /// + /// Gets an optional human-readable error message describing why + /// the source code could not be downloaded, or null when + /// no error occurred. + /// + public string? ErrorMessage { get; } = null; + + /// + /// Gets the URI associated with this result. When + /// is true, this is the URI of the + /// downloaded source code (typically a local file URI) that the + /// host should open. When is false, + /// this value may be null or may be the remote URI + /// corresponding to the row value that the command attempted to + /// download from; in the failure case hosts should not treat it as + /// a successfully downloaded local resource. + /// + public Uri? Uri { get; } +} diff --git a/src/Microsoft.Performance.SDK/IDataColumnWithCommands.cs b/src/Microsoft.Performance.SDK/IDataColumnWithCommands.cs new file mode 100644 index 000000000..7136c2df0 --- /dev/null +++ b/src/Microsoft.Performance.SDK/IDataColumnWithCommands.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using Microsoft.Performance.SDK.ColumnCommands; + +namespace Microsoft.Performance.SDK.Processing; + +/// +/// Implemented by columns (such as and +/// ) that expose a set of +/// host-invokable commands. Hosts cast a column to this interface +/// to discover the commands the plugin has associated with the +/// column. +/// +public interface IDataColumnWithCommands +{ + /// + /// Gets the associated with + /// the column. Never null; a column with no commands + /// exposes . + /// + DataColumnCommands Commands { get; } +} diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder.cs index ca8dc021e..5247fd6c1 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; - namespace Microsoft.Performance.SDK.Processing.ColumnBuilding; /// @@ -16,4 +14,4 @@ private protected ColumnBuilder() } internal abstract void Commit(); -} \ No newline at end of file +} diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs new file mode 100644 index 000000000..6898e4431 --- /dev/null +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; +using Microsoft.Performance.SDK.ColumnCommands; + +namespace Microsoft.Performance.SDK.Processing.ColumnBuilding; + +/// +/// Builds a strongly-typed and adds it to an +/// . Plugins configure the column's +/// metadata, projection, commands, and optional variants on an instance +/// of this class and then pass it to +/// +/// to materialize the column on a table. +/// +/// +/// The type of data produced by the column's projection. +/// +public class ColumnBuilder +{ + private DataColumnCommands? commands = null; + + /// + /// Initializes a new instance of the + /// class from the specified metadata, display hints, and projection. + /// A will be constructed from + /// and . + /// + /// + /// The metadata describing the column. + /// + /// + /// The UI hints describing how the column should be displayed. + /// + /// + /// The projection that produces the column's values. + /// + /// + /// , , or + /// is null. + /// + public ColumnBuilder( + ColumnMetadata metadata, + UIHints displayHints, + IProjection projection) + { + Guard.NotNull(metadata, nameof(metadata)); + Guard.NotNull(displayHints, nameof(displayHints)); + Guard.NotNull(projection, nameof(projection)); + + this.Projection = projection; + this.Configuration = new(metadata, displayHints); + } + + /// + /// Initializes a new instance of the + /// class from an existing and a + /// projection. + /// + /// + /// The column configuration to use for the built column. + /// + /// + /// The projection that produces the column's values. + /// + /// + /// or + /// is null. + /// + public ColumnBuilder( + ColumnConfiguration configuration, + IProjection projection) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(projection, nameof(projection)); + + this.Projection = projection; + this.Configuration = configuration; + } + + /// + /// Gets the that describes the + /// column being built. + /// + protected ColumnConfiguration Configuration { get; } + + /// + /// Gets the projection that produces the column's values. + /// + protected IProjection Projection { get; } + + /// + /// Gets or sets the delegate used to configure column variants on + /// the built column, or null if no variants have been + /// configured via . + /// + protected Func? VariantOptions { get; private set; } = null; + + /// + /// Associates the specified with + /// the column being built. Hosts can later retrieve these commands + /// from the resulting via + /// . + /// + /// + /// The commands to attach to the column. + /// + /// + /// This instance, to allow chaining. + /// + public ColumnBuilder WithCommands( + DataColumnCommands commands) + { + this.commands = commands; + return this; + } + + /// + /// Configures column variants on the column being built by supplying + /// a delegate that further customizes a + /// . + /// + /// + /// A delegate that receives a and + /// returns the configured that will be + /// used to add variants to the column. + /// + /// + /// This instance, to allow chaining. + /// + public ColumnBuilder WithVariants( + Func options) + { + this.VariantOptions = options; + return this; + } + + /// + /// Builds the configured and adds it to + /// the specified . If variants have + /// been configured via , the column is + /// added with those variants; otherwise it is added as a plain + /// column. + /// + /// + /// The table builder to which the built column is added. + /// + /// + /// The , to allow chaining additional + /// table-building calls. + /// + internal ITableBuilderWithRowCount AddColumnToTable( + ITableBuilderWithRowCount tableBuilder) + { + DataColumn dataColumn = BuildColumn(this.commands); + + if (this.VariantOptions is not null) + { + return tableBuilder.AddColumnWithVariants(dataColumn, this.VariantOptions); + } + + return tableBuilder.AddColumn(dataColumn); + } + + /// + /// Constructs the that this builder + /// produces. Derived classes may override this method to return a + /// specialized column type. + /// + /// + /// The commands to associate with the built column, or null + /// if no commands have been configured. + /// + /// + /// A new configured with this builder's + /// , , and + /// . + /// + protected virtual DataColumn BuildColumn( + DataColumnCommands? commands) + { + return new(this.Configuration, this.Projection, commands); + } +} diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/HierchicalColumnBuilder`1.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/HierchicalColumnBuilder`1.cs new file mode 100644 index 000000000..a9d25d89b --- /dev/null +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/HierchicalColumnBuilder`1.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using Microsoft.Performance.SDK.ColumnCommands; + +namespace Microsoft.Performance.SDK.Processing.ColumnBuilding; + +/// +/// Builds a and adds it to an +/// . Extends +/// with the +/// needed to describe the +/// hierarchical structure of the column's values. +/// +/// +/// The type of data produced by the column's projection. +/// +public sealed class HierchicalColumnBuilder + : ColumnBuilder +{ + private readonly ICollectionInfoProvider infoProvider; + + /// + /// Initializes a new instance of the + /// class from the specified + /// metadata, display hints, projection, and collection info + /// provider. + /// + /// + /// The metadata describing the column. + /// + /// + /// The UI hints describing how the column should be displayed. + /// + /// + /// The projection that produces the column's values. + /// + /// + /// The collection info provider that describes the hierarchical + /// structure of the column's values. + /// + public HierchicalColumnBuilder( + ColumnMetadata metadata, + UIHints displayHints, + IProjection projection, + ICollectionInfoProvider infoProvider) + : base(metadata, displayHints, projection) + { + this.infoProvider = infoProvider; + } + + /// + /// Initializes a new instance of the + /// class from an existing + /// , projection, and collection + /// info provider. + /// + /// + /// The column configuration to use for the built column. + /// + /// + /// The projection that produces the column's values. + /// + /// + /// The collection info provider that describes the hierarchical + /// structure of the column's values. + /// + public HierchicalColumnBuilder( + ColumnConfiguration columnConfiguration, + IProjection projection, + ICollectionInfoProvider infoProvider) + : base(columnConfiguration, projection) + { + this.infoProvider = infoProvider; + } + + /// + /// Constructs a using this + /// builder's configuration, projection, collection info provider, + /// and the supplied . + /// + /// + /// The commands to associate with the built column, or null + /// if no commands have been configured. + /// + /// + /// A new . + /// + protected override DataColumn BuildColumn(DataColumnCommands? commands) + { + return new HierarchicalDataColumn(this.Configuration, this.Projection, this.infoProvider, commands); + } +} \ No newline at end of file diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalColumnBuilder.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalColumnBuilder.cs index 0724bc8db..39c9cb316 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalColumnBuilder.cs @@ -145,4 +145,68 @@ public abstract ModalColumnBuilder WithHierarchicalMode( /// If this method is not called, the first mode added will be the default mode. /// public abstract ColumnBuilder WithDefaultMode(Guid modeIdentifierGuid); -} \ No newline at end of file + + /// + /// Adds a mode to the column whose sub-variants are built by a + /// . + /// + /// + /// The for the mode. + /// + /// + /// The projection that will be used to generate the column for this mode. + /// + /// + /// A callback that builds the sub-variants of the added mode and returns its final + /// configuration. + /// + /// + /// The type of data that the projection will produce. + /// + /// + /// A new instance of that has been + /// configured with the new mode. + /// + /// + /// , , or + /// is null. + /// + public abstract ModalColumnBuilder WithModalBuilder( + ColumnVariantDescriptor modeDescriptor, + IProjection projection, + Func buildVariant); + + /// + /// Adds a hierarchical mode to the column whose sub-variants are built by a + /// . + /// + /// + /// The for the mode. + /// + /// + /// The projection that will be used to generate the column for this mode. + /// + /// + /// The collection provider for the column. + /// + /// + /// A callback that builds the sub-variants of the added mode and returns its final + /// configuration. + /// + /// + /// The type of data that the projection will produce. + /// + /// + /// A new instance of that has been + /// configured with the new mode. + /// + /// + /// , , + /// , or is null. + /// + public abstract ModalColumnBuilder WithHierarchicalModalBuilder( + ColumnVariantDescriptor modeDescriptor, + IProjection projection, + ICollectionInfoProvider collectionProvider, + Func buildVariant); +} diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalVariant.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalVariant.cs new file mode 100644 index 000000000..3971cf08f --- /dev/null +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalVariant.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; + +namespace Microsoft.Performance.SDK.Processing.ColumnBuilding; + +internal record ModalVariant( + ColumnVariantDescriptor Descriptor, + IDataColumn Column, + Func? Builder); diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalVariantBuilder.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalVariantBuilder.cs new file mode 100644 index 000000000..73383bcf7 --- /dev/null +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalVariantBuilder.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using Microsoft.Performance.SDK.ColumnCommands; +using System; + +namespace Microsoft.Performance.SDK.Processing.ColumnBuilding; + +/// +/// A builder for configuring a single mode (i.e. one of the mutually +/// exclusive variants) of a column that has been configured as modal. +/// +/// +/// An instance of this builder represents one mode that has already been +/// given a and a projection. Use the +/// methods on this builder to further configure that mode, for example by +/// attaching the commands it supports or by nesting additional toggleable +/// variants underneath it. The methods return a builder so that calls can +/// be chained together. +/// +public abstract class ModalVariantBuilder +{ + private protected ModalVariantBuilder() + { + // Only internal implementations + } + + /// + /// Associates the given with this mode. + /// + /// + /// The commands supported by this mode. + /// + /// + /// A that has been configured with the + /// given commands. + /// + public abstract ModalVariantBuilder WithCommands(DataColumnCommands commands); + + /// + /// Nests additional toggleable variants underneath this mode by invoking + /// the given callback. + /// + /// + /// A callback that builds toggleable sub-variants of this mode and returns + /// its final column configuration. + /// + /// + /// A that has been configured with the + /// nested toggleable variants produced by . + /// + public abstract ModalVariantBuilder WithBuilder(Func builder); + + /// + /// Creates the represented by this builder for + /// the given base column. + /// + /// + /// The base that the mode is being built for. + /// + /// + /// The described by this builder. + /// + internal abstract ModalVariant CreateVariant(IDataColumn baseColumn); +} diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs index b3c68b4f3..fd067ab63 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs @@ -21,9 +21,7 @@ private protected ToggleableColumnBuilder() /// added via calls to this method. /// /// - /// The for the toggle. The - /// represents the name of the toggled - /// on variant. + /// The for the toggle. /// /// /// The projection that will be used to generate the column when this toggle is on. @@ -48,9 +46,7 @@ public abstract ToggleableColumnBuilder WithToggle( /// added via calls to this method. /// /// - /// The for the toggle. The - /// represents the name of the toggled - /// on variant. + /// The for the toggle. /// /// /// The projection that will be used to generate the column when this toggle is on. @@ -94,4 +90,70 @@ public abstract ToggleableColumnBuilder WithHierarchicalToggle( public abstract ColumnBuilder WithToggledModes( string toggleText, Func builder); -} \ No newline at end of file + + /// + /// Adds a new toggleable variant to the column whose nested variants are built by a + /// . The added toggleable variant is nested at + /// the "end" of the chain of toggleable variants already added. + /// + /// + /// The for the toggle. + /// + /// + /// The projection that will be used to generate the column when this toggle is on. + /// + /// + /// A callback that builds the nested variants of the added toggle and returns its final + /// configuration. + /// + /// + /// The type of data that the projection will produce. + /// + /// + /// A new instance of that has been + /// configured with the added toggle. + /// + /// + /// , , or + /// is null. + /// + public abstract ToggleableColumnBuilder WithToggleableBuilder( + ColumnVariantDescriptor toggleDescriptor, + IProjection projection, + Func buildVariant); + + /// + /// Adds a new hierarchical toggleable variant to the column whose nested variants are built + /// by a . The added toggleable variant is nested at + /// the "end" of the chain of toggleable variants already added. + /// + /// + /// The for the toggle. + /// + /// + /// The projection that will be used to generate the column when this toggle is on. + /// + /// + /// The collection provider for the column. + /// + /// + /// A callback that builds the nested variants of the added toggle and returns its final + /// configuration. + /// + /// + /// The type of data that the projection will produce. + /// + /// + /// A new instance of that has been + /// configured with the added toggle. + /// + /// + /// , , + /// , or is null. + /// + public abstract ToggleableColumnBuilder WithHierarchicalToggleableBuilder( + ColumnVariantDescriptor toggleDescriptor, + IProjection projection, + ICollectionInfoProvider collectionProvider, + Func buildVariant); +} diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableVariant.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableVariant.cs new file mode 100644 index 000000000..bef8ddf0e --- /dev/null +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableVariant.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +namespace Microsoft.Performance.SDK.Processing.ColumnBuilding; + +internal record ToggleableVariant( + ColumnVariantDescriptor ToggleDescriptor, + IDataColumn Column); diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableVariantBuilder.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableVariantBuilder.cs new file mode 100644 index 000000000..1e2e9ac79 --- /dev/null +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableVariantBuilder.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using Microsoft.Performance.SDK.ColumnCommands; + +namespace Microsoft.Performance.SDK.Processing.ColumnBuilding; + +/// +/// A builder for configuring a toggleable column variant, such as +/// associating with it. +/// +public abstract class ToggleableVariantBuilder +{ + private protected ToggleableVariantBuilder() + { + // Only internal implementations + } + + /// + /// Associates the given with this variant. + /// + /// + /// The commands supported by this variant. + /// + /// + /// A that has been configured with the + /// given commands. + /// + public abstract ToggleableVariantBuilder WithCommands(DataColumnCommands commands); + + /// + /// Creates the represented by this builder. + /// + /// + /// The base column that the variant is built on top of. + /// + /// + /// The configured by this builder. + /// + internal abstract ToggleableVariant CreateVariant(IDataColumn baseColumn); +} \ No newline at end of file diff --git a/src/Microsoft.Performance.SDK/Processing/DataColumn.cs b/src/Microsoft.Performance.SDK/Processing/DataColumn.cs index b1a43390e..30e0e3ab4 100644 --- a/src/Microsoft.Performance.SDK/Processing/DataColumn.cs +++ b/src/Microsoft.Performance.SDK/Processing/DataColumn.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using Microsoft.Performance.SDK.ColumnCommands; namespace Microsoft.Performance.SDK.Processing { @@ -13,7 +14,8 @@ namespace Microsoft.Performance.SDK.Processing /// The of data projected by this column. /// public class DataColumn - : IDataColumn + : IDataColumn, + IDataColumnWithCommands { /// /// Initializes a new instance of the @@ -60,6 +62,32 @@ public DataColumn( public DataColumn( ColumnConfiguration configuration, IProjection projection) + : this(configuration, projection, null) + { + } + + /// + /// Initializes a new instance of the + /// class. + /// + /// + /// The configuration of this column. + /// + /// + /// The projection that projects the data in the column. + /// + /// + /// The commands supported by this column. May be null. + /// + /// + /// is null. + /// - or - + /// is null. + /// + public DataColumn( + ColumnConfiguration configuration, + IProjection projection, + DataColumnCommands dataColumnCommands) { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(projection, nameof(projection)); @@ -67,6 +95,7 @@ public DataColumn( this.Configuration = configuration; this.ProjectorInterface = projection.GetType(); this.Projector = projection; + this.Commands = dataColumnCommands ?? DataColumnCommands.Empty; } /// @@ -81,6 +110,13 @@ public DataColumn( /// public IProjection Projector { get; } + /// + /// Gets the collection of commands supported by this column. + /// This will be if no + /// commands were provided when this column was constructed. + /// + public DataColumnCommands Commands { get; } + /// /// Projects the data in this column for the given row. /// diff --git a/src/Microsoft.Performance.SDK/Processing/HeirarchicalDataColumn.cs b/src/Microsoft.Performance.SDK/Processing/HeirarchicalDataColumn.cs index 9b2116505..8021f90e4 100644 --- a/src/Microsoft.Performance.SDK/Processing/HeirarchicalDataColumn.cs +++ b/src/Microsoft.Performance.SDK/Processing/HeirarchicalDataColumn.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using Microsoft.Performance.SDK.ColumnCommands; namespace Microsoft.Performance.SDK.Processing { @@ -46,7 +47,39 @@ public HierarchicalDataColumn( ColumnConfiguration configuration, IProjection projection, ICollectionInfoProvider collectionProvider) - : base(configuration, projection) + : this(configuration, projection, collectionProvider, null) + { + } + + /// + /// Initializes a new instance of the + /// class. + /// + /// + /// The configuration of this column. + /// + /// + /// The projection that projects the data in the column. + /// + /// + /// The providers that define how to display the hierarchical data. + /// + /// + /// The commands supported by this column. May be null. + /// + /// + /// is null. + /// - or - + /// is null. + /// - or - + /// is null. + /// + public HierarchicalDataColumn( + ColumnConfiguration configuration, + IProjection projection, + ICollectionInfoProvider collectionProvider, + DataColumnCommands dataColumnCommands) + : base(configuration, projection, dataColumnCommands) { Guard.NotNull(collectionProvider, nameof(collectionProvider)); @@ -64,7 +97,7 @@ public HierarchicalDataColumn( if (collectionInputType != typeof(T)) { throw new InvalidOperationException( - $"TCollection on the ICollectionAccessProviderimplemented on " + + $"TCollection on the ICollectionAccessProviderimplemented on " + $"{nameof(collectionProvider)} doesn't match T of {nameof(HierarchicalDataColumn)} from column " + $"{configuration.Metadata.Guid}. TCollection = {collectionInputType.Name}, T = {typeof(T).Name}"); } diff --git a/src/Microsoft.Performance.SDK/Processing/ITableBuilder.cs b/src/Microsoft.Performance.SDK/Processing/ITableBuilder.cs index b74559040..3e7905379 100644 --- a/src/Microsoft.Performance.SDK/Processing/ITableBuilder.cs +++ b/src/Microsoft.Performance.SDK/Processing/ITableBuilder.cs @@ -159,6 +159,23 @@ public interface ITableBuilderWithRowCount /// ITableBuilderWithRowCount AddColumn(IDataColumn column); + /// + /// Builds the specified column and adds it to this builder instance. + /// + /// + /// The type of data produced by the column's projection. + /// + /// + /// The column builder to build and add. + /// + /// + /// This instance of the builder. + /// + /// + /// is null. + /// + ITableBuilderWithRowCount AddColumn(ColumnBuilder columnBuilder); + /// /// Adds a column that can be configured with multiple variants to this builder instance. /// diff --git a/src/Microsoft.Performance.Toolkit.Engine/RuntimeExecutionResults.cs b/src/Microsoft.Performance.Toolkit.Engine/RuntimeExecutionResults.cs index f5b6d8392..b1f58df21 100644 --- a/src/Microsoft.Performance.Toolkit.Engine/RuntimeExecutionResults.cs +++ b/src/Microsoft.Performance.Toolkit.Engine/RuntimeExecutionResults.cs @@ -630,6 +630,14 @@ public ITableBuilderWithRowCount AddColumn(IDataColumn column) return this.AddColumnWithVariants(column, null); } + /// + public ITableBuilderWithRowCount AddColumn(ColumnBuilder columnBuilder) + { + Guard.NotNull(columnBuilder, nameof(columnBuilder)); + + return columnBuilder.AddColumnToTable(this); + } + /// public ITableBuilderWithRowCount AddColumnWithVariants( IDataColumn column,