From 371599e6ba56f7f50abd388ba9bc6016a6a47485 Mon Sep 17 00:00:00 2001 From: Jayson Maxson Date: Wed, 2 Sep 2026 16:38:32 -0700 Subject: [PATCH 01/25] Add IDataColumnCommands and builders. --- .../ColumnCommands/DataColumnCommands.cs | 13 +++ .../DownloadSourceCodeCommand.cs | 13 +++ .../DownloadSourceCodeResult.cs | 96 +++++++++++++++++++ .../IDataColumnCommands.cs | 12 +++ .../ColumnBuilding/ColumnBuilder.cs | 4 +- .../ColumnBuilding/ColumnBuilder`1.cs | 74 ++++++++++++++ .../HierchicalColumnBuilder`1.cs | 25 +++++ .../Processing/DataColumn.cs | 6 +- 8 files changed, 239 insertions(+), 4 deletions(-) create mode 100644 src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs create mode 100644 src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeCommand.cs create mode 100644 src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeResult.cs create mode 100644 src/Microsoft.Performance.SDK/IDataColumnCommands.cs create mode 100644 src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs create mode 100644 src/Microsoft.Performance.SDK/Processing/ColumnBuilding/HierchicalColumnBuilder`1.cs diff --git a/src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs b/src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs new file mode 100644 index 000000000..a9a3ac570 --- /dev/null +++ b/src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Performance.SDK.ColumnCommands; + +public sealed class DataColumnCommands +{ + internal DataColumnCommands() + { + } + + public DownloadSourceCodeCommand DownloadSourceCodeCommand { get; internal set; } +} diff --git a/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeCommand.cs b/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeCommand.cs new file mode 100644 index 000000000..a64868d74 --- /dev/null +++ b/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeCommand.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Performance.SDK.ColumnCommands; + +public abstract class DownloadSourceCodeCommand +{ + public string CommandName { get; } + + public abstract bool CanExecute(T rowValue); + + public abstract System.Threading.Tasks.Task ExecuteAsync(T rowValue, 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..06497e999 --- /dev/null +++ b/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeResult.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Performance.SDK.ColumnCommands; + +/// +/// The result of 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. Its meaning, when + /// not null, is the attempted download URI. + /// + /// + /// 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 the command 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 pointing to the downloaded source code that the + /// host should open. + /// + public Uri? Uri { get; } +} + + +/// Decision: Where to add the column commands. +/// A. Directly to the IDataColumn or IDataColumn<T> +/// B. In the ITableBuilder +/// +/// Reasons for A: +/// This is column data in much the same way the ColumnConfiguration or the Projection is. +/// +/// Reasons for B: +/// We've never added data to IColumnData types, but we have added to ITableBuilder. +/// This would follow the same behavior as column variants. +/// diff --git a/src/Microsoft.Performance.SDK/IDataColumnCommands.cs b/src/Microsoft.Performance.SDK/IDataColumnCommands.cs new file mode 100644 index 000000000..c2644ee54 --- /dev/null +++ b/src/Microsoft.Performance.SDK/IDataColumnCommands.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Performance.SDK.ColumnCommands; + +namespace Microsoft.Performance.SDK.Processing +{ + public interface IDataColumnCommands + { + 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..1d8ecfab4 --- /dev/null +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Performance.SDK.ColumnCommands; +using System; + +namespace Microsoft.Performance.SDK.Processing.ColumnBuilding; + +public class ColumnBuilder +{ + 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); + } + + public ColumnBuilder( + ColumnConfiguration configuration, + IProjection projection) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(projection, nameof(projection)); + + this.Projection = projection; + this.Configuration = configuration; + } + + protected ColumnConfiguration Configuration { get; } + + protected IProjection Projection { get; } + + protected DownloadSourceCodeCommand DownloadSourceCommand { get; set; } = null; + + protected Func VariantOptions { get; set; } = null; + + public ColumnBuilder WithDownloadSourceCodeCommand( + DownloadSourceCodeCommand downloadSourceCommand) + { + this.DownloadSourceCommand = downloadSourceCommand; + return this; + } + + public ColumnBuilder WithVariants( + Func options) + { + this.VariantOptions = options; + return this; + } + + public ITableBuilderWithRowCount AddColumn(ITableBuilderWithRowCount tableBuilder) + { + DataColumn dataColumn = BuildColumn(); + dataColumn.Commands.DownloadSourceCodeCommand = this.DownloadSourceCommand; + + if (this.VariantOptions is not null) + { + return tableBuilder.AddColumnWithVariants(dataColumn, this.VariantOptions); + } + + return tableBuilder.AddColumn(dataColumn); + } + + protected virtual DataColumn BuildColumn() + { + return new(this.Configuration, this.Projection); + } +} 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..64ff4173d --- /dev/null +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/HierchicalColumnBuilder`1.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Performance.SDK.Processing.ColumnBuilding; + +public sealed class HierchicalColumnBuilder + : ColumnBuilder +{ + private readonly ICollectionInfoProvider infoProvider; + + public HierchicalColumnBuilder( + ColumnMetadata metadata, + UIHints displayHints, + IProjection projection, + ICollectionInfoProvider infoProvider) + : base(metadata, displayHints, projection) + { + this.infoProvider = infoProvider; + } + + protected override DataColumn BuildColumn() + { + return new HierarchicalDataColumn(this.Configuration, this.Projection, this.infoProvider); + } +} \ 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..658a7e8e5 100644 --- a/src/Microsoft.Performance.SDK/Processing/DataColumn.cs +++ b/src/Microsoft.Performance.SDK/Processing/DataColumn.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.Performance.SDK.ColumnCommands; using System; 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, + IDataColumnCommands { /// /// Initializes a new instance of the @@ -81,6 +83,8 @@ public DataColumn( /// public IProjection Projector { get; } + public DataColumnCommands Commands { get; internal set; } + /// /// Projects the data in this column for the given row. /// From da26a6f0550c80f2f03898a2dfefa20383cd76c0 Mon Sep 17 00:00:00 2001 From: Jayson Maxson Date: Thu, 3 Sep 2026 11:15:25 -0700 Subject: [PATCH 02/25] Commands added to DataColumn constructor --- .../ColumnCommands/DataColumnCommands.cs | 4 +- .../ColumnBuilding/ColumnBuilder`1.cs | 15 +++++--- .../HierchicalColumnBuilder`1.cs | 8 +++- .../Processing/DataColumn.cs | 29 ++++++++++++++- .../Processing/HeirarchicalDataColumn.cs | 37 ++++++++++++++++++- 5 files changed, 81 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs b/src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs index a9a3ac570..e03ac4383 100644 --- a/src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs +++ b/src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#nullable enable + namespace Microsoft.Performance.SDK.ColumnCommands; public sealed class DataColumnCommands @@ -9,5 +11,5 @@ internal DataColumnCommands() { } - public DownloadSourceCodeCommand DownloadSourceCodeCommand { get; internal set; } + public DownloadSourceCodeCommand? DownloadSourceCodeCommand { get; init; } = null; } diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs index 1d8ecfab4..c8168463d 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#nullable enable + using Microsoft.Performance.SDK.ColumnCommands; using System; @@ -36,9 +38,9 @@ public ColumnBuilder( protected IProjection Projection { get; } - protected DownloadSourceCodeCommand DownloadSourceCommand { get; set; } = null; + protected DownloadSourceCodeCommand? DownloadSourceCommand { get; set; } = null; - protected Func VariantOptions { get; set; } = null; + protected Func? VariantOptions { get; set; } = null; public ColumnBuilder WithDownloadSourceCodeCommand( DownloadSourceCodeCommand downloadSourceCommand) @@ -56,8 +58,9 @@ public ColumnBuilder WithVariants( public ITableBuilderWithRowCount AddColumn(ITableBuilderWithRowCount tableBuilder) { - DataColumn dataColumn = BuildColumn(); - dataColumn.Commands.DownloadSourceCodeCommand = this.DownloadSourceCommand; + DataColumnCommands commands = new() { DownloadSourceCodeCommand = this.DownloadSourceCommand }; + + DataColumn dataColumn = BuildColumn(commands); if (this.VariantOptions is not null) { @@ -67,8 +70,8 @@ public ITableBuilderWithRowCount AddColumn(ITableBuilderWithRowCount tableBuilde return tableBuilder.AddColumn(dataColumn); } - protected virtual DataColumn BuildColumn() + protected virtual DataColumn BuildColumn(DataColumnCommands? commands) { - return new(this.Configuration, this.Projection); + 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 index 64ff4173d..c25cd1384 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/HierchicalColumnBuilder`1.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/HierchicalColumnBuilder`1.cs @@ -1,6 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#nullable enable + +using Microsoft.Performance.SDK.ColumnCommands; + namespace Microsoft.Performance.SDK.Processing.ColumnBuilding; public sealed class HierchicalColumnBuilder @@ -18,8 +22,8 @@ public HierchicalColumnBuilder( this.infoProvider = infoProvider; } - protected override DataColumn BuildColumn() + protected override DataColumn BuildColumn(DataColumnCommands? commands) { - return new HierarchicalDataColumn(this.Configuration, this.Projection, this.infoProvider); + return new HierarchicalDataColumn(this.Configuration, this.Projection, this.infoProvider, commands); } } \ 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 658a7e8e5..e9afc220e 100644 --- a/src/Microsoft.Performance.SDK/Processing/DataColumn.cs +++ b/src/Microsoft.Performance.SDK/Processing/DataColumn.cs @@ -62,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)); @@ -69,6 +95,7 @@ public DataColumn( this.Configuration = configuration; this.ProjectorInterface = projection.GetType(); this.Projector = projection; + this.Commands = dataColumnCommands; } /// @@ -83,7 +110,7 @@ public DataColumn( /// public IProjection Projector { get; } - public DataColumnCommands Commands { get; internal set; } + 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..8ce94c0d9 100644 --- a/src/Microsoft.Performance.SDK/Processing/HeirarchicalDataColumn.cs +++ b/src/Microsoft.Performance.SDK/Processing/HeirarchicalDataColumn.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.Performance.SDK.ColumnCommands; using System; 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}"); } From b07f7212e75f34823aaf812b4feb89b7357d454f Mon Sep 17 00:00:00 2001 From: Jayson Maxson Date: Thu, 3 Sep 2026 11:25:29 -0700 Subject: [PATCH 03/25] Add HierchicalColumnBuilder constructor. --- .../ColumnBuilding/HierchicalColumnBuilder`1.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/HierchicalColumnBuilder`1.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/HierchicalColumnBuilder`1.cs index c25cd1384..e27e3940f 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/HierchicalColumnBuilder`1.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/HierchicalColumnBuilder`1.cs @@ -22,6 +22,15 @@ public HierchicalColumnBuilder( this.infoProvider = infoProvider; } + public HierchicalColumnBuilder( + ColumnConfiguration columnConfiguration, + IProjection projection, + ICollectionInfoProvider infoProvider) + : base(columnConfiguration, projection) + { + this.infoProvider = infoProvider; + } + protected override DataColumn BuildColumn(DataColumnCommands? commands) { return new HierarchicalDataColumn(this.Configuration, this.Projection, this.infoProvider, commands); From 84cf548a66549604ac47b7a40b2aed098faf5b03 Mon Sep 17 00:00:00 2001 From: Jayson Maxson Date: Thu, 3 Sep 2026 16:52:54 -0700 Subject: [PATCH 04/25] Add variant column command overloads. --- .../Builders/EmptyColumnBuilder.cs | 30 ++++++-- .../Builders/ModalColumnWithModesBuilder.cs | 39 ++++++++--- .../Builders/ToggledColumnBuilder.cs | 32 +++++++-- .../ColumnBuilding/ModalColumnBuilder.cs | 68 +++++++++++++++++++ .../ColumnBuilding/ToggleableColumnBuilder.cs | 68 +++++++++++++++++++ 5 files changed, 220 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/EmptyColumnBuilder.cs b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/EmptyColumnBuilder.cs index 59bb8c962..a1e333525 100644 --- a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/EmptyColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/EmptyColumnBuilder.cs @@ -1,13 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Collections.Generic; +using Microsoft.Performance.SDK.ColumnCommands; 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; @@ -47,6 +48,15 @@ internal override void Commit() public override ToggleableColumnBuilder WithToggle( ColumnVariantDescriptor toggleDescriptor, IProjection projection) + { + return WithToggle(toggleDescriptor, projection, default); + } + + /// + public override ToggleableColumnBuilder WithToggle( + ColumnVariantDescriptor toggleDescriptor, + IProjection projection, + DataColumnCommands dataColumnCommands) { Guard.NotNull(toggleDescriptor, nameof(toggleDescriptor)); Guard.NotNull(projection, nameof(projection)); @@ -59,7 +69,8 @@ public override ToggleableColumnBuilder WithToggle( { Metadata = new ColumnMetadata(this.baseColumn.Configuration.Metadata) { Name = toggleDescriptor.Properties.ColumnName ?? this.baseColumn.Configuration.Metadata.Name}, }, - projection)), + projection, + dataColumnCommands)), ], baseColumn, processor); @@ -70,6 +81,16 @@ public override ToggleableColumnBuilder WithHierarchicalToggle( ColumnVariantDescriptor toggleDescriptor, IProjection projection, ICollectionInfoProvider collectionProvider) + { + return WithHierarchicalToggle(toggleDescriptor, projection, collectionProvider, default); + } + + /// + public override ToggleableColumnBuilder WithHierarchicalToggle( + ColumnVariantDescriptor toggleDescriptor, + IProjection projection, + ICollectionInfoProvider collectionProvider, + DataColumnCommands dataColumnCommands) { Guard.NotNull(toggleDescriptor, nameof(toggleDescriptor)); Guard.NotNull(projection, nameof(projection)); @@ -84,7 +105,8 @@ public override ToggleableColumnBuilder WithHierarchicalToggle( Metadata = new ColumnMetadata(this.baseColumn.Configuration.Metadata) { Name = toggleDescriptor.Properties.ColumnName ?? this.baseColumn.Configuration.Metadata.Name}, }, projection, - collectionProvider)), + collectionProvider, + dataColumnCommands)), ], baseColumn, processor); diff --git a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs index 4b02b17f3..80ecbd758 100644 --- a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs +++ b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs @@ -1,15 +1,16 @@ // 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.ColumnCommands; 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; @@ -95,6 +96,16 @@ public override ModalColumnBuilder WithMode( ColumnVariantDescriptor modeDescriptor, IProjection projection, Func builder) + { + return WithMode(modeDescriptor, projection, default, builder); + } + + /// + public override ModalColumnBuilder WithMode( + ColumnVariantDescriptor modeDescriptor, + IProjection projection, + DataColumnCommands dataColumnCommands, + Func builder) { Guard.NotNull(modeDescriptor, nameof(modeDescriptor)); Guard.NotNull(projection, nameof(projection)); @@ -104,9 +115,10 @@ public override ModalColumnBuilder WithMode( 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), + projection, + dataColumnCommands), builder); return WithMode(newMode); @@ -125,6 +137,16 @@ public override ModalColumnBuilder WithHierarchicalMode( IProjection projection, ICollectionInfoProvider collectionProvider, Func builder) + { + return WithHierarchicalMode(modeDescriptor, projection, collectionProvider, null, builder); + } + + public override ModalColumnBuilder WithHierarchicalMode( + ColumnVariantDescriptor modeDescriptor, + IProjection projection, + ICollectionInfoProvider collectionProvider, + DataColumnCommands dataColumnCommands, + Func builder) { Guard.NotNull(modeDescriptor, nameof(modeDescriptor)); Guard.NotNull(projection, nameof(projection)); @@ -135,10 +157,11 @@ public override ModalColumnBuilder WithHierarchicalMode( 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), + collectionProvider, + dataColumnCommands), builder); return WithMode(newMode); diff --git a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnBuilder.cs b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnBuilder.cs index 41869eb2b..616b24510 100644 --- a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnBuilder.cs @@ -1,14 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Collections.Generic; -using System.Linq; +using Microsoft.Performance.SDK.ColumnCommands; 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; @@ -58,6 +59,15 @@ internal override void Commit() public override ToggleableColumnBuilder WithToggle( ColumnVariantDescriptor toggleDescriptor, IProjection projection) + { + return WithToggle(toggleDescriptor, projection, default); + } + + /// + public override ToggleableColumnBuilder WithToggle( + ColumnVariantDescriptor toggleDescriptor, + IProjection projection, + DataColumnCommands dataColumnCommands) { Guard.NotNull(toggleDescriptor, nameof(toggleDescriptor)); Guard.NotNull(projection, nameof(projection)); @@ -71,7 +81,8 @@ public override ToggleableColumnBuilder WithToggle( { Metadata = new ColumnMetadata(this.baseColumn.Configuration.Metadata) { Name = toggleDescriptor.Properties.ColumnName ?? this.baseColumn.Configuration.Metadata.Name }, }, - projection)) + projection, + dataColumnCommands)) ).ToList(), this.baseColumn, this.processor); @@ -82,6 +93,16 @@ public override ToggleableColumnBuilder WithHierarchicalToggle( ColumnVariantDescriptor toggleDescriptor, IProjection projection, ICollectionInfoProvider collectionProvider) + { + return WithHierarchicalToggle(toggleDescriptor, projection, collectionProvider, default); + } + + /// + public override ToggleableColumnBuilder WithHierarchicalToggle( + ColumnVariantDescriptor toggleDescriptor, + IProjection projection, + ICollectionInfoProvider collectionProvider, + DataColumnCommands dataColumnCommands) { Guard.NotNull(toggleDescriptor, nameof(toggleDescriptor)); Guard.NotNull(projection, nameof(projection)); @@ -97,7 +118,8 @@ public override ToggleableColumnBuilder WithHierarchicalToggle( Metadata = new ColumnMetadata(this.baseColumn.Configuration.Metadata) { Name = toggleDescriptor.Properties.ColumnName ?? this.baseColumn.Configuration.Metadata.Name }, }, projection, - collectionProvider)) + collectionProvider, + dataColumnCommands)) ).ToList(), baseColumn, processor); diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalColumnBuilder.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalColumnBuilder.cs index 0724bc8db..70faf9759 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalColumnBuilder.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.Performance.SDK.ColumnCommands; using System; namespace Microsoft.Performance.SDK.Processing.ColumnBuilding; @@ -94,6 +95,37 @@ public abstract ModalColumnBuilder WithMode( IProjection projection, Func builder); + /// + /// Adds a mode to the column. + /// + /// + /// The for the mode. + /// + /// + /// The projection that will be used to generate the column for this mode. + /// + /// + /// Data column commands. + /// + /// + /// A callback that builds sub-variants of the added mode and returns its final column 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 WithMode( + ColumnVariantDescriptor modeDescriptor, + IProjection projection, + DataColumnCommands dataColumnCommands, + Func builder); + /// /// Adds a hierarchical mode to the column. /// @@ -126,6 +158,42 @@ public abstract ModalColumnBuilder WithHierarchicalMode( ICollectionInfoProvider collectionProvider, Func builder); + /// + /// Adds a hierarchical mode to the column. + /// + /// + /// The for the mode. + /// + /// + /// The projection that will be used to generate the column for this mode. + /// + /// + /// The collection provider for the column. + /// + /// + /// Data column commands. + /// + /// + /// A callback that builds sub-variants of the added mode and returns its final column 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 WithHierarchicalMode( + ColumnVariantDescriptor modeDescriptor, + IProjection projection, + ICollectionInfoProvider collectionProvider, + DataColumnCommands dataColumnCommands, + Func builder); + /// /// Sets the default mode for the column. /// diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs index b3c68b4f3..d084a4f8c 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.Performance.SDK.ColumnCommands; using System; namespace Microsoft.Performance.SDK.Processing.ColumnBuilding; @@ -42,6 +43,37 @@ public abstract ToggleableColumnBuilder WithToggle( ColumnVariantDescriptor toggleDescriptor, IProjection projection); + /// + /// Adds a new toggleable variant to the column. The added toggleable variant + /// is nested at the "end" of the chain of toggleable variants already + /// added via calls to this method. + /// + /// + /// The for the toggle. The + /// represents the name of the toggled + /// on variant. + /// + /// + /// The projection that will be used to generate the column when this toggle is on. + /// + /// + /// Data column commands. + /// + /// + /// 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 WithToggle( + ColumnVariantDescriptor toggleDescriptor, + IProjection projection, + DataColumnCommands dataColumnCommands); + /// /// Adds a new toggleable variant to the column. The added toggleable variant /// is nested at the "end" of the chain of toggleable variants already @@ -74,6 +106,42 @@ public abstract ToggleableColumnBuilder WithHierarchicalToggle( IProjection projection, ICollectionInfoProvider collectionProvider); + /// + /// Adds a new toggleable variant to the column. The added toggleable variant + /// is nested at the "end" of the chain of toggleable variants already + /// added via calls to this method. + /// + /// + /// The for the toggle. The + /// represents the name of the toggled + /// on variant. + /// + /// + /// The projection that will be used to generate the column when this toggle is on. + /// + /// + /// The collection provider for the column. + /// + /// + /// Data column commands. + /// + /// + /// 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 WithHierarchicalToggle( + ColumnVariantDescriptor toggleDescriptor, + IProjection projection, + ICollectionInfoProvider collectionProvider, + DataColumnCommands dataColumnCommands); + /// /// Adds a set of modes to the column that are nested inside of a toggle with no /// associated projection. From 078e5ee850302b7f598eee3167c5509234116623 Mon Sep 17 00:00:00 2001 From: Jayson Maxson Date: Thu, 3 Sep 2026 17:01:28 -0700 Subject: [PATCH 05/25] Add DownloadSourceCodeCommand constructor. --- .../ColumnCommands/DownloadSourceCodeCommand.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeCommand.cs b/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeCommand.cs index a64868d74..a484d533b 100644 --- a/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeCommand.cs +++ b/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeCommand.cs @@ -5,6 +5,11 @@ namespace Microsoft.Performance.SDK.ColumnCommands; public abstract class DownloadSourceCodeCommand { + protected DownloadSourceCodeCommand(string commandName) + { + CommandName = commandName; + } + public string CommandName { get; } public abstract bool CanExecute(T rowValue); From 24d3a12fc6fffcda1030627750e9885c693b8c59 Mon Sep 17 00:00:00 2001 From: Jayson Maxson Date: Thu, 3 Sep 2026 17:18:33 -0700 Subject: [PATCH 06/25] Add DownloadSourceCodeCommand context. --- .../ColumnCommands/DownloadSourceCodeCommand.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeCommand.cs b/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeCommand.cs index a484d533b..57d66d3ea 100644 --- a/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeCommand.cs +++ b/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeCommand.cs @@ -12,7 +12,9 @@ protected DownloadSourceCodeCommand(string commandName) public string CommandName { get; } - public abstract bool CanExecute(T rowValue); + public abstract bool CanExecute(Context context); - public abstract System.Threading.Tasks.Task ExecuteAsync(T rowValue, System.Threading.CancellationToken cancellationToken); + public abstract System.Threading.Tasks.Task ExecuteAsync(Context context, System.Threading.CancellationToken cancellationToken); + + public record Context(T Value, string DownloadPath); } From ea3f8b11279b73c913c74f901012ef8b2bf8b8ed Mon Sep 17 00:00:00 2001 From: Jayson Maxson Date: Tue, 8 Sep 2026 16:43:21 -0700 Subject: [PATCH 07/25] Empty From aa5347c9235be779d971bd113e4f1da2373eacfd Mon Sep 17 00:00:00 2001 From: Jayson Maxson Date: Tue, 8 Sep 2026 16:58:15 -0700 Subject: [PATCH 08/25] Empty From 28dedab75b2321dee534bf403e0c8952176dfedc Mon Sep 17 00:00:00 2001 From: Jayson Maxson Date: Tue, 8 Sep 2026 17:01:13 -0700 Subject: [PATCH 09/25] DataColumnCommands is no longer a generic type. --- .../Builders/EmptyColumnBuilder.cs | 4 +- .../Builders/ModalColumnWithModesBuilder.cs | 4 +- .../Builders/ToggledColumnBuilder.cs | 4 +- .../LanguageFeatures.cs | 18 ++++- .../ColumnCommands/DataColumnCommands.cs | 68 ++++++++++++++++++- .../IDataColumnCommands.cs | 4 +- .../ColumnBuilding/ColumnBuilder`1.cs | 14 ++-- .../HierchicalColumnBuilder`1.cs | 2 +- .../ColumnBuilding/ModalColumnBuilder.cs | 4 +- .../ColumnBuilding/ToggleableColumnBuilder.cs | 4 +- .../Processing/DataColumn.cs | 6 +- .../Processing/HeirarchicalDataColumn.cs | 2 +- 12 files changed, 105 insertions(+), 29 deletions(-) diff --git a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/EmptyColumnBuilder.cs b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/EmptyColumnBuilder.cs index a1e333525..2ec6d15ba 100644 --- a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/EmptyColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/EmptyColumnBuilder.cs @@ -56,7 +56,7 @@ public override ToggleableColumnBuilder WithToggle( public override ToggleableColumnBuilder WithToggle( ColumnVariantDescriptor toggleDescriptor, IProjection projection, - DataColumnCommands dataColumnCommands) + DataColumnCommands dataColumnCommands) { Guard.NotNull(toggleDescriptor, nameof(toggleDescriptor)); Guard.NotNull(projection, nameof(projection)); @@ -90,7 +90,7 @@ public override ToggleableColumnBuilder WithHierarchicalToggle( ColumnVariantDescriptor toggleDescriptor, IProjection projection, ICollectionInfoProvider collectionProvider, - DataColumnCommands dataColumnCommands) + DataColumnCommands dataColumnCommands) { Guard.NotNull(toggleDescriptor, nameof(toggleDescriptor)); Guard.NotNull(projection, nameof(projection)); diff --git a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs index 80ecbd758..2cf73810c 100644 --- a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs +++ b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs @@ -104,7 +104,7 @@ public override ModalColumnBuilder WithMode( public override ModalColumnBuilder WithMode( ColumnVariantDescriptor modeDescriptor, IProjection projection, - DataColumnCommands dataColumnCommands, + DataColumnCommands dataColumnCommands, Func builder) { Guard.NotNull(modeDescriptor, nameof(modeDescriptor)); @@ -145,7 +145,7 @@ public override ModalColumnBuilder WithHierarchicalMode( ColumnVariantDescriptor modeDescriptor, IProjection projection, ICollectionInfoProvider collectionProvider, - DataColumnCommands dataColumnCommands, + DataColumnCommands dataColumnCommands, Func builder) { Guard.NotNull(modeDescriptor, nameof(modeDescriptor)); diff --git a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnBuilder.cs b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnBuilder.cs index 616b24510..78e6ca941 100644 --- a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnBuilder.cs @@ -67,7 +67,7 @@ public override ToggleableColumnBuilder WithToggle( public override ToggleableColumnBuilder WithToggle( ColumnVariantDescriptor toggleDescriptor, IProjection projection, - DataColumnCommands dataColumnCommands) + DataColumnCommands dataColumnCommands) { Guard.NotNull(toggleDescriptor, nameof(toggleDescriptor)); Guard.NotNull(projection, nameof(projection)); @@ -102,7 +102,7 @@ public override ToggleableColumnBuilder WithHierarchicalToggle( ColumnVariantDescriptor toggleDescriptor, IProjection projection, ICollectionInfoProvider collectionProvider, - DataColumnCommands dataColumnCommands) + DataColumnCommands dataColumnCommands) { Guard.NotNull(toggleDescriptor, nameof(toggleDescriptor)); Guard.NotNull(projection, nameof(projection)); 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/ColumnCommands/DataColumnCommands.cs b/src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs index e03ac4383..5aa1e8415 100644 --- a/src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs +++ b/src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs @@ -3,13 +3,75 @@ #nullable enable +using System; +using System.Diagnostics.CodeAnalysis; + namespace Microsoft.Performance.SDK.ColumnCommands; -public sealed class DataColumnCommands +/// +/// This class exposes commands on a given column. +/// +/// +/// 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. +/// +public sealed class DataColumnCommands { - internal DataColumnCommands() + public static readonly DataColumnCommands Empty = new(new EmptyCommandsImpl()); + + private readonly DataColumnCommandsImpl commands; + + private DataColumnCommands(DataColumnCommandsImpl commands) + { + this.commands = commands; + } + + public bool TryGetDownloadSourceCodeCommand([NotNullWhen(true)] out DownloadSourceCodeCommand? command) + { + return this.commands.TryGetDownloadSourceCodeCommand(out command); + } + + public static DataColumnCommands Create(object? downloadSourceCommand) + { + if (downloadSourceCommand is null) + { + return Empty; + } + + var commandsType = typeof(DataColumnCommandsImpl<>).MakeGenericType(downloadSourceCommand.GetType()); + return (DataColumnCommands)Activator.CreateInstance(commandsType, [downloadSourceCommand]); + } + + private abstract class DataColumnCommandsImpl + { + public abstract bool TryGetDownloadSourceCodeCommand([NotNullWhen(true)] out DownloadSourceCodeCommand? command); + } + + private sealed class EmptyCommandsImpl + : DataColumnCommandsImpl { + public override bool TryGetDownloadSourceCodeCommand([NotNullWhen(true)] out DownloadSourceCodeCommand? command) + { + command = null; + return false; + } } - public DownloadSourceCodeCommand? DownloadSourceCodeCommand { get; init; } = null; + private sealed class DataColumnCommandsImpl + : DataColumnCommandsImpl + { + DownloadSourceCodeCommand? downloadSourceCommand = null; + + public DataColumnCommandsImpl(DownloadSourceCodeCommand? downloadSourceCommand) + { + this.downloadSourceCommand = downloadSourceCommand; + } + + public override bool TryGetDownloadSourceCodeCommand([NotNullWhen(true)] out DownloadSourceCodeCommand? command) + { + command = this.downloadSourceCommand as DownloadSourceCodeCommand; + return command is not null; + } + } } diff --git a/src/Microsoft.Performance.SDK/IDataColumnCommands.cs b/src/Microsoft.Performance.SDK/IDataColumnCommands.cs index c2644ee54..e2a386f5f 100644 --- a/src/Microsoft.Performance.SDK/IDataColumnCommands.cs +++ b/src/Microsoft.Performance.SDK/IDataColumnCommands.cs @@ -5,8 +5,8 @@ namespace Microsoft.Performance.SDK.Processing { - public interface IDataColumnCommands + public interface IDataColumnCommands { - DataColumnCommands Commands { get; } + DataColumnCommands Commands { get; } } } diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs index c8168463d..521f9a6f7 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs @@ -10,6 +10,8 @@ namespace Microsoft.Performance.SDK.Processing.ColumnBuilding; public class ColumnBuilder { + private object? downloadSourceCommand = null; + public ColumnBuilder( ColumnMetadata metadata, UIHints displayHints, @@ -38,14 +40,12 @@ public ColumnBuilder( protected IProjection Projection { get; } - protected DownloadSourceCodeCommand? DownloadSourceCommand { get; set; } = null; - protected Func? VariantOptions { get; set; } = null; - public ColumnBuilder WithDownloadSourceCodeCommand( - DownloadSourceCodeCommand downloadSourceCommand) + public ColumnBuilder WithDownloadSourceCodeCommand( + DownloadSourceCodeCommand downloadSourceCommand) { - this.DownloadSourceCommand = downloadSourceCommand; + this.downloadSourceCommand = downloadSourceCommand; return this; } @@ -58,7 +58,7 @@ public ColumnBuilder WithVariants( public ITableBuilderWithRowCount AddColumn(ITableBuilderWithRowCount tableBuilder) { - DataColumnCommands commands = new() { DownloadSourceCodeCommand = this.DownloadSourceCommand }; + DataColumnCommands commands = DataColumnCommands.Create(this.downloadSourceCommand); DataColumn dataColumn = BuildColumn(commands); @@ -70,7 +70,7 @@ public ITableBuilderWithRowCount AddColumn(ITableBuilderWithRowCount tableBuilde return tableBuilder.AddColumn(dataColumn); } - protected virtual DataColumn BuildColumn(DataColumnCommands? commands) + 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 index e27e3940f..6648de410 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/HierchicalColumnBuilder`1.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/HierchicalColumnBuilder`1.cs @@ -31,7 +31,7 @@ public HierchicalColumnBuilder( this.infoProvider = infoProvider; } - protected override DataColumn BuildColumn(DataColumnCommands? commands) + protected override DataColumn BuildColumn(DataColumnCommands? commands) { return new HierarchicalDataColumn(this.Configuration, this.Projection, this.infoProvider, commands); } diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalColumnBuilder.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalColumnBuilder.cs index 70faf9759..41ce2ff2b 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalColumnBuilder.cs @@ -123,7 +123,7 @@ public abstract ModalColumnBuilder WithMode( public abstract ModalColumnBuilder WithMode( ColumnVariantDescriptor modeDescriptor, IProjection projection, - DataColumnCommands dataColumnCommands, + DataColumnCommands dataColumnCommands, Func builder); /// @@ -191,7 +191,7 @@ public abstract ModalColumnBuilder WithHierarchicalMode( ColumnVariantDescriptor modeDescriptor, IProjection projection, ICollectionInfoProvider collectionProvider, - DataColumnCommands dataColumnCommands, + DataColumnCommands dataColumnCommands, Func builder); /// diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs index d084a4f8c..8716b6cfa 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs @@ -72,7 +72,7 @@ public abstract ToggleableColumnBuilder WithToggle( public abstract ToggleableColumnBuilder WithToggle( ColumnVariantDescriptor toggleDescriptor, IProjection projection, - DataColumnCommands dataColumnCommands); + DataColumnCommands dataColumnCommands); /// /// Adds a new toggleable variant to the column. The added toggleable variant @@ -140,7 +140,7 @@ public abstract ToggleableColumnBuilder WithHierarchicalToggle( ColumnVariantDescriptor toggleDescriptor, IProjection projection, ICollectionInfoProvider collectionProvider, - DataColumnCommands dataColumnCommands); + DataColumnCommands dataColumnCommands); /// /// Adds a set of modes to the column that are nested inside of a toggle with no diff --git a/src/Microsoft.Performance.SDK/Processing/DataColumn.cs b/src/Microsoft.Performance.SDK/Processing/DataColumn.cs index e9afc220e..206612e16 100644 --- a/src/Microsoft.Performance.SDK/Processing/DataColumn.cs +++ b/src/Microsoft.Performance.SDK/Processing/DataColumn.cs @@ -15,7 +15,7 @@ namespace Microsoft.Performance.SDK.Processing /// public class DataColumn : IDataColumn, - IDataColumnCommands + IDataColumnCommands { /// /// Initializes a new instance of the @@ -87,7 +87,7 @@ public DataColumn( public DataColumn( ColumnConfiguration configuration, IProjection projection, - DataColumnCommands dataColumnCommands) + DataColumnCommands dataColumnCommands) { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(projection, nameof(projection)); @@ -110,7 +110,7 @@ public DataColumn( /// public IProjection Projector { get; } - public DataColumnCommands Commands { get; } + 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 8ce94c0d9..58d2b2255 100644 --- a/src/Microsoft.Performance.SDK/Processing/HeirarchicalDataColumn.cs +++ b/src/Microsoft.Performance.SDK/Processing/HeirarchicalDataColumn.cs @@ -78,7 +78,7 @@ public HierarchicalDataColumn( ColumnConfiguration configuration, IProjection projection, ICollectionInfoProvider collectionProvider, - DataColumnCommands dataColumnCommands) + DataColumnCommands dataColumnCommands) : base(configuration, projection, dataColumnCommands) { Guard.NotNull(collectionProvider, nameof(collectionProvider)); From bf431efeca9c96f9a57f905b548d9d53081e47b0 Mon Sep 17 00:00:00 2001 From: Jayson Maxson Date: Wed, 9 Sep 2026 12:13:03 -0700 Subject: [PATCH 10/25] DownloadSourceCodeCommand is not generic. --- .../ColumnCommands/DataColumnCommands.cs | 57 +++---------------- .../DownloadSourceCodeCommand.cs | 11 ++-- .../ColumnBuilding/ColumnBuilder`1.cs | 8 +-- 3 files changed, 17 insertions(+), 59 deletions(-) diff --git a/src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs b/src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs index 5aa1e8415..1c68a6af0 100644 --- a/src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs +++ b/src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs @@ -3,7 +3,6 @@ #nullable enable -using System; using System.Diagnostics.CodeAnalysis; namespace Microsoft.Performance.SDK.ColumnCommands; @@ -18,60 +17,18 @@ namespace Microsoft.Performance.SDK.ColumnCommands; /// public sealed class DataColumnCommands { - public static readonly DataColumnCommands Empty = new(new EmptyCommandsImpl()); + public static readonly DataColumnCommands Empty = new(null); - private readonly DataColumnCommandsImpl commands; + private readonly DownloadSourceCodeCommand? downloadSourceCodeCommand; - private DataColumnCommands(DataColumnCommandsImpl commands) + public DataColumnCommands(DownloadSourceCodeCommand? downloadSourceCodeCommand) { - this.commands = commands; + this.downloadSourceCodeCommand = downloadSourceCodeCommand; } - public bool TryGetDownloadSourceCodeCommand([NotNullWhen(true)] out DownloadSourceCodeCommand? command) + public bool TryGetDownloadSourceCodeCommand([NotNullWhen(true)] out DownloadSourceCodeCommand? command) { - return this.commands.TryGetDownloadSourceCodeCommand(out command); - } - - public static DataColumnCommands Create(object? downloadSourceCommand) - { - if (downloadSourceCommand is null) - { - return Empty; - } - - var commandsType = typeof(DataColumnCommandsImpl<>).MakeGenericType(downloadSourceCommand.GetType()); - return (DataColumnCommands)Activator.CreateInstance(commandsType, [downloadSourceCommand]); - } - - private abstract class DataColumnCommandsImpl - { - public abstract bool TryGetDownloadSourceCodeCommand([NotNullWhen(true)] out DownloadSourceCodeCommand? command); - } - - private sealed class EmptyCommandsImpl - : DataColumnCommandsImpl - { - public override bool TryGetDownloadSourceCodeCommand([NotNullWhen(true)] out DownloadSourceCodeCommand? command) - { - command = null; - return false; - } - } - - private sealed class DataColumnCommandsImpl - : DataColumnCommandsImpl - { - DownloadSourceCodeCommand? downloadSourceCommand = null; - - public DataColumnCommandsImpl(DownloadSourceCodeCommand? downloadSourceCommand) - { - this.downloadSourceCommand = downloadSourceCommand; - } - - public override bool TryGetDownloadSourceCodeCommand([NotNullWhen(true)] out DownloadSourceCodeCommand? command) - { - command = this.downloadSourceCommand as DownloadSourceCodeCommand; - return command is not null; - } + 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 index 57d66d3ea..41999a528 100644 --- a/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeCommand.cs +++ b/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeCommand.cs @@ -3,7 +3,7 @@ namespace Microsoft.Performance.SDK.ColumnCommands; -public abstract class DownloadSourceCodeCommand +public abstract class DownloadSourceCodeCommand { protected DownloadSourceCodeCommand(string commandName) { @@ -12,9 +12,10 @@ protected DownloadSourceCodeCommand(string commandName) public string CommandName { get; } - public abstract bool CanExecute(Context context); + public abstract bool CanExecute(object value, string downloadPath); - public abstract System.Threading.Tasks.Task ExecuteAsync(Context context, System.Threading.CancellationToken cancellationToken); - - public record Context(T Value, string DownloadPath); + public abstract System.Threading.Tasks.Task ExecuteAsync( + object value, + string downloadPath, + System.Threading.CancellationToken cancellationToken); } diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs index 521f9a6f7..5309e7622 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs @@ -10,7 +10,7 @@ namespace Microsoft.Performance.SDK.Processing.ColumnBuilding; public class ColumnBuilder { - private object? downloadSourceCommand = null; + private DownloadSourceCodeCommand? downloadSourceCommand = null; public ColumnBuilder( ColumnMetadata metadata, @@ -42,8 +42,8 @@ public ColumnBuilder( protected Func? VariantOptions { get; set; } = null; - public ColumnBuilder WithDownloadSourceCodeCommand( - DownloadSourceCodeCommand downloadSourceCommand) + public ColumnBuilder WithDownloadSourceCodeCommand( + DownloadSourceCodeCommand downloadSourceCommand) { this.downloadSourceCommand = downloadSourceCommand; return this; @@ -58,7 +58,7 @@ public ColumnBuilder WithVariants( public ITableBuilderWithRowCount AddColumn(ITableBuilderWithRowCount tableBuilder) { - DataColumnCommands commands = DataColumnCommands.Create(this.downloadSourceCommand); + DataColumnCommands commands = new(this.downloadSourceCommand); DataColumn dataColumn = BuildColumn(commands); From 5ab3b25d64000602a888f8719f4929a266ab08a4 Mon Sep 17 00:00:00 2001 From: Jayson Maxson Date: Thu, 10 Sep 2026 08:40:12 -0700 Subject: [PATCH 11/25] ColumnBuilder.AddCommands for consistency with variants. --- .../ColumnBuilding/ColumnBuilder`1.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs index 5309e7622..4defe2623 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs @@ -10,7 +10,7 @@ namespace Microsoft.Performance.SDK.Processing.ColumnBuilding; public class ColumnBuilder { - private DownloadSourceCodeCommand? downloadSourceCommand = null; + private DataColumnCommands? commands = null; public ColumnBuilder( ColumnMetadata metadata, @@ -42,10 +42,10 @@ public ColumnBuilder( protected Func? VariantOptions { get; set; } = null; - public ColumnBuilder WithDownloadSourceCodeCommand( - DownloadSourceCodeCommand downloadSourceCommand) + public ColumnBuilder WithCommands( + DataColumnCommands commands) { - this.downloadSourceCommand = downloadSourceCommand; + this.commands = commands; return this; } @@ -56,11 +56,10 @@ public ColumnBuilder WithVariants( return this; } - public ITableBuilderWithRowCount AddColumn(ITableBuilderWithRowCount tableBuilder) + public ITableBuilderWithRowCount AddToTable( + ITableBuilderWithRowCount tableBuilder) { - DataColumnCommands commands = new(this.downloadSourceCommand); - - DataColumn dataColumn = BuildColumn(commands); + DataColumn dataColumn = BuildColumn(this.commands); if (this.VariantOptions is not null) { @@ -70,7 +69,8 @@ public ITableBuilderWithRowCount AddColumn(ITableBuilderWithRowCount tableBuilde return tableBuilder.AddColumn(dataColumn); } - protected virtual DataColumn BuildColumn(DataColumnCommands? commands) + protected virtual DataColumn BuildColumn( + DataColumnCommands? commands) { return new(this.Configuration, this.Projection, commands); } From 9354150df79aa4c036bd035e75302b056c38107e Mon Sep 17 00:00:00 2001 From: Jayson Maxson Date: Thu, 10 Sep 2026 11:33:40 -0700 Subject: [PATCH 12/25] Add doc comments and cleanup. --- .../ColumnCommands/DataColumnCommands.cs | 43 ++++++- .../DownloadSourceCodeCommand.cs | 71 +++++++++++ .../DownloadSourceCodeResult.cs | 32 +++-- .../IDataColumnCommands.cs | 23 +++- .../ColumnBuilding/ColumnBuilder`1.cs | 111 +++++++++++++++++- .../HierchicalColumnBuilder`1.cs | 57 +++++++++ .../Processing/DataColumn.cs | 2 +- 7 files changed, 311 insertions(+), 28 deletions(-) diff --git a/src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs b/src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs index 1c68a6af0..803f00458 100644 --- a/src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs +++ b/src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs @@ -8,24 +8,59 @@ namespace Microsoft.Performance.SDK.ColumnCommands; /// -/// This class exposes commands on a given column. +/// 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. +/// 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; diff --git a/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeCommand.cs b/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeCommand.cs index 41999a528..e4997e1ae 100644 --- a/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeCommand.cs +++ b/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeCommand.cs @@ -3,17 +3,88 @@ 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 a +/// whose +/// the host can then open. +/// +/// +/// 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 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 URI via + /// . + /// + /// + /// A token that may be used to cancel the download operation. + /// + /// + /// A task that produces a + /// describing either the successful download (with a + /// the host can open) or + /// the failure (with an + /// the host can + /// surface). + /// public abstract System.Threading.Tasks.Task ExecuteAsync( object value, string downloadPath, diff --git a/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeResult.cs b/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeResult.cs index 06497e999..5bfe1aab6 100644 --- a/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeResult.cs +++ b/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeResult.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#nullable enable + using System; namespace Microsoft.Performance.SDK.ColumnCommands; @@ -45,8 +47,11 @@ public DownloadSourceCodeResult(Uri uri) /// /// 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. Its meaning, when - /// not null, is the attempted download URI. + /// 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. @@ -76,21 +81,14 @@ public DownloadSourceCodeResult(string errorMessage, Uri? uri) public string? ErrorMessage { get; } = null; /// - /// Gets the URI pointing to the downloaded source code that the - /// host should open. + /// 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; } } - - -/// Decision: Where to add the column commands. -/// A. Directly to the IDataColumn or IDataColumn<T> -/// B. In the ITableBuilder -/// -/// Reasons for A: -/// This is column data in much the same way the ColumnConfiguration or the Projection is. -/// -/// Reasons for B: -/// We've never added data to IColumnData types, but we have added to ITableBuilder. -/// This would follow the same behavior as column variants. -/// diff --git a/src/Microsoft.Performance.SDK/IDataColumnCommands.cs b/src/Microsoft.Performance.SDK/IDataColumnCommands.cs index e2a386f5f..7c02bd615 100644 --- a/src/Microsoft.Performance.SDK/IDataColumnCommands.cs +++ b/src/Microsoft.Performance.SDK/IDataColumnCommands.cs @@ -1,12 +1,25 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#nullable enable + using Microsoft.Performance.SDK.ColumnCommands; -namespace Microsoft.Performance.SDK.Processing +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 IDataColumnCommands { - public interface IDataColumnCommands - { - DataColumnCommands Commands { get; } - } + /// + /// 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`1.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs index 4defe2623..22fadec79 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs @@ -8,10 +8,39 @@ 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 call 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, @@ -25,6 +54,21 @@ public ColumnBuilder( 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) @@ -36,12 +80,36 @@ public ColumnBuilder( 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; 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) { @@ -49,6 +117,19 @@ public ColumnBuilder WithCommands( 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) { @@ -56,7 +137,21 @@ public ColumnBuilder WithVariants( return this; } - public ITableBuilderWithRowCount AddToTable( + /// + /// 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. + /// + public ITableBuilderWithRowCount AddColumnToTable( ITableBuilderWithRowCount tableBuilder) { DataColumn dataColumn = BuildColumn(this.commands); @@ -69,6 +164,20 @@ public ITableBuilderWithRowCount AddToTable( 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) { diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/HierchicalColumnBuilder`1.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/HierchicalColumnBuilder`1.cs index 6648de410..a9d25d89b 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/HierchicalColumnBuilder`1.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/HierchicalColumnBuilder`1.cs @@ -7,11 +7,40 @@ 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, @@ -22,6 +51,22 @@ public HierchicalColumnBuilder( 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, @@ -31,6 +76,18 @@ public HierchicalColumnBuilder( 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); diff --git a/src/Microsoft.Performance.SDK/Processing/DataColumn.cs b/src/Microsoft.Performance.SDK/Processing/DataColumn.cs index 206612e16..0ed064961 100644 --- a/src/Microsoft.Performance.SDK/Processing/DataColumn.cs +++ b/src/Microsoft.Performance.SDK/Processing/DataColumn.cs @@ -95,7 +95,7 @@ public DataColumn( this.Configuration = configuration; this.ProjectorInterface = projection.GetType(); this.Projector = projection; - this.Commands = dataColumnCommands; + this.Commands = dataColumnCommands ?? DataColumnCommands.Empty; } /// From c3b4b5bcb0c0dc25b91895efb95134458ed5c507 Mon Sep 17 00:00:00 2001 From: Jayson Maxson Date: Thu, 10 Sep 2026 11:40:33 -0700 Subject: [PATCH 13/25] Remov unnecessary updates. --- .../ColumnBuilding/Builders/EmptyColumnBuilder.cs | 4 ++-- .../Builders/ModalColumnWithModesBuilder.cs | 8 ++++---- .../Processing/ColumnBuilding/ColumnBuilder`1.cs | 2 +- .../Processing/ColumnBuilding/ModalColumnBuilder.cs | 2 +- .../Processing/ColumnBuilding/ToggleableColumnBuilder.cs | 2 +- src/Microsoft.Performance.SDK/Processing/DataColumn.cs | 2 +- .../Processing/HeirarchicalDataColumn.cs | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/EmptyColumnBuilder.cs b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/EmptyColumnBuilder.cs index 2ec6d15ba..612667f09 100644 --- a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/EmptyColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/EmptyColumnBuilder.cs @@ -1,14 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; +using System.Collections.Generic; using Microsoft.Performance.SDK.ColumnCommands; 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; diff --git a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs index 2cf73810c..1126aac22 100644 --- a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs +++ b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs @@ -1,16 +1,16 @@ // 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.ColumnCommands; 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; diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs index 22fadec79..d250d6a9a 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs @@ -3,8 +3,8 @@ #nullable enable -using Microsoft.Performance.SDK.ColumnCommands; using System; +using Microsoft.Performance.SDK.ColumnCommands; namespace Microsoft.Performance.SDK.Processing.ColumnBuilding; diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalColumnBuilder.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalColumnBuilder.cs index 41ce2ff2b..51e2d4021 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalColumnBuilder.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.Performance.SDK.ColumnCommands; using System; +using Microsoft.Performance.SDK.ColumnCommands; namespace Microsoft.Performance.SDK.Processing.ColumnBuilding; diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs index 8716b6cfa..47a5f6c14 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.Performance.SDK.ColumnCommands; using System; +using Microsoft.Performance.SDK.ColumnCommands; namespace Microsoft.Performance.SDK.Processing.ColumnBuilding; diff --git a/src/Microsoft.Performance.SDK/Processing/DataColumn.cs b/src/Microsoft.Performance.SDK/Processing/DataColumn.cs index 0ed064961..95f8dcee9 100644 --- a/src/Microsoft.Performance.SDK/Processing/DataColumn.cs +++ b/src/Microsoft.Performance.SDK/Processing/DataColumn.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.Performance.SDK.ColumnCommands; using System; +using Microsoft.Performance.SDK.ColumnCommands; namespace Microsoft.Performance.SDK.Processing { diff --git a/src/Microsoft.Performance.SDK/Processing/HeirarchicalDataColumn.cs b/src/Microsoft.Performance.SDK/Processing/HeirarchicalDataColumn.cs index 58d2b2255..8021f90e4 100644 --- a/src/Microsoft.Performance.SDK/Processing/HeirarchicalDataColumn.cs +++ b/src/Microsoft.Performance.SDK/Processing/HeirarchicalDataColumn.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.Performance.SDK.ColumnCommands; using System; +using Microsoft.Performance.SDK.ColumnCommands; namespace Microsoft.Performance.SDK.Processing { From e2d03a36373e04fcdac0d399a9fe4b46e5c5c967 Mon Sep 17 00:00:00 2001 From: Jayson Maxson Date: Thu, 10 Sep 2026 11:46:42 -0700 Subject: [PATCH 14/25] Update doc comments. --- .../ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs | 3 +++ .../ColumnBuilding/Builders/ToggledColumnBuilder.cs | 6 +++--- .../Processing/ColumnBuilding/ModalColumnBuilder.cs | 4 ++-- .../Processing/ColumnBuilding/ToggleableColumnBuilder.cs | 4 ++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs index 1126aac22..28d0ea880 100644 --- a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs +++ b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs @@ -124,6 +124,7 @@ public override ModalColumnBuilder WithMode( return WithMode(newMode); } + /// public override ModalColumnBuilder WithHierarchicalMode( ColumnVariantDescriptor modeDescriptor, IProjection projection, @@ -132,6 +133,7 @@ public override ModalColumnBuilder WithHierarchicalMode( return WithHierarchicalMode(modeDescriptor, projection, collectionProvider, null); } + /// public override ModalColumnBuilder WithHierarchicalMode( ColumnVariantDescriptor modeDescriptor, IProjection projection, @@ -141,6 +143,7 @@ public override ModalColumnBuilder WithHierarchicalMode( return WithHierarchicalMode(modeDescriptor, projection, collectionProvider, null, builder); } + /// public override ModalColumnBuilder WithHierarchicalMode( ColumnVariantDescriptor modeDescriptor, IProjection projection, diff --git a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnBuilder.cs b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnBuilder.cs index 78e6ca941..0ef6c4d0e 100644 --- a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnBuilder.cs @@ -1,15 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; +using System.Collections.Generic; +using System.Linq; using Microsoft.Performance.SDK.ColumnCommands; 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; diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalColumnBuilder.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalColumnBuilder.cs index 51e2d4021..b5cce74f0 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalColumnBuilder.cs @@ -105,7 +105,7 @@ public abstract ModalColumnBuilder WithMode( /// The projection that will be used to generate the column for this mode. /// /// - /// Data column commands. + /// The commands supported by this column variant. May be null. /// /// /// A callback that builds sub-variants of the added mode and returns its final column configuration. @@ -171,7 +171,7 @@ public abstract ModalColumnBuilder WithHierarchicalMode( /// The collection provider for the column. /// /// - /// Data column commands. + /// The commands supported by this column variant. May be null. /// /// /// A callback that builds sub-variants of the added mode and returns its final column configuration. diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs index 47a5f6c14..639fabff8 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs @@ -57,7 +57,7 @@ public abstract ToggleableColumnBuilder WithToggle( /// The projection that will be used to generate the column when this toggle is on. /// /// - /// Data column commands. + /// The commands supported by this column variant. May be null. /// /// /// The type of data that the projection will produce. @@ -123,7 +123,7 @@ public abstract ToggleableColumnBuilder WithHierarchicalToggle( /// The collection provider for the column. /// /// - /// Data column commands. + /// The commands supported by this column variant. May be null. /// /// /// The type of data that the projection will produce. From 7fbc890c77ffdb39d33c6b3a03089b66cf2ed32b Mon Sep 17 00:00:00 2001 From: Jayson Maxson Date: Thu, 10 Sep 2026 12:09:40 -0700 Subject: [PATCH 15/25] Update documentation. --- .../Advanced/Adding-Column-Commands.md | 161 ++++++++++++++++++ .../Advanced/Adding-Column-Variants.md | 2 + .../Using-the-SDK/Advanced/README.md | 1 + .../Using-the-SDK/Building-a-table.md | 6 + 4 files changed, 170 insertions(+) create mode 100644 documentation/Using-the-SDK/Advanced/Adding-Column-Commands.md 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..5bed59e99 --- /dev/null +++ b/documentation/Using-the-SDK/Advanced/Adding-Column-Commands.md @@ -0,0 +1,161 @@ +# 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 `IDataColumnCommands`. + +## 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 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 DownloadSourceCodeResult(new Uri(destinationPath)); + } + catch (Exception error) when (!(error is OperationCanceledException)) + { + return 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`. On success, return a `DownloadSourceCodeResult` containing the URI of the downloaded file. On failure, return an error message and, optionally, the 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` provides an equivalent fluent form and can also configure variants: + +```cs +new ColumnBuilder(sourceColumnConfiguration, sourceProjection) + .WithCommands(commands) + .AddColumnToTable(tableBuilderWithRowCount); +``` + +`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. Use the overloads that accept `DataColumnCommands` to attach commands to a toggle, mode, hierarchical toggle, or hierarchical mode: + +```cs +tableBuilderWithRowCount.AddColumnWithVariants( + sourceColumnConfiguration, + sourceProjection, + builder => builder.WithToggle( + localSourceDescriptor, + localSourceProjection, + commands)); +``` + +For a mode with child variants, pass the commands before the builder callback: + +```cs +return modesBuilder.WithMode( + sourceModeDescriptor, + sourceProjection, + commands, + modeBuilder => modeBuilder.WithToggle( + alternateSourceDescriptor, + alternateSourceProjection)); +``` + +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 `IDataColumnCommands`, then querying its `Commands` property: + +```cs +if (column is IDataColumnCommands columnWithCommands && + columnWithCommands.Commands.TryGetDownloadSourceCodeCommand(out var command) && + command.CanExecute(value, downloadPath)) +{ + DownloadSourceCodeResult result = + await command.ExecuteAsync(value, downloadPath, cancellationToken); + + if (result.Success) + { + Open(result.Uri); + } + else + { + ShowError(result.ErrorMessage); + } +} +``` + +Hosts should use `CommandName` as the user-facing action name. 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..5ebe5291a 100644 --- a/documentation/Using-the-SDK/Advanced/Adding-Column-Variants.md +++ b/documentation/Using-the-SDK/Advanced/Adding-Column-Variants.md @@ -332,4 +332,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 builder overloads that accept `DataColumnCommands`. 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. From e54780833c1deb581a032c3e5ef5551943630083 Mon Sep 17 00:00:00 2001 From: Jayson Maxson Date: Fri, 11 Sep 2026 09:33:36 -0700 Subject: [PATCH 16/25] Fix doc comment references. --- .../ColumnCommands/DataColumnCommands.cs | 6 +++--- .../Processing/ColumnBuilding/ToggleableColumnBuilder.cs | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs b/src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs index 803f00458..e261cd7ea 100644 --- a/src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs +++ b/src/Microsoft.Performance.SDK/ColumnCommands/DataColumnCommands.cs @@ -15,10 +15,10 @@ namespace Microsoft.Performance.SDK.ColumnCommands; /// that a host may perform against the column's row values. /// /// -/// This class works with both and -/// . Note that with the latter, +/// 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 +/// on the column. For /// this reason, individual command APIs operate on /// rather than a generic value type. /// diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs index 639fabff8..f6f2d3a08 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs @@ -80,9 +80,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. From c7f852f0cc18a90cd7803f21ca2328247983fd1b Mon Sep 17 00:00:00 2001 From: Jayson Maxson Date: Fri, 11 Sep 2026 10:00:18 -0700 Subject: [PATCH 17/25] Rename IDataColumnCommands. Update doc comments. --- ...aColumnCommands.cs => IDataColumnWithCommands.cs} | 2 +- .../Processing/ColumnBuilding/ColumnBuilder`1.cs | 2 +- .../ColumnBuilding/ToggleableColumnBuilder.cs | 12 +++--------- .../Processing/DataColumn.cs | 7 ++++++- 4 files changed, 11 insertions(+), 12 deletions(-) rename src/Microsoft.Performance.SDK/{IDataColumnCommands.cs => IDataColumnWithCommands.cs} (95%) diff --git a/src/Microsoft.Performance.SDK/IDataColumnCommands.cs b/src/Microsoft.Performance.SDK/IDataColumnWithCommands.cs similarity index 95% rename from src/Microsoft.Performance.SDK/IDataColumnCommands.cs rename to src/Microsoft.Performance.SDK/IDataColumnWithCommands.cs index 7c02bd615..7136c2df0 100644 --- a/src/Microsoft.Performance.SDK/IDataColumnCommands.cs +++ b/src/Microsoft.Performance.SDK/IDataColumnWithCommands.cs @@ -14,7 +14,7 @@ namespace Microsoft.Performance.SDK.Processing; /// to discover the commands the plugin has associated with the /// column. /// -public interface IDataColumnCommands +public interface IDataColumnWithCommands { /// /// Gets the associated with diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs index d250d6a9a..f2139bb54 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs @@ -102,7 +102,7 @@ public ColumnBuilder( /// 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. diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs index f6f2d3a08..5c757dcdb 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs @@ -22,9 +22,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. @@ -49,9 +47,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. @@ -110,9 +106,7 @@ public abstract ToggleableColumnBuilder WithHierarchicalToggle( /// 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. diff --git a/src/Microsoft.Performance.SDK/Processing/DataColumn.cs b/src/Microsoft.Performance.SDK/Processing/DataColumn.cs index 95f8dcee9..30e0e3ab4 100644 --- a/src/Microsoft.Performance.SDK/Processing/DataColumn.cs +++ b/src/Microsoft.Performance.SDK/Processing/DataColumn.cs @@ -15,7 +15,7 @@ namespace Microsoft.Performance.SDK.Processing /// public class DataColumn : IDataColumn, - IDataColumnCommands + IDataColumnWithCommands { /// /// Initializes a new instance of the @@ -110,6 +110,11 @@ 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; } /// From 6057b355896f6e61ee5980d2bb6da7498c67510d Mon Sep 17 00:00:00 2001 From: Jayson Maxson Date: Wed, 16 Sep 2026 09:00:11 -0700 Subject: [PATCH 18/25] Update documentation --- .../Advanced/Adding-Column-Commands.md | 42 ++++++++++++------- .../DownloadSourceCodeCommand.cs | 26 ++++++------ .../DownloadSourceCodeResult.cs | 11 ++--- 3 files changed, 46 insertions(+), 33 deletions(-) diff --git a/documentation/Using-the-SDK/Advanced/Adding-Column-Commands.md b/documentation/Using-the-SDK/Advanced/Adding-Column-Commands.md index 5bed59e99..96ae10392 100644 --- a/documentation/Using-the-SDK/Advanced/Adding-Column-Commands.md +++ b/documentation/Using-the-SDK/Advanced/Adding-Column-Commands.md @@ -33,16 +33,19 @@ public sealed class DownloadSourceCommand && !string.IsNullOrWhiteSpace(downloadPath); } - public override async Task ExecuteAsync( + public override async Task ExecuteAsync( object value, string downloadPath, CancellationToken cancellationToken) { if (!CanExecute(value, downloadPath)) { - return new DownloadSourceCodeResult( - "The selected value does not identify downloadable source code.", - value as Uri); + return new[] + { + new DownloadSourceCodeResult( + "The selected value does not identify downloadable source code.", + value as Uri), + }; } var sourceUri = (Uri)value; @@ -66,11 +69,17 @@ public sealed class DownloadSourceCommand } } - return new DownloadSourceCodeResult(new Uri(destinationPath)); + return new[] + { + new DownloadSourceCodeResult(new Uri(destinationPath)), + }; } catch (Exception error) when (!(error is OperationCanceledException)) { - return new DownloadSourceCodeResult(error.Message, sourceUri); + return new[] + { + new DownloadSourceCodeResult(error.Message, sourceUri), + }; } } } @@ -78,7 +87,7 @@ public sealed class DownloadSourceCommand 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`. On success, return a `DownloadSourceCodeResult` containing the URI of the downloaded file. On failure, return an error message and, optionally, the remote source URI. Allow cancellation to propagate as an `OperationCanceledException`. +`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. @@ -144,18 +153,21 @@ if (column is IDataColumnCommands columnWithCommands && columnWithCommands.Commands.TryGetDownloadSourceCodeCommand(out var command) && command.CanExecute(value, downloadPath)) { - DownloadSourceCodeResult result = + DownloadSourceCodeResult[] results = await command.ExecuteAsync(value, downloadPath, cancellationToken); - if (result.Success) - { - Open(result.Uri); - } - else + foreach (DownloadSourceCodeResult result in results) { - ShowError(result.ErrorMessage); + if (result.Success) + { + Open(result.Uri); + } + else + { + ShowError(result.ErrorMessage); + } } } ``` -Hosts should use `CommandName` as the user-facing action name. 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 +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/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeCommand.cs b/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeCommand.cs index e4997e1ae..e08023425 100644 --- a/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeCommand.cs +++ b/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeCommand.cs @@ -7,9 +7,8 @@ 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 a -/// whose -/// the host can then open. +/// and invoke to obtain the results of the +/// source code download attempts. /// /// /// Implementations must be safe to call from a host on an arbitrary @@ -61,7 +60,7 @@ protected DownloadSourceCodeCommand(string commandName) public abstract bool CanExecute(object value, string downloadPath); /// - /// Asynchronously downloads the source code associated with the + /// Asynchronously downloads the source code files associated with the /// specified row value to the specified location. /// /// @@ -71,21 +70,22 @@ protected DownloadSourceCodeCommand(string commandName) /// /// The local path under which the source code should be /// downloaded. Implementations decide the exact file layout beneath - /// this path and return the resulting URI via - /// . + /// 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 a - /// describing either the successful download (with a - /// the host can open) or - /// the failure (with an - /// the host can - /// surface). + /// 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( + 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 index 5bfe1aab6..515d8d104 100644 --- a/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeResult.cs +++ b/src/Microsoft.Performance.SDK/ColumnCommands/DownloadSourceCodeResult.cs @@ -8,10 +8,11 @@ namespace Microsoft.Performance.SDK.ColumnCommands; /// -/// The result of 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. +/// 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 { @@ -66,7 +67,7 @@ public DownloadSourceCodeResult(string errorMessage, Uri? uri) } /// - /// Gets a value indicating whether the command completed + /// 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. From 454351b96e29d0a80799a5251b48bfcde66724d3 Mon Sep 17 00:00:00 2001 From: Jayson Maxson Date: Wed, 16 Sep 2026 09:29:49 -0700 Subject: [PATCH 19/25] Address some PR feedback. --- .../Advanced/Adding-Column-Commands.md | 8 +++---- .../TableBuilderTests.cs | 24 +++++++++++++++++++ .../TableBuilder.cs | 8 +++++++ .../ColumnBuilding/ColumnBuilder`1.cs | 9 +++---- .../Processing/ITableBuilder.cs | 17 +++++++++++++ .../RuntimeExecutionResults.cs | 8 +++++++ 6 files changed, 66 insertions(+), 8 deletions(-) diff --git a/documentation/Using-the-SDK/Advanced/Adding-Column-Commands.md b/documentation/Using-the-SDK/Advanced/Adding-Column-Commands.md index 96ae10392..ea58dc725 100644 --- a/documentation/Using-the-SDK/Advanced/Adding-Column-Commands.md +++ b/documentation/Using-the-SDK/Advanced/Adding-Column-Commands.md @@ -102,12 +102,12 @@ tableBuilderWithRowCount.AddColumn( new DataColumn(sourceColumnConfiguration, sourceProjection, commands)); ``` -The strongly typed `ColumnBuilder` provides an equivalent fluent form and can also configure variants: +The strongly typed `ColumnBuilderBuilder` can be used with `ITableBuilderWithRowCount` to add a column to a table: ```cs -new ColumnBuilder(sourceColumnConfiguration, sourceProjection) - .WithCommands(commands) - .AddColumnToTable(tableBuilderWithRowCount); +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. 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/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/Processing/ColumnBuilding/ColumnBuilder`1.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs index f2139bb54..6898e4431 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ColumnBuilder`1.cs @@ -12,8 +12,9 @@ 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 call to materialize -/// the column on a table. +/// 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. @@ -96,7 +97,7 @@ public ColumnBuilder( /// the built column, or null if no variants have been /// configured via . /// - protected Func? VariantOptions { get; set; } = null; + protected Func? VariantOptions { get; private set; } = null; /// /// Associates the specified with @@ -151,7 +152,7 @@ public ColumnBuilder WithVariants( /// The , to allow chaining additional /// table-building calls. /// - public ITableBuilderWithRowCount AddColumnToTable( + internal ITableBuilderWithRowCount AddColumnToTable( ITableBuilderWithRowCount tableBuilder) { DataColumn dataColumn = BuildColumn(this.commands); 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, From af730e7535a44a4bc10f50f3d751ea18e0313110 Mon Sep 17 00:00:00 2001 From: Jayson Maxson Date: Wed, 16 Sep 2026 14:21:55 -0700 Subject: [PATCH 20/25] Empty From 2e63f3faf5f5311dfce927068b948bd91789a58f Mon Sep 17 00:00:00 2001 From: Jayson Maxson Date: Fri, 18 Sep 2026 11:10:52 -0700 Subject: [PATCH 21/25] Modal variant doc comments and cleanup --- .../ColumnBuilding/ModalColumnBuilderTests.cs | 6 +- .../ModesBuilderCallbackInvoker.cs | 6 +- .../Builders/EmptyColumnBuilder.cs | 6 +- .../Builders/ModalColumnWithModesBuilder.cs | 82 ++++++++-------- .../Builders/ModalVariantBuilder`1.cs | 94 +++++++++++++++++++ .../ColumnBuilding/ModalColumnBuilder.cs | 94 +++++++++---------- .../Processing/ColumnBuilding/ModalVariant.cs | 13 +++ .../ColumnBuilding/ModalVariantBuilder.cs | 67 +++++++++++++ 8 files changed, 266 insertions(+), 102 deletions(-) create mode 100644 src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalVariantBuilder`1.cs create mode 100644 src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalVariant.cs create mode 100644 src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalVariantBuilder.cs diff --git a/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ModalColumnBuilderTests.cs b/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ModalColumnBuilderTests.cs index fa7835da0..c774b0d99 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; @@ -121,7 +121,7 @@ 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/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 612667f09..01e6266a6 100644 --- a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/EmptyColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/EmptyColumnBuilder.cs @@ -1,14 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Collections.Generic; using Microsoft.Performance.SDK.ColumnCommands; 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; @@ -144,7 +144,7 @@ public override ModalColumnBuilder WithModes( return new ModalColumnWithModesBuilder( processor, [ - new ModalColumnWithModesBuilder.AddedMode( + new ModalVariant( new ColumnVariantDescriptor(baseColumn.Configuration.Metadata.Guid, baseProjectionProperties), baseColumn, builder), diff --git a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs index 28d0ea880..d0ca1d7df 100644 --- a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs +++ b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs @@ -1,16 +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.ColumnCommands; 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; @@ -22,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 /// @@ -47,7 +41,7 @@ internal record AddedMode( /// public ModalColumnWithModesBuilder( IColumnVariantsProcessor processor, - List addedModes, + List addedModes, IDataColumn baseColumn, int? defaultModeIndex) { @@ -67,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); @@ -96,29 +90,18 @@ public override ModalColumnBuilder WithMode( ColumnVariantDescriptor modeDescriptor, IProjection projection, Func builder) - { - return WithMode(modeDescriptor, projection, default, builder); - } - - /// - public override ModalColumnBuilder WithMode( - ColumnVariantDescriptor modeDescriptor, - IProjection projection, - DataColumnCommands dataColumnCommands, - Func builder) { 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 }, }, - projection, - dataColumnCommands), + projection), builder); return WithMode(newMode); @@ -139,23 +122,12 @@ public override ModalColumnBuilder WithHierarchicalMode( IProjection projection, ICollectionInfoProvider collectionProvider, Func builder) - { - return WithHierarchicalMode(modeDescriptor, projection, collectionProvider, null, builder); - } - - /// - public override ModalColumnBuilder WithHierarchicalMode( - ColumnVariantDescriptor modeDescriptor, - IProjection projection, - ICollectionInfoProvider collectionProvider, - DataColumnCommands dataColumnCommands, - Func builder) { Guard.NotNull(modeDescriptor, nameof(modeDescriptor)); Guard.NotNull(projection, nameof(projection)); Guard.NotNull(collectionProvider, nameof(collectionProvider)); - AddedMode newMode = new( + ModalVariant newMode = new( modeDescriptor, new HierarchicalDataColumn( new ColumnConfiguration(this.baseColumn.Configuration) @@ -163,13 +135,35 @@ public override ModalColumnBuilder WithHierarchicalMode( Metadata = new ColumnMetadata(this.baseColumn.Configuration.Metadata) { Name = modeDescriptor.Properties.ColumnName ?? this.baseColumn.Configuration.Metadata.Name }, }, projection, - collectionProvider, - dataColumnCommands), + collectionProvider), builder); return WithMode(newMode); } + public override ModalColumnBuilder WithModalBuilder( + ColumnVariantDescriptor modeDescriptor, + IProjection projection, + Func 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) + { + ModalVariantBuilder variantBuilder = new ModalVariantBuilder(modeDescriptor, projection, collectionProvider); + variantBuilder = buildVariant(variantBuilder); + + return WithMode(variantBuilder.CreateVariant(this.baseColumn)); + } + /// public override ColumnBuilder WithDefaultMode(Guid modeIdentifierGuid) { @@ -202,7 +196,7 @@ public override ColumnBuilder WithDefaultMode(Guid modeIdentifierGuid) index); } - private ModalColumnBuilder WithMode(AddedMode newMode) + private ModalColumnBuilder WithMode(ModalVariant newMode) { return new ModalColumnWithModesBuilder( this.processor, @@ -210,4 +204,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/Processing/ColumnBuilding/ModalColumnBuilder.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalColumnBuilder.cs index b5cce74f0..39c9cb316 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ModalColumnBuilder.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using Microsoft.Performance.SDK.ColumnCommands; namespace Microsoft.Performance.SDK.Processing.ColumnBuilding; @@ -96,7 +95,7 @@ public abstract ModalColumnBuilder WithMode( Func builder); /// - /// Adds a mode to the column. + /// Adds a hierarchical mode to the column. /// /// /// The for the mode. @@ -104,8 +103,8 @@ public abstract ModalColumnBuilder WithMode( /// /// The projection that will be used to generate the column for this mode. /// - /// - /// The commands supported by this column variant. May be null. + /// + /// The collection provider for the column. /// /// /// A callback that builds sub-variants of the added mode and returns its final column configuration. @@ -118,16 +117,38 @@ public abstract ModalColumnBuilder WithMode( /// configured with the new mode. /// /// - /// or is null. + /// , , + /// or is null. /// - public abstract ModalColumnBuilder WithMode( + public abstract ModalColumnBuilder WithHierarchicalMode( ColumnVariantDescriptor modeDescriptor, IProjection projection, - DataColumnCommands dataColumnCommands, + ICollectionInfoProvider collectionProvider, Func builder); /// - /// Adds a hierarchical mode to the column. + /// Sets the default mode for the column. + /// + /// + /// The of the that + /// identifies the mode to set as the default. + /// + /// + /// A new instance of that has been + /// configured with the default mode. + /// + /// + /// The mode with the given has not been + /// added as an available mode. + /// + /// + /// If this method is not called, the first mode added will be the default mode. + /// + public abstract ColumnBuilder WithDefaultMode(Guid modeIdentifierGuid); + + /// + /// Adds a mode to the column whose sub-variants are built by a + /// . /// /// /// The for the mode. @@ -135,11 +156,9 @@ public abstract ModalColumnBuilder WithMode( /// /// The projection that will be used to generate the column for this mode. /// - /// - /// The collection provider for the column. - /// - /// - /// A callback that builds sub-variants of the added mode and returns its final column configuration. + /// + /// 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. @@ -149,17 +168,17 @@ public abstract ModalColumnBuilder WithMode( /// configured with the new mode. /// /// - /// , , - /// or is null. + /// , , or + /// is null. /// - public abstract ModalColumnBuilder WithHierarchicalMode( + public abstract ModalColumnBuilder WithModalBuilder( ColumnVariantDescriptor modeDescriptor, IProjection projection, - ICollectionInfoProvider collectionProvider, - Func builder); + Func buildVariant); /// - /// Adds a hierarchical mode to the column. + /// Adds a hierarchical mode to the column whose sub-variants are built by a + /// . /// /// /// The for the mode. @@ -170,11 +189,9 @@ public abstract ModalColumnBuilder WithHierarchicalMode( /// /// The collection provider for the column. /// - /// - /// The commands supported by this column variant. May be null. - /// - /// - /// A callback that builds sub-variants of the added mode and returns its final column configuration. + /// + /// 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. @@ -185,32 +202,11 @@ public abstract ModalColumnBuilder WithHierarchicalMode( /// /// /// , , - /// or is null. + /// , or is null. /// - public abstract ModalColumnBuilder WithHierarchicalMode( + public abstract ModalColumnBuilder WithHierarchicalModalBuilder( ColumnVariantDescriptor modeDescriptor, IProjection projection, ICollectionInfoProvider collectionProvider, - DataColumnCommands dataColumnCommands, - Func builder); - - /// - /// Sets the default mode for the column. - /// - /// - /// The of the that - /// identifies the mode to set as the default. - /// - /// - /// A new instance of that has been - /// configured with the default mode. - /// - /// - /// The mode with the given has not been - /// added as an available mode. - /// - /// - /// 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 + 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); +} From d1d6b73161b681e2873511d9b7b312074eb80331 Mon Sep 17 00:00:00 2001 From: Jayson Maxson Date: Fri, 18 Sep 2026 11:48:36 -0700 Subject: [PATCH 22/25] Toggleable column variants support variant builders. --- .../ColumnBuilding/EmptyColumnBuilderTests.cs | 2 +- .../ToggledColumnBuilderTests.cs | 4 +- ...ggledColumnWithToggledModesBuilderTests.cs | 4 +- .../Builders/EmptyColumnBuilder.cs | 77 ++++++---- .../Builders/ToggledColumnBuilder.cs | 90 +++++++----- .../ToggledColumnWithToggledModesBuilder.cs | 5 +- .../Builders/ToggledVariantBuilder`1.cs | 131 ++++++++++++++++++ .../ColumnBuilding/ToggleableColumnBuilder.cs | 92 ++++++------ .../ColumnBuilding/ToggleableVariant.cs | 10 ++ .../ToggleableVariantBuilder.cs | 43 ++++++ 10 files changed, 343 insertions(+), 115 deletions(-) create mode 100644 src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledVariantBuilder`1.cs create mode 100644 src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableVariant.cs create mode 100644 src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableVariantBuilder.cs diff --git a/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/EmptyColumnBuilderTests.cs b/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/EmptyColumnBuilderTests.cs index 94ec8c397..cb57ea091 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; diff --git a/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ToggledColumnBuilderTests.cs b/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ToggledColumnBuilderTests.cs index 7b7dc7dbd..42ab05e04 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; @@ -109,7 +109,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/ColumnBuilding/Builders/EmptyColumnBuilder.cs b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/EmptyColumnBuilder.cs index 01e6266a6..a9c6cec71 100644 --- a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/EmptyColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/EmptyColumnBuilder.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.Performance.SDK.ColumnCommands; using Microsoft.Performance.SDK.Processing; using Microsoft.Performance.SDK.Processing.ColumnBuilding; using Microsoft.Performance.SDK.Runtime.ColumnBuilding.Builders.CallbackInvokers; @@ -48,29 +47,19 @@ internal override void Commit() public override ToggleableColumnBuilder WithToggle( ColumnVariantDescriptor toggleDescriptor, IProjection projection) - { - return WithToggle(toggleDescriptor, projection, default); - } - - /// - public override ToggleableColumnBuilder WithToggle( - ColumnVariantDescriptor toggleDescriptor, - IProjection projection, - DataColumnCommands dataColumnCommands) { Guard.NotNull(toggleDescriptor, nameof(toggleDescriptor)); Guard.NotNull(projection, nameof(projection)); return new ToggledColumnBuilder( [ - new ToggledColumnBuilder.AddedToggle(toggleDescriptor, + new ToggleableVariant(toggleDescriptor, new DataColumn( new ColumnConfiguration(this.baseColumn.Configuration) { Metadata = new ColumnMetadata(this.baseColumn.Configuration.Metadata) { Name = toggleDescriptor.Properties.ColumnName ?? this.baseColumn.Configuration.Metadata.Name}, }, - projection, - dataColumnCommands)), + projection)), ], baseColumn, processor); @@ -81,16 +70,6 @@ public override ToggleableColumnBuilder WithHierarchicalToggle( ColumnVariantDescriptor toggleDescriptor, IProjection projection, ICollectionInfoProvider collectionProvider) - { - return WithHierarchicalToggle(toggleDescriptor, projection, collectionProvider, default); - } - - /// - public override ToggleableColumnBuilder WithHierarchicalToggle( - ColumnVariantDescriptor toggleDescriptor, - IProjection projection, - ICollectionInfoProvider collectionProvider, - DataColumnCommands dataColumnCommands) { Guard.NotNull(toggleDescriptor, nameof(toggleDescriptor)); Guard.NotNull(projection, nameof(projection)); @@ -98,20 +77,47 @@ public override ToggleableColumnBuilder WithHierarchicalToggle( return new ToggledColumnBuilder( [ - new ToggledColumnBuilder.AddedToggle(toggleDescriptor, + new ToggleableVariant(toggleDescriptor, new HierarchicalDataColumn( new ColumnConfiguration(this.baseColumn.Configuration) { Metadata = new ColumnMetadata(this.baseColumn.Configuration.Metadata) { Name = toggleDescriptor.Properties.ColumnName ?? this.baseColumn.Configuration.Metadata.Name}, }, projection, - collectionProvider, - dataColumnCommands)), + collectionProvider)), ], baseColumn, 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, @@ -120,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), @@ -152,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/ToggledColumnBuilder.cs b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnBuilder.cs index 0ef6c4d0e..c21bfc60f 100644 --- a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnBuilder.cs @@ -1,15 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Performance.SDK.ColumnCommands; 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; @@ -19,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; @@ -40,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) { @@ -59,30 +55,20 @@ internal override void Commit() public override ToggleableColumnBuilder WithToggle( ColumnVariantDescriptor toggleDescriptor, IProjection projection) - { - return WithToggle(toggleDescriptor, projection, default); - } - - /// - public override ToggleableColumnBuilder WithToggle( - ColumnVariantDescriptor toggleDescriptor, - IProjection projection, - DataColumnCommands dataColumnCommands) { Guard.NotNull(toggleDescriptor, nameof(toggleDescriptor)); Guard.NotNull(projection, nameof(projection)); return new ToggledColumnBuilder( this.toggles.Append( - new AddedToggle( + new ToggleableVariant( toggleDescriptor, new DataColumn( new ColumnConfiguration(this.baseColumn.Configuration) { Metadata = new ColumnMetadata(this.baseColumn.Configuration.Metadata) { Name = toggleDescriptor.Properties.ColumnName ?? this.baseColumn.Configuration.Metadata.Name }, }, - projection, - dataColumnCommands)) + projection)) ).ToList(), this.baseColumn, this.processor); @@ -93,16 +79,6 @@ public override ToggleableColumnBuilder WithHierarchicalToggle( ColumnVariantDescriptor toggleDescriptor, IProjection projection, ICollectionInfoProvider collectionProvider) - { - return WithHierarchicalToggle(toggleDescriptor, projection, collectionProvider, default); - } - - /// - public override ToggleableColumnBuilder WithHierarchicalToggle( - ColumnVariantDescriptor toggleDescriptor, - IProjection projection, - ICollectionInfoProvider collectionProvider, - DataColumnCommands dataColumnCommands) { Guard.NotNull(toggleDescriptor, nameof(toggleDescriptor)); Guard.NotNull(projection, nameof(projection)); @@ -110,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) @@ -118,13 +94,38 @@ public override ToggleableColumnBuilder WithHierarchicalToggle( Metadata = new ColumnMetadata(this.baseColumn.Configuration.Metadata) { Name = toggleDescriptor.Properties.ColumnName ?? this.baseColumn.Configuration.Metadata.Name }, }, projection, - collectionProvider, - dataColumnCommands)) + collectionProvider)) ).ToList(), baseColumn, processor); } + /// + public override ToggleableColumnBuilder WithToggleableBuilder( + ColumnVariantDescriptor toggleDescriptor, + IProjection projection, + Func buildVariant) + { + Guard.NotNull(toggleDescriptor, nameof(toggleDescriptor)); + Guard.NotNull(projection, nameof(projection)); + + 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)); + + return CreateFromBuilder(toggleDescriptor, projection, collectionProvider, buildVariant); + } + /// public override ColumnBuilder WithToggledModes( string toggleText, @@ -146,7 +147,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; @@ -156,4 +157,21 @@ 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( + [ + variantBuilder.CreateVariant(this.baseColumn), + ], + 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..41cacdf8a --- /dev/null +++ b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledVariantBuilder`1.cs @@ -0,0 +1,131 @@ +// 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 }, + }, + projection, + collectionProvider)); + } +} \ No newline at end of file diff --git a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs index 5c757dcdb..fd067ab63 100644 --- a/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK/Processing/ColumnBuilding/ToggleableColumnBuilder.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using Microsoft.Performance.SDK.ColumnCommands; namespace Microsoft.Performance.SDK.Processing.ColumnBuilding; @@ -52,8 +51,8 @@ public abstract ToggleableColumnBuilder WithToggle( /// /// The projection that will be used to generate the column when this toggle is on. /// - /// - /// The commands supported by this column variant. May be null. + /// + /// The collection provider for the column. /// /// /// The type of data that the projection will produce. @@ -63,17 +62,39 @@ public abstract ToggleableColumnBuilder WithToggle( /// configured with the added toggle. /// /// - /// or is null. + /// , , + /// or is null. /// - public abstract ToggleableColumnBuilder WithToggle( + public abstract ToggleableColumnBuilder WithHierarchicalToggle( ColumnVariantDescriptor toggleDescriptor, IProjection projection, - DataColumnCommands dataColumnCommands); + ICollectionInfoProvider collectionProvider); /// - /// Adds a new toggleable variant to the column. The added toggleable variant - /// is nested at the "end" of the chain of toggleable variants already - /// added via calls to this method. + /// Adds a set of modes to the column that are nested inside of a toggle with no + /// associated projection. + /// + /// + /// The text to display for the toggle that represents the modes. + /// + /// + /// A callback that builds the modes and returns the final column configuration. + /// + /// + /// A new instance of that has been + /// configured with the added toggled modes. + /// + /// + /// is null. + /// + public abstract ColumnBuilder WithToggledModes( + string toggleText, + Func builder); + + /// + /// 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. @@ -81,8 +102,9 @@ public abstract ToggleableColumnBuilder WithToggle( /// /// 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. @@ -92,18 +114,18 @@ public abstract ToggleableColumnBuilder WithToggle( /// configured with the added toggle. /// /// - /// , , - /// or is null. + /// , , or + /// is null. /// - public abstract ToggleableColumnBuilder WithHierarchicalToggle( + public abstract ToggleableColumnBuilder WithToggleableBuilder( ColumnVariantDescriptor toggleDescriptor, IProjection projection, - ICollectionInfoProvider collectionProvider); + Func buildVariant); /// - /// Adds a new toggleable variant to the column. The added toggleable variant - /// is nested at the "end" of the chain of toggleable variants already - /// added via calls to this method. + /// 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. @@ -114,8 +136,9 @@ public abstract ToggleableColumnBuilder WithHierarchicalToggle( /// /// The collection provider for the column. /// - /// - /// The commands supported by this column variant. May be null. + /// + /// 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. @@ -126,32 +149,11 @@ public abstract ToggleableColumnBuilder WithHierarchicalToggle( /// /// /// , , - /// or is null. + /// , or is null. /// - public abstract ToggleableColumnBuilder WithHierarchicalToggle( + public abstract ToggleableColumnBuilder WithHierarchicalToggleableBuilder( ColumnVariantDescriptor toggleDescriptor, IProjection projection, ICollectionInfoProvider collectionProvider, - DataColumnCommands dataColumnCommands); - - /// - /// Adds a set of modes to the column that are nested inside of a toggle with no - /// associated projection. - /// - /// - /// The text to display for the toggle that represents the modes. - /// - /// - /// A callback that builds the modes and returns the final column configuration. - /// - /// - /// A new instance of that has been - /// configured with the added toggled modes. - /// - /// - /// is null. - /// - public abstract ColumnBuilder WithToggledModes( - string toggleText, - Func builder); -} \ No newline at end of file + 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 From 22dceab140263d283681e66cc863a4f081b009c2 Mon Sep 17 00:00:00 2001 From: Jayson Maxson Date: Fri, 18 Sep 2026 12:08:30 -0700 Subject: [PATCH 23/25] Fixes to CreateHierarchicalToggleableVariant method. --- .../ColumnBuilding/Builders/ToggledVariantBuilder`1.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledVariantBuilder`1.cs b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledVariantBuilder`1.cs index 41cacdf8a..a1f1917f0 100644 --- a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledVariantBuilder`1.cs +++ b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledVariantBuilder`1.cs @@ -125,7 +125,8 @@ private ToggleableVariant CreateHierarchicalToggleableVariant(IDataColumn baseCo { Metadata = new ColumnMetadata(baseColumn.Configuration.Metadata) { Name = this.toggleDescriptor.Properties.ColumnName ?? baseColumn.Configuration.Metadata.Name }, }, - projection, - collectionProvider)); + this.projection, + this.collectionProvider, + this.commands)); } } \ No newline at end of file From 8779cf05bddc3616475fe64cefcb5a2b9706147f Mon Sep 17 00:00:00 2001 From: Jayson Maxson Date: Fri, 18 Sep 2026 12:38:11 -0700 Subject: [PATCH 24/25] Fixes and new tests. Update documentation for fluent change. --- .../Advanced/Adding-Column-Commands.md | 27 ++-- .../Advanced/Adding-Column-Variants.md | 27 +++- .../ColumnBuilding/EmptyColumnBuilderTests.cs | 99 +++++++++++++ .../ColumnBuilding/ModalColumnBuilderTests.cs | 93 ++++++++++++ .../ModalVariantBuilderTests.cs | 133 ++++++++++++++++++ .../ToggleableVariantBuilderTests.cs | 104 ++++++++++++++ .../ToggledColumnBuilderTests.cs | 99 +++++++++++++ .../Builders/ModalColumnWithModesBuilder.cs | 9 ++ .../Builders/ToggledColumnBuilder.cs | 6 +- .../ColumnVariantsTests.cs | 100 +++++++++++++ 10 files changed, 680 insertions(+), 17 deletions(-) create mode 100644 src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ModalVariantBuilderTests.cs create mode 100644 src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ToggleableVariantBuilderTests.cs diff --git a/documentation/Using-the-SDK/Advanced/Adding-Column-Commands.md b/documentation/Using-the-SDK/Advanced/Adding-Column-Commands.md index ea58dc725..42645da40 100644 --- a/documentation/Using-the-SDK/Advanced/Adding-Column-Commands.md +++ b/documentation/Using-the-SDK/Advanced/Adding-Column-Commands.md @@ -2,7 +2,7 @@ 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 `IDataColumnCommands`. +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 @@ -114,31 +114,32 @@ tableBuilderWithRowCount.AddColumn( ## 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. Use the overloads that accept `DataColumnCommands` to attach commands to a toggle, mode, hierarchical toggle, or hierarchical mode: +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.WithToggle( + builder => builder.WithToggleableBuilder( localSourceDescriptor, localSourceProjection, - commands)); + variantBuilder => variantBuilder.WithCommands(commands))); ``` -For a mode with child variants, pass the commands before the builder callback: +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.WithMode( +return modesBuilder.WithModalBuilder( sourceModeDescriptor, sourceProjection, - commands, - modeBuilder => modeBuilder.WithToggle( - alternateSourceDescriptor, - alternateSourceProjection)); + variantBuilder => variantBuilder + .WithCommands(commands) + .WithBuilder(modeBuilder => modeBuilder.WithToggle( + alternateSourceDescriptor, + alternateSourceProjection))); ``` -Attach commands only to variants whose projected values the command understands. +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 @@ -146,10 +147,10 @@ For a `HierarchicalDataColumn`, the value supplied to `CanExecute` and `Execu ## Host Discovery -A host discovers commands by testing whether an `IDataColumn` implements `IDataColumnCommands`, then querying its `Commands` property: +A host discovers commands by testing whether an `IDataColumn` implements `IDataColumnWithCommands`, then querying its `Commands` property: ```cs -if (column is IDataColumnCommands columnWithCommands && +if (column is IDataColumnWithCommands columnWithCommands && columnWithCommands.Commands.TryGetDownloadSourceCodeCommand(out var command) && command.CanExecute(value, downloadPath)) { diff --git a/documentation/Using-the-SDK/Advanced/Adding-Column-Variants.md b/documentation/Using-the-SDK/Advanced/Adding-Column-Variants.md index 5ebe5291a..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,6 +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 builder overloads that accept `DataColumnCommands`. 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. +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/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/EmptyColumnBuilderTests.cs b/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/EmptyColumnBuilderTests.cs index cb57ea091..66a8af567 100644 --- a/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/EmptyColumnBuilderTests.cs +++ b/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/EmptyColumnBuilderTests.cs @@ -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 c774b0d99..b9363a6dd 100644 --- a/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ModalColumnBuilderTests.cs +++ b/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ModalColumnBuilderTests.cs @@ -117,6 +117,99 @@ 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( 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 42ab05e04..ebd94d2a2 100644 --- a/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ToggledColumnBuilderTests.cs +++ b/src/Microsoft.Performance.SDK.Runtime.Tests/ColumnBuilding/ToggledColumnBuilderTests.cs @@ -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( diff --git a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs index d0ca1d7df..9e6d19d12 100644 --- a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs +++ b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ModalColumnWithModesBuilder.cs @@ -146,6 +146,10 @@ public override ModalColumnBuilder WithModalBuilder( 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); @@ -158,6 +162,11 @@ public override ModalColumnBuilder WithHierarchicalModalBuilder( 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); diff --git a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnBuilder.cs b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnBuilder.cs index c21bfc60f..ae1cab66a 100644 --- a/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnBuilder.cs +++ b/src/Microsoft.Performance.SDK.Runtime/ColumnBuilding/Builders/ToggledColumnBuilder.cs @@ -108,6 +108,7 @@ public override ToggleableColumnBuilder WithToggleableBuilder( { Guard.NotNull(toggleDescriptor, nameof(toggleDescriptor)); Guard.NotNull(projection, nameof(projection)); + Guard.NotNull(buildVariant, nameof(buildVariant)); return CreateFromBuilder(toggleDescriptor, projection, null, buildVariant); } @@ -122,6 +123,7 @@ public override ToggleableColumnBuilder WithHierarchicalToggleableBuilder( 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); } @@ -168,9 +170,7 @@ private ToggleableColumnBuilder CreateFromBuilder( variantBuilder = buildVariant(variantBuilder); return new ToggledColumnBuilder( - [ - variantBuilder.CreateVariant(this.baseColumn), - ], + this.toggles.Append(variantBuilder.CreateVariant(this.baseColumn)).ToList(), baseColumn, processor); } 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 From 8bd6b3921f2dd9c2e2012cf185639635e462263e Mon Sep 17 00:00:00 2001 From: Jayson Maxson Date: Tue, 22 Sep 2026 09:09:11 -0700 Subject: [PATCH 25/25] Fix typo --- documentation/Using-the-SDK/Advanced/Adding-Column-Commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/Using-the-SDK/Advanced/Adding-Column-Commands.md b/documentation/Using-the-SDK/Advanced/Adding-Column-Commands.md index 42645da40..b85d68272 100644 --- a/documentation/Using-the-SDK/Advanced/Adding-Column-Commands.md +++ b/documentation/Using-the-SDK/Advanced/Adding-Column-Commands.md @@ -102,7 +102,7 @@ tableBuilderWithRowCount.AddColumn( new DataColumn(sourceColumnConfiguration, sourceProjection, commands)); ``` -The strongly typed `ColumnBuilderBuilder` can be used with `ITableBuilderWithRowCount` to add a column to a table: +The strongly typed `ColumnBuilder` can be used with `ITableBuilderWithRowCount` to add a column to a table: ```cs tableBuilderWithRowCount.AddColumn(