From 31224981fd0b6b2a1e2e42cc115803716a470950 Mon Sep 17 00:00:00 2001 From: Adrian Ignat Date: Fri, 6 Mar 2026 13:25:43 +0000 Subject: [PATCH] docs: add activity dev guide the documentation should be used by AI agents or humans who want to develop activities --- .../advanced/patterns.md | 358 ++++++ .../advanced/sdk-framework.md | 353 ++++++ .../core/activity-context.md | 284 +++++ .../core/architecture.md | 224 ++++ .../core/best-practices.md | 262 +++++ .../core/project-structure.md | 301 +++++ .../design/bindings.md | 155 +++ .../design/datasources.md | 212 ++++ .../design/localization.md | 68 ++ .../design/menu-actions.md | 118 ++ .../design/metadata.md | 213 ++++ .../design/project-settings.md | 73 ++ .../design/rules-and-dependencies.md | 208 ++++ .../design/solutions.md | 95 ++ .../design/validation.md | 80 ++ .../design/viewmodel.md | 425 +++++++ .../design/widgets/action-button.md | 129 ++ .../design/widgets/autocomplete.md | 139 +++ .../design/widgets/connection.md | 122 ++ .../design/widgets/dropdown.md | 120 ++ .../design/widgets/filter-builder.md | 480 ++++++++ .../design/widgets/index.md | 223 ++++ .../design/widgets/output-mapping.md | 86 ++ .../design/widgets/plain-number.md | 115 ++ .../design/widgets/solution-resources.md | 153 +++ .../design/widgets/text-block.md | 149 +++ .../design/widgets/text-input.md | 81 ++ .../design/widgets/type-picker.md | 120 ++ .../examples/complete-example.md | 558 +++++++++ .claude/activity-development-guide/index.md | 99 ++ .../reference/compilation-constants.md | 78 ++ .../reference/feature-flags.md | 115 ++ .../reference/service-access.md | 59 + .../runtime/activity-code.md | 357 ++++++ .../runtime/orchestrator.md | 243 ++++ .../runtime/platform-api.md | 299 +++++ .../testing/activity-testing.md | 395 +++++++ .../testing/viewmodel-testing.md | 298 +++++ .claude/skills/create-activity/SKILL.md | 1035 +++++++++++++++++ CLAUDE.md | 95 ++ README.md | 64 +- 41 files changed, 9018 insertions(+), 23 deletions(-) create mode 100644 .claude/activity-development-guide/advanced/patterns.md create mode 100644 .claude/activity-development-guide/advanced/sdk-framework.md create mode 100644 .claude/activity-development-guide/core/activity-context.md create mode 100644 .claude/activity-development-guide/core/architecture.md create mode 100644 .claude/activity-development-guide/core/best-practices.md create mode 100644 .claude/activity-development-guide/core/project-structure.md create mode 100644 .claude/activity-development-guide/design/bindings.md create mode 100644 .claude/activity-development-guide/design/datasources.md create mode 100644 .claude/activity-development-guide/design/localization.md create mode 100644 .claude/activity-development-guide/design/menu-actions.md create mode 100644 .claude/activity-development-guide/design/metadata.md create mode 100644 .claude/activity-development-guide/design/project-settings.md create mode 100644 .claude/activity-development-guide/design/rules-and-dependencies.md create mode 100644 .claude/activity-development-guide/design/solutions.md create mode 100644 .claude/activity-development-guide/design/validation.md create mode 100644 .claude/activity-development-guide/design/viewmodel.md create mode 100644 .claude/activity-development-guide/design/widgets/action-button.md create mode 100644 .claude/activity-development-guide/design/widgets/autocomplete.md create mode 100644 .claude/activity-development-guide/design/widgets/connection.md create mode 100644 .claude/activity-development-guide/design/widgets/dropdown.md create mode 100644 .claude/activity-development-guide/design/widgets/filter-builder.md create mode 100644 .claude/activity-development-guide/design/widgets/index.md create mode 100644 .claude/activity-development-guide/design/widgets/output-mapping.md create mode 100644 .claude/activity-development-guide/design/widgets/plain-number.md create mode 100644 .claude/activity-development-guide/design/widgets/solution-resources.md create mode 100644 .claude/activity-development-guide/design/widgets/text-block.md create mode 100644 .claude/activity-development-guide/design/widgets/text-input.md create mode 100644 .claude/activity-development-guide/design/widgets/type-picker.md create mode 100644 .claude/activity-development-guide/examples/complete-example.md create mode 100644 .claude/activity-development-guide/index.md create mode 100644 .claude/activity-development-guide/reference/compilation-constants.md create mode 100644 .claude/activity-development-guide/reference/feature-flags.md create mode 100644 .claude/activity-development-guide/reference/service-access.md create mode 100644 .claude/activity-development-guide/runtime/activity-code.md create mode 100644 .claude/activity-development-guide/runtime/orchestrator.md create mode 100644 .claude/activity-development-guide/runtime/platform-api.md create mode 100644 .claude/activity-development-guide/testing/activity-testing.md create mode 100644 .claude/activity-development-guide/testing/viewmodel-testing.md create mode 100644 .claude/skills/create-activity/SKILL.md create mode 100644 CLAUDE.md diff --git a/.claude/activity-development-guide/advanced/patterns.md b/.claude/activity-development-guide/advanced/patterns.md new file mode 100644 index 000000000..8a0fceef1 --- /dev/null +++ b/.claude/activity-development-guide/advanced/patterns.md @@ -0,0 +1,358 @@ +# Advanced Patterns + +> **When to read this:** You are building a complex activity that goes beyond simple property binding — for example, an activity with multiple input modes, dynamic display names, composite child activities, persistence/bookmarks, or inverse property mappings. These patterns are drawn from real implementations like RunJob, InvokeWorkflow, and Orchestrator activities. + +**Cross-references:** +- [ViewModel fundamentals](../design/viewmodel.md) +- [Activity code and CacheMetadata](../runtime/activity-code.md) +- [Architecture overview](../core/architecture.md) +- [FilterBuilder widget](./filter-builder.md) (another advanced topic) +- [SDK framework](./sdk-framework.md) (alternative base classes) + +--- + +## Bidirectional Property Mapping (Inverted Boolean) + +When the ViewModel needs a user-friendly property that maps inversely to the Activity property: + +```csharp +// Activity has: public bool FailWhenFaulted { get; set; } +// ViewModel exposes the opposite for better UX: + +[NotMappedProperty] +public DesignProperty ContinueWhenFaulted { get; set; } + +private ModelProperty _failWhenFaultedModelProperty; + +protected override async ValueTask InitializeModelAsync() +{ + await base.InitializeModelAsync(); + _failWhenFaultedModelProperty = ModelItem.Properties[nameof(RunJob.FailWhenFaulted)]; + ContinueWhenFaulted.Value = !(bool)_failWhenFaultedModelProperty.ComputedValue; +} + +// Sync back on change +public override async ValueTask UpdateAsync(string propertyName, object value) +{ + if (propertyName == nameof(ContinueWhenFaulted)) + _failWhenFaultedModelProperty.SetValue(!(bool)value); + return await base.UpdateAsync(propertyName, value); +} +``` + +Key points: +- Mark the ViewModel property with `[NotMappedProperty]` so the framework does not attempt automatic mapping. +- Read the activity's `ModelProperty` directly in `InitializeModelAsync`. +- Write back through `ModelProperty.SetValue()` in `UpdateAsync`. + +--- + +## Multiple Input Mode Switching (3+ Modes) + +For activities supporting fundamentally different input strategies (e.g., RunJob supports Object, Dictionary, and DataMapper modes): + +```csharp +private readonly MenuAction _objectInputSwitch = new(); +private readonly MenuAction _dictionaryInputSwitch = new(); +private readonly MenuAction _dataMapperInputSwitch = new(); + +private async Task SwitchToObjectHandler(MenuAction _) +{ + // Clear other modes + _inputModelProperty.SetValue(null); + // Build new InArgument + var argument = Argument.Create(typeof(object), ArgumentDirection.In); + _inputDesignProperty = BuildInputDesignProperty(argument); + // Optionally import output schema + await ImportArgument(processName, ArgumentDirection.Out); +} + +private Task SwitchToDictionaryHandler(MenuAction _) +{ + _inputModelProperty.SetValue(null); + _outputModelProperty.SetValue(null); + var args = new Dictionary(); + _argumentsModelProperty.SetValue(args); + return Task.CompletedTask; +} + +private async Task SwitchToDataMapperHandler(MenuAction _) +{ + // Fetch schema and generate JIT types + var metadata = await _schemaProvider.GetArguments(processName); + await ImportArgument(processName, ArgumentDirection.In, metadata?.Input); +} +``` + +Each `MenuAction` is wired to a handler that: +1. Clears properties belonging to other modes (set to `null`). +2. Creates and configures the new mode's properties. +3. Optionally fetches external metadata (schema, arguments). + +--- + +## Localized Enum Values + +Enum values can carry localization attributes for display in the designer: + +```csharp +public enum JobExecutionMode +{ + [LocalizedDisplayName(nameof(Resources.RunJob_ExecutionMode_None_DisplayName))] + [LocalizedDescription(nameof(Resources.RunJob_ExecutionMode_None_Description))] + None, + + [LocalizedDisplayName(nameof(Resources.RunJob_ExecutionMode_Busy_DisplayName))] + [LocalizedDescription(nameof(Resources.RunJob_ExecutionMode_Busy_Description))] + Busy, + + [LocalizedDisplayName(nameof(Resources.RunJob_ExecutionMode_Suspend_DisplayName))] + [LocalizedDescription(nameof(Resources.RunJob_ExecutionMode_Suspend_Description))] + Suspend +} +``` + +Both `[LocalizedDisplayName]` and `[LocalizedDescription]` reference keys in the `.resx` resource file. The Studio designer resolves these at design time for the current locale. + +--- + +## Display Name Alias Keys (Fuzzy Search) + +Activities can define alias keys so users can find them by searching different terms in the Studio activities panel: + +```json +{ + "fullName": "UiPath.Activities.System.Jobs.RunJob", + "displayNameKey": "RunJob_DisplayName", + "displayNameAliasKeys": [ + "RunJob_Synonym_Agent", + "RunJob_Synonym_API", + "RunJob_Synonym_RPA", + "RunJob_Synonym_RunAgent", + "RunJob_Synonym_RunAPI", + "RunJob_Synonym_CaseManagement" + ] +} +``` + +Each alias key is a resource key in the `.resx` file. The Studio activities panel searches these aliases when the user types in the search box. This is configured in the activity metadata JSON file. + +--- + +## Dynamic Display Name from External Data + +Activities can update their display name based on runtime data (e.g., process type fetched from Orchestrator): + +```csharp +private async Task ProcessNameChangedRule() +{ + if (!ProcessName.TryGetLiteralOfType(out var processName)) return; + + // Fetch process type from Orchestrator metadata + var suffix = await _dataSourceBuilder.GetSuffixByProcessType(processName); + // e.g., suffix = "RPA", "API", "Agent", "App" + + if (!string.IsNullOrWhiteSpace(suffix)) + { + var newDisplayName = $"Run {suffix}: {processName}"; + ModelItem.Properties[nameof(RunJob.DisplayName)]?.SetValue(newDisplayName); + Dispatcher.Invoke(() => DisplayName.Value = newDisplayName); + } +} +``` + +Important: update both the `ModelItem` property (persisted to XAML) and the `DesignProperty.Value` (displayed in the UI). Use `Dispatcher.Invoke` for UI thread safety. + +--- + +## Event-Driven DataSource + +DataSource builders can raise events to notify the ViewModel when data changes: + +```csharp +internal class ProcessesDataSourceBuilder : FolderPathDynamicDataSourceBuilder +{ + public event EventHandler> DataSourceGenerated; + + public override async ValueTask GetDynamicDataSourceAsyncInternal( + string searchText, int limit, CancellationToken ct) + { + var data = await GetProcesses(searchText, ct, limit); + DataSource.Data = data ?? Array.Empty(); + DataSourceGenerated?.Invoke(this, DataSource); + return DataSource; + } +} + +// In ViewModel: +_dataSourceBuilder.DataSourceGenerated += (sender, ds) => +{ + // React to data changes, e.g., update dependent properties +}; +``` + +This pattern decouples data fetching from the ViewModel reaction logic. The DataSource builder handles the async fetch and notifies subscribers when fresh data is available. + +--- + +## Conditional Rule Wrapping + +Wrap rules with optional services (like BusyService) without duplicating logic: + +```csharp +// Helper that conditionally wraps with busy indicator +private Func ResolveRule(Func rule) + => _busyService is not null + ? () => RunWithBusyService(() => rule()) + : rule; + +protected override void InitializeRules() +{ + base.InitializeRules(); + Rule(nameof(ProcessName), ResolveRule(ProcessNameChangedRule), false); + Rule(nameof(ExecutionMode), ResolveRule(ExecutionModeChangedRule), false); +} +``` + +This avoids duplicating busy-indicator logic in every rule handler. If `_busyService` is not available (e.g., in unit tests), the rule runs without the wrapper. + +--- + +## NativeActivity with Composite Implementation + +For activities that dynamically compose child activities at design time: + +```csharp +public class RunJob : NativeActivity +{ + private readonly Sequence _implementationSequence = new(); + private readonly WaitForJob _waitForJobActivity = new(); + private readonly WaitForJobAndResume _waitForJobAndResumeActivity = new(); + + protected override void CacheMetadata(NativeActivityMetadata metadata) + { + _implementationSequence.Activities.Clear(); + + // Dynamically add the right child activity based on configuration + Activity waitActivity = ExecutionMode switch + { + JobExecutionMode.Busy => _waitForJobActivity, + JobExecutionMode.Suspend => _waitForJobAndResumeActivity, + _ => throw new InvalidOperationException() + }; + + _implementationSequence.Activities.Add(waitActivity); + metadata.AddImplementationChild(_implementationSequence); + + // Bind dynamic arguments to metadata + foreach (var (name, argument) in Arguments) + { + var runtimeArgument = new RuntimeArgument(name, argument.ArgumentType, argument.Direction); + metadata.Bind(argument, runtimeArgument); + metadata.AddArgument(runtimeArgument); + } + } +} +``` + +Key points: +- `AddImplementationChild` registers the composed sequence as an implementation detail (not visible to the user). +- Dynamic arguments (e.g., from a dictionary) must be individually registered with `Bind` + `AddArgument`. +- The implementation sequence is rebuilt on every `CacheMetadata` call, so it always reflects the current configuration. + +--- + +## Persistent Activity with Bookmarks + +For long-running activities that survive process restarts: + +```csharp +[PersistentActivity] +[ValidatePersistenceDependsOn(nameof(ExecutionMode), nameof(JobExecutionMode.Suspend))] +public class RunJob : NativeActivity +{ + // Dynamically add/remove persistence constraints + private void UpdateNoPersistScopeConstraint(bool shouldHaveConstraint) + { + if (shouldHaveConstraint && !Constraints.Contains(_constraint)) + Constraints.Add(_constraint); + else if (!shouldHaveConstraint && Constraints.Contains(_constraint)) + Constraints.Remove(_constraint); + } +} + +// In the child activity: +protected void Persist(NativeActivityContext context, object resumeTrigger) +{ + var bookmark = context.CreateBookmark(Guid.NewGuid().ToString(), OnWaitResume); + var persistenceBookmarks = context.GetExtension(); + persistenceBookmarks.RegisterBookmark( + new PersistenceBookmark(bookmark.Name, resumeTrigger)); +} +``` + +- `[PersistentActivity]` marks the activity as persistence-aware. +- `[ValidatePersistenceDependsOn]` conditionally validates persistence requirements based on property values. +- `CreateBookmark` pauses execution; the workflow can be persisted and later resumed. +- `IPersistenceBookmarks.RegisterBookmark` associates the bookmark with a resume trigger for the orchestrator. + +--- + +## Interface Contracts for Bindings + +Activities implement interfaces that the bindings system uses for polymorphic resource resolution: + +```csharp +// Contract interfaces +internal interface IOrchestratorActivity +{ + InArgument FolderPath { get; } +} + +internal interface IProcessNameActivity : IOrchestratorActivity +{ + InArgument ProcessName { get; } +} + +// Activity implements the interface +public class RunJob : NativeActivity, IProcessNameActivity +{ + public InArgument FolderPath { get; set; } + public InArgument ProcessName { get; set; } + + // Bindings key generated from interface contract + public string BindingsKey => BindingsKeyFactory.GenerateBindingsKey(this); +} + +// At runtime, check for binding overrides before using property value +private string GetProcessNameValue(ActivityContext context) + => _bindingsService.GetBindingsOverride( + SystemBindingTypes.Process, BindingsKey, PropertyContracts.Name, context) + ?? ProcessName.Get(context); +``` + +The bindings system allows Studio to override property values at runtime based on project-level configuration (e.g., connecting a RunJob activity to a specific Orchestrator folder without hardcoding it). The interface contracts define which properties are bindable. + +--- + +## Rule-Driven Property ReadOnly State + +Change property editability dynamically based on another property's value: + +```csharp +Rule(nameof(ExecutionMode), () => +{ + var mode = ExecutionMode.Value; + TimeoutMS.IsReadOnly = mode is not JobExecutionMode.Busy; + ContinueOnError.IsReadOnly = mode is JobExecutionMode.None; + ContinueWhenFaulted.IsReadOnly = mode is JobExecutionMode.None; +}); +``` + +When `ExecutionMode` changes, the rule fires and updates `IsReadOnly` on dependent properties. This provides immediate visual feedback in the designer — readonly properties are grayed out and non-editable. + +--- + +## Troubleshooting + + diff --git a/.claude/activity-development-guide/advanced/sdk-framework.md b/.claude/activity-development-guide/advanced/sdk-framework.md new file mode 100644 index 000000000..a90905225 --- /dev/null +++ b/.claude/activity-development-guide/advanced/sdk-framework.md @@ -0,0 +1,353 @@ +# Activities SDK Framework + +> **When to read this:** You are building activities that need dependency injection, automatic telemetry, project settings integration, connection/binding support, built-in retry logic, or governance. If your activity is a simple synchronous operation (string manipulation, math), the classic `CodeActivity` pattern is sufficient and you do not need the SDK. + +**Cross-references:** +- [Activity code and CacheMetadata](../runtime/activity-code.md) — classic activity patterns (CodeActivity, AsyncCodeActivity, NativeActivity) +- [ViewModel fundamentals](../design/viewmodel.md) — classic DesignPropertiesViewModel +- [Architecture overview](../core/architecture.md) — project structure, naming conventions +- [Advanced patterns](./patterns.md) — NativeActivity composites, bookmarks, interface contracts + +--- + +## When to Use the SDK (vs Classic CodeActivity) + +Use the SDK when your activities need: +- **Dependency injection** -- register and resolve services via `Microsoft.Extensions.DependencyInjection` +- **Automatic telemetry** -- execution metrics tracked without manual instrumentation +- **Project settings integration** -- `[ArgumentSetting]` attribute auto-reads values from Studio project settings +- **Connection/binding support** -- `[ConnectionBinding]`, `[PropertyBinding]`, `[BindingContract]` attributes +- **Built-in retry logic** -- `SdkNativeActivity` provides `ShouldRetry()` and `PrepareForRetry()` +- **Governance** -- runtime governance checks applied automatically + +For simple synchronous activities (e.g., string manipulation, math), the classic `CodeActivity` pattern is sufficient -- the SDK adds unnecessary complexity. + +--- + +## SDK Setup + +The SDK is added as a git submodule and integrated via MSBuild targets: + +``` +repo/ + +-- Activities.SDK/ <-- git submodule + | +-- Sdk.imports.build.targets + | +-- Sdk.dependencies.build.targets + | +-- Sdk.constants.build.targets + +-- MyActivityPack/ + | +-- Directory.build.targets <-- imports SDK targets + | +-- Activities/ + | | +-- Activities.csproj + | | +-- MyActivity.cs + | +-- Activities.UnitTest/ + | +-- Activities.UnitTest.csproj +``` + +**Directory.build.targets** (in the activity pack directory): + +```xml + + + + +``` + +**Activity .csproj** -- enable SDK shared project imports: + +```xml + + True + True + +``` + +Package versions are centrally managed by `Sdk.dependencies.build.targets` -- do NOT specify versions for SDK-managed packages in individual `.csproj` files. + +--- + +## SDK Build Constants + +The SDK defines conditional compilation constants in `Sdk.constants.build.targets`: + +| Constant | Condition | +|----------|-----------| +| `NETCORE_UIPATH` | `net5.0+` (any .NET Core target) | +| `WINDOWS_UIPATH` | `net461`/`net462` or `*-windows` | +| `NETPORTABLE_UIPATH` | `net5.0`/`net6.0`/`net7.0`/`net8.0` (non-Windows) | +| `INTEGRATION_SERVICE` | `True` | +| `GOVERNANCE_SERVICE` | `True` | + +Use these constants for platform-specific code: + +```csharp +#if NETCORE_UIPATH + // .NET Core-specific implementation +#elif WINDOWS_UIPATH + // .NET Framework / Windows-specific implementation +#endif +``` + +--- + +## SdkActivity\ + +The primary SDK base class for async activities. Derives from `AsyncCodeActivity` and wraps the APM pattern into a clean async/await API with automatic service scope management. + +### Class Hierarchy + +```csharp +public abstract class SdkActivity : SdkActivity { } + +public abstract class SdkActivity : AsyncCodeActivity, IDisposable + where TPolicy : class, IRuntimeServicePolicy, new() +{ + protected IServiceProvider Services { get; } + protected IRuntimeServices RuntimeServices { get; } + + protected abstract Task ExecuteAsync( + AsyncCodeActivityContext context, + IServiceProvider serviceProvider, // scoped to this execution + CancellationToken cancellationToken); + + protected virtual void OnCompleted( + AsyncCodeActivityContext context, + IServiceProvider serviceProvider) { } // runs with fresh context after await +} +``` + +### Lifecycle + +1. SDK creates a DI scope tied to the execution context. +2. Applies project settings via `RuntimeServices.ProjectSettings?.Apply(context, this)`. +3. Applies bindings via `RuntimeServices.BindingService?.Apply(this)`. +4. Calls `ExecuteAsync()` -- read all inputs before the first `await` (context disposed after). +5. Stores the result in `Result`. +6. Creates a new DI scope and calls `OnCompleted()` with a fresh context. + +### Example + +```csharp +public partial class OpenAccount : SdkActivity +{ + [RequiredArgument] + public string AccountHolder { get; set; } + + public OpenAccount() : base() { } + internal OpenAccount(IServiceProvider provider) : base(provider) { } + + protected override async Task ExecuteAsync( + AsyncCodeActivityContext context, + IServiceProvider serviceProvider, + CancellationToken cancellationToken) + { + var bank = serviceProvider.GetService(); + var client = await bank.GetClientAsync(AccountHolder, cancellationToken); + + RuntimeServices.ExecutorRuntime?.LogMessage(new LogMessage + { + EventType = TraceEventType.Information, + Message = $"Account opened for {AccountHolder}" + }); + + return client.Accounts[0]; + } +} +``` + +Note the dual constructor pattern: the parameterless constructor is for the workflow runtime; the `IServiceProvider` constructor is for unit testing. + +--- + +## SdkNativeActivity\ + +For activities that need WF4 native features: child activity scheduling, bookmarks, or built-in retry. + +### Class Definition + +```csharp +public abstract class SdkNativeActivity : NativeActivity +{ + protected abstract Task ExecuteAsync( + NativeActivityContext context, + IServiceProvider serviceProvider, + CancellationToken cancellationToken); + + protected virtual bool ShouldRetry(NativeActivityContext context, object executionValue, int retryCount) => false; + protected virtual void PrepareForRetry(NativeActivityContext context, int retryCount) { } + protected int GetRetryCount(NativeActivityContext context); + protected virtual void OnCompleted(NativeActivityContext context, IServiceProvider serviceProvider) { } +} +``` + +### Retry Support + +Override `ShouldRetry` and `PrepareForRetry` for built-in retry logic: + +```csharp +protected override bool ShouldRetry(NativeActivityContext context, object executionValue, int retryCount) +{ + // Retry up to 3 times on transient failures + return retryCount < 3 && executionValue is TransientException; +} + +protected override void PrepareForRetry(NativeActivityContext context, int retryCount) +{ + // Reset state before retry + _cachedResult = null; +} +``` + +### Container Activity Example + +Schedules child activities via `OnCompleted`: + +```csharp +public partial class MyContainer : SdkNativeActivity +{ + [Browsable(false)] + public ActivityAction Body { get; set; } = new() + { + Argument = new DelegateInArgument("Service"), + Handler = new Sequence() { DisplayName = "Do" } + }; + + protected override Task ExecuteAsync( + NativeActivityContext context, IServiceProvider sp, CancellationToken ct) + => Task.FromResult(42); + + protected override void OnCompleted(NativeActivityContext context, IServiceProvider sp) + { + base.OnCompleted(context, sp); + context.ScheduleAction(Body, new MyService()); + } +} +``` + +--- + +## Custom Service Policies + +Override `DefaultRuntimeServicePolicy` to register custom services in the DI container: + +```csharp +public class CustomServicePolicy : DefaultRuntimeServicePolicy +{ + public override IServicePolicy Register(Action collection = null) + { + _services.TryAddSingleton(); + return base.Register(collection); + } +} + +// Use as second type parameter +public partial class MyActivity : SdkActivity { ... } +``` + +`TryAddSingleton` ensures the service is only registered once, even if multiple activities use the same policy. + +--- + +## SDK ViewModel Pattern + +SDK ViewModels inherit from `BaseViewModel` (or `BaseViewModel`) and are linked to activities via the `[ViewModelClass]` attribute on a partial class. + +### Activity Partial for ViewModel Registration + +```csharp +// ViewModels/MyActivity.Design.cs +[ViewModelClass(typeof(MyActivityViewModel))] +public partial class MyActivity { } +``` + +### ViewModel Class + +```csharp +internal class MyActivityViewModel : BaseViewModel +{ + public DesignInArgument Input { get; set; } = new(); + public DesignOutArgument Result { get; set; } = new(); + + public MyActivityViewModel(IDesignServices services) : base(services) { } + internal MyActivityViewModel(IDesignServices services, IServiceProvider sp) : base(services, sp) { } + + protected override void InitializeModel() + { + base.InitializeModel(); + Input.IsPrincipal = true; + Input.OrderIndex = PropertyOrderIndex++; // built-in counter + Result.OrderIndex = PropertyOrderIndex++; + } +} +``` + +--- + +## Differences Table: Classic vs SDK + +| Aspect | Classic (`DesignPropertiesViewModel`) | SDK (`BaseViewModel`) | +|--------|--------------------------------------|----------------------| +| Order counter | Manual `var order = 0` | Built-in `PropertyOrderIndex` | +| Class visibility | `public` | `internal` | +| Activity linking | `viewModelType` in metadata JSON | `[ViewModelClass]` attribute | +| DI in ViewModel | Not available | `ActivityServices.GetService()` | +| Design-time services | Via `IDesignServices` | Via `DesignServices` property | +| Init call | `base.InitializeModel()` + `PersistValuesChangedDuringInit()` | `base.InitializeModel()` only | +| Testability | Mock `IDesignServices` | Mock `IDesignServices` + inject `IServiceProvider` | + +--- + +## SDK-Specific Attributes + +| Attribute | Purpose | +|-----------|---------| +| `[ConnectionBinding]` | Marks a property as an Integration Service connection | +| `[PropertyBinding(nameof(X))]` | Declares a dependency on another property for binding | +| `[BindingContract("Name")]` | Defines a contract name for the binding system | +| `[ArgumentSetting(section, property)]` | Auto-reads value from Studio project settings | +| `[Sensitive]` | Excludes property data from telemetry | +| `[ViewModelClass(typeof(VM))]` | Links activity to its ViewModel (on partial class) | + +### Example: ArgumentSetting + +```csharp +// Automatically reads from project settings at runtime +[ArgumentSetting("OrchestratorSettings", "DefaultFolderPath")] +public InArgument FolderPath { get; set; } +``` + +### Example: ConnectionBinding + +```csharp +[ConnectionBinding] +[BindingContract("SalesforceConnection")] +public InArgument ConnectionId { get; set; } +``` + +--- + +## IRuntimeServices + +Available via the `RuntimeServices` property on any SDK activity: + +```csharp +RuntimeServices.ExecutorRuntime // IExecutorRuntime -- logging, settings, OAuth +RuntimeServices.WorkflowRuntime // IWorkflowRuntime -- workflow services +RuntimeServices.ProjectSettings // Read/apply project settings +RuntimeServices.Telemetry // Automatic telemetry tracking +RuntimeServices.BindingService // Apply connection/property bindings +RuntimeServices.Governance // Runtime governance (if GOVERNANCE_SERVICE defined) +RuntimeServices.ConnectionClient // Integration Service client (if INTEGRATION_SERVICE defined) +``` + +All properties are nullable -- the SDK gracefully handles missing services (e.g., when running outside Studio or in unit tests). Always use null-conditional access: + +```csharp +RuntimeServices.ExecutorRuntime?.LogMessage(new LogMessage { ... }); +RuntimeServices.Governance?.ValidateExecution(this); +``` + +--- + +## Troubleshooting + + diff --git a/.claude/activity-development-guide/core/activity-context.md b/.claude/activity-development-guide/core/activity-context.md new file mode 100644 index 000000000..d18865c52 --- /dev/null +++ b/.claude/activity-development-guide/core/activity-context.md @@ -0,0 +1,284 @@ +# ActivityContext and Workflow Foundation + +> **When to read this**: Before writing ANY async activity. This is the single most critical file in this guide. The "Read Before Await" pattern described here prevents the #1 cause of runtime crashes in UiPath activities. If you read nothing else, read this file. + +--- + +## What is ActivityContext? + +UiPath activities are built on .NET's Windows Workflow Foundation (WF). `ActivityContext` is a short-lived handle to the workflow execution environment. It provides access to: + +- **Arguments**: Read `InArgument` values, write `OutArgument` values +- **Variables**: Read/write workflow variables in scope +- **Extensions**: Access runtime services (`IExecutorRuntime`, `IWorkflowRuntime`) + +--- + +## Context Lifetime Rule (CRITICAL) + +> **The context is disposed after the first `await` in async methods.** All argument reads and extension lookups MUST happen before any async operation. + +This is explicitly documented in the SDK source: + +```csharp +// From SdkActivity.cs: +/// will be disposed after the first await +protected abstract Task ExecuteAsync( + AsyncCodeActivityContext context, + IServiceProvider serviceProvider, + CancellationToken cancellationToken); +``` + +**Why**: The workflow runtime reuses context objects. After you yield control (via `await`), the runtime may recycle the context for another activity. Any read/write after that point touches recycled or disposed state. + +--- + +## The "Read Before Await" Pattern + +Every async activity MUST follow this 3-step pattern: + +```csharp +protected override async Task> ExecuteAsync( + AsyncCodeActivityContext context, CancellationToken cancellationToken) +{ + // STEP 1: Read ALL inputs from context BEFORE any await + var name = Name.Get(context); + var timeout = Timeout.Get(context); + var folderPath = FolderPath.Get(context); + var labels = Labels.Select(l => l.Get(context)).ToList(); // materialize collections! + var runtime = context.GetExtension(); + var robotId = runtime.RobotSettings.RobotId; + + // STEP 2: Perform async work (context is now invalid after first await) + var result = await _service.ProcessAsync(name, timeout, cancellationToken); + + // STEP 3: Return a callback that writes outputs with a NEW context + return ctx => + { + Result.Set(ctx, result); + MessageId.Set(ctx, result.Id); + }; +} +``` + +### What Goes Wrong + +```csharp +// WRONG: Reading context after await +protected override async Task> ExecuteAsync( + AsyncCodeActivityContext context, CancellationToken cancellationToken) +{ + var name = Name.Get(context); + var data = await _service.FetchAsync(name, cancellationToken); + + // Context is disposed here -- this will throw or return stale data + var format = OutputFormat.Get(context); // CRASH or WRONG VALUE + + return ctx => { Result.Set(ctx, data); }; +} +``` + +### Checklist + +Before submitting any async activity code, verify: + +1. All `InArgument.Get(context)` calls are BEFORE the first `await` +2. All `context.GetExtension()` calls are BEFORE the first `await` +3. Collections from context are materialized with `.ToList()` or `.ToArray()` BEFORE the first `await` +4. Output writes (`OutArgument.Set()`) use the callback's `ctx` parameter, NOT the original `context` + +--- + +## Pattern: AsyncCodeActivity (APM Style) + +The older `AsyncCodeActivity` uses the Asynchronous Programming Model (APM) with `BeginExecute`/`EndExecute`: + +```csharp +public class MyOrchestratorActivity : AsyncCodeActivity +{ + public InArgument AssetName { get; set; } + public InArgument TimeoutMS { get; set; } + + protected override IAsyncResult BeginExecute( + AsyncCodeActivityContext context, AsyncCallback callback, object state) + { + // Read ALL context values here -- context is invalid after this returns + var assetName = AssetName.Get(context); + var timeout = TimeoutMS.Get(context); + var folderPath = GetFolderPath(context); + var client = CreateClient(context, timeout, folderPath); + + // Start async work + return FetchAssetAsync(client, assetName) + .ToApm(callback, state); + } + + protected override string EndExecute( + AsyncCodeActivityContext context, IAsyncResult result) + { + // Write outputs here -- context is valid again in EndExecute + var task = (Task)result; + return task.Result; + } +} +``` + +**Key points**: +- `BeginExecute`: Context is valid. Read all inputs here. +- Between `BeginExecute` and `EndExecute`: Context is invalid. Async work happens here. +- `EndExecute`: Context is valid again. Write outputs here. + +--- + +## Pattern: SdkActivity (Modern SDK) + +The SDK wraps the APM pattern into a cleaner async/await model. The SDK creates a **service scope** before the first await, and a **new scope** in the completion callback: + +```csharp +public class MyActivity : SdkActivity +{ + public InArgument Input { get; set; } + public OutArgument Extra { get; set; } + + protected override async Task ExecuteAsync( + AsyncCodeActivityContext context, + IServiceProvider serviceProvider, + CancellationToken cancellationToken) + { + // Read from context (still valid -- first statement, no await yet) + var input = Input.Get(context); + + // Services from DI are safe to use across awaits + var myService = serviceProvider.GetRequiredService(); + + // Async work + return await myService.ProcessAsync(input, cancellationToken); + // Result is automatically set via the SDK framework + } +} +``` + +**Key points**: +- Context reads must still be before the first `await` +- `IServiceProvider` services are safe to use across `await` boundaries (they are captured, not context-bound) +- The return value is automatically assigned to `Result` + +See `../advanced/sdk-activity.md` for the full Activities SDK reference. + +--- + +## Pattern: NativeActivity (Scheduling and Bookmarks) + +`NativeActivity` uses `NativeActivityContext` which supports scheduling child activities and creating bookmarks. Context values must be captured before scheduling: + +```csharp +protected override void Execute(NativeActivityContext context) +{ + // Read ALL values before scheduling child activities + var timeout = TimeoutMS.Get(context); + var processName = GetProcessNameValue(context); + + // Store values in implementation variables for access in callbacks + _timeoutVariable.Set(context, timeout); + _processNameVariable.Set(context, processName); + + // Schedule child activity -- execution continues in callbacks + context.ScheduleActivity(_childActivity, OnChildCompleted, OnChildFaulted); +} + +// Callback receives a NEW context +private void OnChildCompleted(NativeActivityContext context, + ActivityInstance completedInstance) +{ + // Read from implementation variables (persisted across async boundaries) + var processName = _processNameVariable.Get(context); + + // Write outputs + Result.Set(context, processName); +} +``` + +**Key points**: +- `Execute`: Read all inputs, store in implementation variables +- Callbacks (`OnChildCompleted`, `OnChildFaulted`): Receive a NEW context. Read state from implementation variables, not from the original context captures. + +--- + +## Parallel Execution and Activity Instances + +In parallel execution (`Parallel`, `ParallelForEach`), the **activity instance is shared** but each execution gets its own **context**. This means: + +- **Activity properties** (like `InArgument`) are shared across parallel branches +- **Context-bound state** (variables, bookmarks) is per-execution +- **Mutable instance fields** on the activity class are NOT thread-safe + +```csharp +// WARNING: From RunJob.cs -- awareness of parallel execution concerns: +//TODO: can these values change in a parallel foreach loop? +// if yes, they should be passed via context +_waitForJobActivity.Timeout = TimeoutMS.Get(context); +``` + +**Rule**: Never store per-execution state in activity instance fields. Use workflow variables or context-bound implementation variables instead. + +--- + +## Implementation Variables (Cross-Async State) + +For `NativeActivity`, use `Variable` to store state across async boundaries. These are per-execution and persisted by the workflow runtime: + +```csharp +public class MyNativeActivity : NativeActivity +{ + // Implementation variables -- per-execution, persisted across async + private readonly Variable _startTime = new(nameof(_startTime)); + private readonly Variable _intermediateResult = new(nameof(_intermediateResult)); + + protected override void CacheMetadata(NativeActivityMetadata metadata) + { + base.CacheMetadata(metadata); + // Register implementation variables so the runtime knows about them + metadata.AddImplementationVariable(_startTime); + metadata.AddImplementationVariable(_intermediateResult); + } + + protected override void Execute(NativeActivityContext context) + { + _startTime.Set(context, DateTime.UtcNow); + context.ScheduleActivity(_childActivity, OnCompleted); + } + + private void OnCompleted(NativeActivityContext context, ActivityInstance instance) + { + var elapsed = DateTime.UtcNow - _startTime.Get(context); + // Safe: each parallel execution has its own variable value + } +} +``` + +**Key points**: +- Declare as `private readonly Variable` fields on the activity class +- Register in `CacheMetadata` via `metadata.AddImplementationVariable()` +- Read/write through context (`_var.Get(context)`, `_var.Set(context, value)`) +- Each parallel execution instance gets its own copy of the variable value + +--- + +## Quick Reference: Context Validity by Pattern + +| Pattern | Context valid for reads | Context valid for writes | Cross-await state | +|---------|------------------------|--------------------------|-------------------| +| `CodeActivity` | In `Execute()` | In `Execute()` | N/A (synchronous) | +| `AsyncCodeActivity` | In `BeginExecute()` | In `EndExecute()` | Local variables captured in closure | +| `SdkActivity` | Before first `await` | Return value auto-set | Local variables + `IServiceProvider` | +| `NativeActivity` | In `Execute()` | In callbacks | Implementation variables (`Variable`) | + +--- + +## Troubleshooting + + diff --git a/.claude/activity-development-guide/core/architecture.md b/.claude/activity-development-guide/core/architecture.md new file mode 100644 index 000000000..cc78219db --- /dev/null +++ b/.claude/activity-development-guide/core/architecture.md @@ -0,0 +1,224 @@ +# Architecture Overview + +> **When to read this**: Before writing your first UiPath activity. This file explains the fundamental 3-part model (Activity, ViewModel, Metadata), the platform components your code interacts with, and the runtime execution contexts. Every other file in this guide assumes you understand these concepts. + +--- + +## The 3-Part Model + +A UiPath activity consists of three parts that are linked by naming conventions: + +``` ++---------------------------------------------------------+ +| Activity (Runtime) | +| - Inherits CodeActivity or SdkActivity | +| - Contains execution logic | +| - Defines InArgument/OutArgument properties | ++----------------------------+----------------------------+ + | property names must match ++----------------------------v----------------------------+ +| ViewModel (Design-Time) | +| - Inherits DesignPropertiesViewModel or BaseViewModel | +| - Configures designer UI (widgets, layout, rules) | +| - Maps to activity properties by name | ++----------------------------+----------------------------+ + | referenced by ++----------------------------v----------------------------+ +| Metadata (ActivitiesMetadata.json) | +| - Links activity class -> ViewModel class | +| - Defines display name, description, category, icon | +| - Uses resource keys for localization | ++---------------------------------------------------------+ +``` + +**Key principle**: The Activity handles *what happens at runtime*. The ViewModel handles *how the activity looks in the designer*. Property names on the ViewModel MUST exactly match the Activity's `InArgument`/`OutArgument`/property names. + +### Activity (Runtime) + +The activity class contains the execution logic. It declares typed input/output arguments and overrides an `Execute` method. + +```csharp +public class MyActivity : CodeActivity +{ + [RequiredArgument] + public InArgument Name { get; set; } + + public OutArgument Result { get; set; } + + protected override void Execute(CodeActivityContext context) + { + var name = Name.Get(context); + Result.Set(context, $"Hello, {name}"); + } +} +``` + +See `../design/viewmodel.md` for the ViewModel side. + +### ViewModel (Design-Time) + +The ViewModel configures how the activity appears in UiPath Studio. Each property on the ViewModel corresponds 1:1 with an Activity property by name. + +```csharp +public class MyActivityViewModel : DesignPropertiesViewModel +{ + public DesignInArgument Name { get; set; } // matches Activity.Name + public DesignOutArgument Result { get; set; } // matches Activity.Result + + public MyActivityViewModel(IDesignServices services) : base(services) { } + + protected override void InitializeModel() + { + base.InitializeModel(); + PersistValuesChangedDuringInit(); + + Name.IsPrincipal = true; + Name.IsRequired = true; + } +} +``` + +### Metadata (ActivitiesMetadata.json) + +The metadata JSON links the Activity class to its ViewModel and defines display attributes: + +```json +{ + "activities": [ + { + "fullName": "MyCompany.MyActivities.MyActivity", + "viewModelFullName": "MyCompany.MyActivities.ViewModels.MyActivityViewModel", + "displayNameResource": "MyActivity_DisplayName", + "descriptionResource": "MyActivity_Description", + "categoryResource": "Category_General", + "iconName": "my-activity", + "codedWorkflowSupport": true + } + ] +} +``` + +--- + +## Platform Components + +``` ++-----------------------------------------------------------------+ +| UiPath Studio / Studio Web | +| - Design-time environment where workflows are built | +| - Hosts activity designers (ViewModels) | +| - Provides IWorkflowDesignApi for design-time services | +| - Studio Desktop: Windows, full feature set | +| - Studio Web: browser-based, cross-platform subset | ++-----------------------------+-----------------------------------+ + | deploys workflows to ++-----------------------------v-----------------------------------+ +| UiPath Robot / Assistant | +| - Runtime engine that executes workflows | +| - Provides IExecutorRuntime (logging, settings, auth tokens) | +| - Provides IWorkflowRuntime (execution, feature detection) | +| - Attended (user-triggered) or Unattended (Orchestrator-triggered)| ++-----------------------------+-----------------------------------+ + | communicates with ++-----------------------------v-----------------------------------+ +| UiPath Orchestrator | +| - Server-side management and execution platform | +| - Manages queues, assets, processes, jobs, storage buckets | +| - Provides REST APIs consumed by activities | +| - Version-aware: activities adapt behavior to Orchestrator ver. | ++-----------------------------------------------------------------+ +``` + +--- + +## Studio Desktop vs Studio Web + +Activities run in two designer environments with different capabilities: + +| Aspect | Studio Desktop | Studio Web | +|--------|---------------|------------| +| Platform | Windows (`net6.0-windows`) | Browser (`net6.0`, cross-platform) | +| Metadata file | `ActivitiesMetadataWindows.json` + `ActivitiesMetadataPortable.json` | `ActivitiesMetadataPortable.json` only | +| Feature detection | `workflowDesignApi.HasFeature(DesignFeatureKeys.StudioDesignSettingsV3)` | Returns `false` for Desktop-only features | +| Helper | `workflowDesignApi.RunningInStudioDesktop()` | Returns `false` | + +```csharp +// Detecting Studio Desktop at design time +var workflowDesignApi = Services.GetService(); +bool isStudioDesktop = workflowDesignApi?.RunningInStudioDesktop() == true; +bool isStudioWeb = !isStudioDesktop; +``` + +For packages with platform-split activities, replace `ActivitiesMetadata.json` with both `ActivitiesMetadataPortable.json` and `ActivitiesMetadataWindows.json`. See `project-structure.md` for the .csproj configuration. + +--- + +## Execution Contexts + +Activities can be triggered from different sources. The `IRunningJobInformation.InitiatedBy` property indicates the source: + +| Value | Context | +|-------|---------| +| `"Orchestrator"` | Unattended job triggered by Orchestrator | +| `"Studio"` | Debug run from Studio | +| `"StudioX"` | Debug run from StudioX | +| `"StudioPro"` | Debug run from Studio Pro | +| `"Assistant"` | Attended run via UiPath Assistant | +| `"CommandLine"` | Command-line execution | +| `"RobotAPI"` | Triggered via Robot API | + +```csharp +// Access execution context at runtime +var executorRuntime = context.GetExtension(); +var jobInfo = executorRuntime?.RunningJobInformation; +var initiatedBy = jobInfo?.InitiatedBy; // "Orchestrator", "Studio", etc. +var projectFramework = jobInfo?.TargetFramework; // Legacy, Windows, Portable +``` + +--- + +## Integration Service + +Activities that connect to external services (email, CRM, cloud storage) use UiPath Integration Service via connection bindings: + +```csharp +// Activity declares a connection binding +[ConnectionBinding] +public InArgument ConnectionId { get; set; } +``` + +```csharp +// ViewModel shows the connection picker widget +ConnectionId.Widget = new DefaultWidget +{ + Type = ViewModelWidgetType.Connection, + Metadata = new() { { nameof(Connector), GetConnector() } } +}; +``` + +See `../design/widgets.md` for the full widget reference including the Connection widget. + +--- + +## Coded Workflows + +Activities can declare whether they support coded workflows (C# code files) vs classic XAML workflows: + +```json +{ + "fullName": "MyCompany.MyActivity", + "codedWorkflowSupport": true +} +``` + +Set `codedWorkflowSupport: false` in metadata for activities that only work in the XAML designer. + +--- + +## Troubleshooting + + diff --git a/.claude/activity-development-guide/core/best-practices.md b/.claude/activity-development-guide/core/best-practices.md new file mode 100644 index 000000000..757e20844 --- /dev/null +++ b/.claude/activity-development-guide/core/best-practices.md @@ -0,0 +1,262 @@ +# Best Practices + +> **When to read this**: Before submitting any activity code for review. This is a checklist of rules distilled from real production issues. Violating the versioning rules in particular can break deployed workflows for all users. + +--- + +## Activity Design + +1. **Separate business logic from workflow context.** Put core logic in a public `ExecuteInternal()` method (or similar) that takes plain parameters. This makes unit testing possible without mocking the workflow runtime. + + ```csharp + // Good: testable business logic separated from context + public class EmailSender : CodeActivity + { + protected override void Execute(CodeActivityContext context) + { + var to = To.Get(context); + var subject = Subject.Get(context); + var body = Body.Get(context); + var messageId = SendEmail(to, subject, body); + MessageId.Set(context, messageId); + } + + // Public, testable, no workflow dependency + public string SendEmail(string to, string subject, string body) + { + // Business logic here + return Guid.NewGuid().ToString(); + } + } + ``` + +2. **Use `[RequiredArgument]`** for mandatory properties. The workflow engine validates these before execution. + +3. **Use `InArgument`** for inputs that should accept expressions/variables. Use direct types (plain properties) for constants/enums that are always set at design time. + +4. **Use `[DefaultValue]`** on all properties for backward compatibility when adding new properties to an existing activity. + +5. **Mark sensitive properties** with `[Sensitive]` to exclude them from telemetry and logging. + +--- + +## ViewModel Design + +1. **Property names must exactly match** the Activity class property names. A mismatch silently fails -- the property will not be serialized or deserialized. + +2. **Always call `base.InitializeModel()`** and `PersistValuesChangedDuringInit()` at the start of `InitializeModel()`: + + ```csharp + protected override void InitializeModel() + { + base.InitializeModel(); // must be first + PersistValuesChangedDuringInit(); // must be second + + // ... configure properties + } + ``` + +3. **Use `IsPrincipal = true`** for the most important 2-4 properties. These appear in the main panel and cannot be collapsed. + +4. **Use `OrderIndex`** to control property display order. Use an incrementing counter: + + ```csharp + var order = 0; + To.OrderIndex = order++; + Subject.OrderIndex = order++; + Body.OrderIndex = order++; + ``` + +5. **Localize all user-visible strings** through resource files. Never hardcode display names: + + ```csharp + // Good + To.DisplayName = Resources.EmailSender_To_DisplayName; + + // Bad -- not localizable + To.DisplayName = "Recipient"; + ``` + +6. **Use partial classes** for large ViewModels to separate concerns (Configuration, Rules, Actions). + +7. **Keep rules focused.** Each rule should handle one concern. Name rules after the property they react to. + +See `../design/viewmodel.md` for the full ViewModel reference. + +--- + +## Widget Selection + +1. **Default behavior is usually correct.** Only set a widget explicitly when you need something specific. + +2. **Use `AutoCompleteForExpression`** when the user should pick from a list but also be able to type an expression. + +3. **Use `Dropdown`** when the user must pick from a fixed set of values. + +4. **Use `Toggle`** for boolean properties. + +5. **Use `PlainNumber`** with Min/Max/Step when you need constrained numeric input without expressions. + +6. **Use `TextBlockWidget`** for read-only information display. + +7. **Check widget availability** with `HasFeature()` and `IsWidgetSupported()` before using platform-specific widgets: + + ```csharp + if (workflowDesignApi.HasFeature(DesignFeatureKeys.SomeFeature)) + { + MyProperty.Widget = new DefaultWidget { Type = ViewModelWidgetType.SpecificWidget }; + } + ``` + +See `../design/widgets.md` for the complete widget reference. + +--- + +## Dynamic Data Sources + +1. **Use static DataSource** when the list is small and known at design time. + +2. **Use `IDynamicDataSourceBuilder`** when data comes from an API or depends on other properties. + +3. **Register dependencies** when a data source depends on another property's value. + +4. **Add async validators** for data sources that need server-side validation. + +See `../design/datasources.md` for DataSource patterns. + +--- + +## Solutions Support + +1. **Always check** `IUserDesignContext.SolutionId` to determine if running in a Solution context. + +2. **Use `SolutionResourcesWidget`** for Solution-managed resources. + +3. **Hide `FolderPath`** when in Solutions context (managed by the Solution): + + ```csharp + var solutionId = userDesignContext?.SolutionId; + if (!string.IsNullOrEmpty(solutionId)) + { + FolderPath.IsVisible = false; + } + ``` + +4. **Provide both paths**: Implement `InitializeProjectScopePropertiesAsync()` and `InitializeSolutionsScopePropertiesAsync()`. + +--- + +## Feature Detection + +1. **Always use `HasFeature()`** before using platform-specific APIs: + + ```csharp + var designApi = Services.GetService(); + if (designApi?.HasFeature(DesignFeatureKeys.StudioDesignSettingsV3) == true) + { + // Safe to use V3 API + } + ``` + +2. **Use `[MethodImpl(MethodImplOptions.NoInlining)]`** when calling methods that reference types from optional assemblies. This prevents JIT from trying to load the assembly when the calling method is compiled: + + ```csharp + [MethodImpl(MethodImplOptions.NoInlining)] + private void ConfigureDesktopOnlyFeature() + { + // References types from an assembly only available on Desktop + } + ``` + +3. **Gracefully degrade** when features are unavailable rather than throwing exceptions. + +--- + +## Performance + +1. **Use `ValueTask`** instead of `Task` for async ViewModel methods that often complete synchronously. + +2. **Use `Dispatcher.Invoke()`** when updating properties from background threads. + +3. **Use `IBusyService`** to show loading indicators during long async operations. + +4. **Minimize rule execution.** Set `runOnInit: false` when the rule does not need to run during initialization: + + ```csharp + Rules.Add(new Rule( + nameof(SomeProperty), + (ctx) => { /* rule logic */ }, + runOnInit: false // only runs when SomeProperty changes + )); + ``` + +--- + +## Testing + +1. **Unit test business logic** separately from the workflow context (via the `ExecuteInternal()` pattern). + +2. **Workflow test the full activity** using `WorkflowInvoker` with mocked extensions: + + ```csharp + var invoker = new WorkflowInvoker(new MyActivity()); + invoker.Extensions.Add(mockExecutorRuntime); + var result = invoker.Invoke( + new Dictionary { ["Input"] = "test" }, + TimeSpan.FromSeconds(10) // always set a timeout + ); + ``` + +3. **ViewModel test** initialization, rules, and property visibility. + +4. **Mock `IExecutorRuntime`** to verify logging behavior. + +5. **Use `TimeSpan` timeouts** in `WorkflowInvoker.Invoke()` to prevent hanging tests. + +--- + +## Versioning and Compatibility (CRITICAL) + +These rules protect deployed workflows from breaking. Violations affect all users who have installed your activity package. + +1. **Never rename or delete activity arguments/properties.** Existing `.xaml` workflows serialize activity properties by name. Renaming or removing a property causes a deserialization error when the workflow loads, breaking all projects that use the activity. + +2. **New properties require `[DefaultValue]`** so that existing workflows load without errors (the missing property gets the default): + + ```csharp + // Adding a new property to an existing activity + [DefaultValue(30000)] + public InArgument TimeoutMS { get; set; } + ``` + +3. **To deprecate a property**: Add `[Obsolete]` and `[Browsable(false)]`. The property must remain on the class -- hide it from the designer, but keep it loadable: + + ```csharp + [Obsolete("Use TimeoutMS instead")] + [Browsable(false)] + public InArgument Timeout { get; set; } + ``` + +4. **Minor version bump** for any public property or activity additions. + +5. **Major version bump** for breaking changes (e.g., changing a property's type). + +--- + +## Cross-References + +- Activity context rules: `activity-context.md` (the "Read Before Await" pattern) +- Project structure and packaging: `project-structure.md` +- Architecture overview: `architecture.md` +- ViewModel patterns: `../design/viewmodel.md` +- Widget reference: `../design/widgets.md` + +--- + +## Troubleshooting + + diff --git a/.claude/activity-development-guide/core/project-structure.md b/.claude/activity-development-guide/core/project-structure.md new file mode 100644 index 000000000..64c735796 --- /dev/null +++ b/.claude/activity-development-guide/core/project-structure.md @@ -0,0 +1,301 @@ +# Project Structure + +> **When to read this**: When setting up a new activity package from scratch, or when troubleshooting build/packaging issues. This file covers the solution layout, NuGet configuration, .csproj templates, and the packaging pipeline. + +--- + +## Recommended Solution Layout + +``` +MyActivities/ ++-- MyActivities.sln ++-- nuget.config # NuGet feed configuration (UiPath Official feed) ++-- MyActivities/ # Main activity + ViewModel project +| +-- MyActivities.csproj +| +-- Activities/ +| | +-- MyActivity.cs # Runtime activity class +| +-- ViewModels/ +| | +-- MyActivityViewModel.cs # Design-time ViewModel +| +-- Resources/ +| | +-- ActivitiesMetadata.json # Activity metadata (single file for cross-platform) +| | +-- ActivitiesBindings.json # Orchestrator bindings (if needed) +| | +-- Resources.resx # Localized strings +| | +-- Resources.Designer.cs # Generated — commit for CLI builds (see note below) +| | +-- Icons/ +| | +-- my-activity.svg # Activity icon (SVG) +| +-- Helpers/ +| +-- ActivityContextExtensions.cs # Runtime logging helper ++-- MyActivities.Packaging/ # NuGet packaging project +| +-- MyActivities.Packaging.csproj ++-- MyActivities.Tests/ # Test project +| +-- MyActivities.Tests.csproj +| +-- Unit/ +| | +-- MyActivityUnitTests.cs +| +-- Workflow/ +| +-- MyActivityWorkflowTests.cs ++-- Output/ + +-- Packages/ # Generated .nupkg files (git-ignored) +``` + +For packages with platform-split activities, replace `ActivitiesMetadata.json` with `ActivitiesMetadataPortable.json` and `ActivitiesMetadataWindows.json`. + +--- + +## NuGet Feed Configuration + +UiPath SDK packages (`System.Activities.ViewModels`, `UiPath.Workflow`) are published on the UiPath official NuGet feed. Create a `nuget.config` at the solution root: + +```xml + + + + + + + +``` + +Without this file, `dotnet restore` will not find the UiPath SDK packages. + +--- + +## Main Project .csproj + +This is the project that contains your activity classes, ViewModels, resources, and metadata. + +```xml + + + Library + net8.0 + enable + enable + + MyCompany.MyActivities.Library + false + MyCompany.MyActivities + + + + + + + + + + + + + + + + + + + + Resources.resx + True + True + MyCompany.MyActivities + + + + + Resources.Designer.cs + PublicResXFileCodeGenerator + MyCompany.MyActivities + + + +``` + +**`System.Activities.ViewModels` version**: Every version matching `1.0.0-*` is a prerelease build. The stable release line starts at `1.20251103.2` (scheme: `1.YYYYMMDD.patch`). The `1.0.0-alpha` builds are missing `PersistValuesChangedDuringInit`, `DefaultWidget`, and `ViewModelWidgetType` — activities using these APIs won't compile. + +**Important**: The `CustomToolNamespace` must match the project's root namespace so the generated `Resources` class is accessible from your code. + +### Platform-Split Metadata + +For packages that need separate Windows and Portable metadata, embed both files instead: + +```xml + + + + +``` + +--- + +## Packaging Project .csproj + +The packaging project is a thin project that references the main library and produces the `.nupkg`. It does not contain any code. The `PackageTags` value `UiPathActivities` is **required** for UiPath Studio to recognize the package as an activity package. + +```xml + + + net8.0 + enable + enable + + + + True + + $([System.DateTime]::UtcNow.DayOfYear.ToString("F0")) + $([System.DateTime]::UtcNow.TimeOfDay.TotalMinutes.ToString("F0")) + 1.0.0 + 1.0.$(VersionBuild)-dev.$(VersionRevision) + MyCompany.MyActivities + MyCompany + My custom UiPath activities package + UiPathActivities + ..\Output\Packages\ + AddDlls + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + All + + + + + + + + + +``` + +--- + +## Understanding PrivateAssets="All" + +`PrivateAssets="All"` on a `PackageReference` means: "I need this package at compile time, but do **not** include it as a dependency in the nupkg -- I expect the host process to already have it loaded." + +### When to USE PrivateAssets="All" + +UiPath platform packages -- always. These are provided by the Robot/Studio host at runtime: + +- `UiPath.Activities.Api` +- `UiPath.Workflow` +- `UiPath.Activities.Contracts` +- `System.Activities.*` + +```xml + + + +``` + +### When to OMIT PrivateAssets (let dependency flow into nupkg) + +Third-party packages your activity needs at runtime that are **not** provided by the executor/Studio: + +```xml + + + + +``` + +If a package is not provided by the host and you mark it `PrivateAssets="All"`, the activity will fail at runtime with `FileNotFoundException`. + +### How it flows through the packaging project + +The packaging project's `ProjectReference` to the main library uses `PrivateAssets="All"` to prevent the packaging project's own (empty) assembly from becoming a dependency. The `AddDlls` target includes the real library DLL instead. Third-party runtime dependencies from the main library flow through to the nupkg automatically as long as they are **not** marked `PrivateAssets="All"` in the main library. + +--- + +## Building the Package + +```bash +# Build and produce .nupkg (output in Output/Packages/) +dotnet build MyActivities.Packaging/MyActivities.Packaging.csproj -c Release + +# Or build the entire solution +dotnet build MyActivities.sln -c Release +``` + +The `.nupkg` file will be in `Output/Packages/`. Install it in UiPath Studio via the Package Manager by adding a local feed pointing to that directory. + +--- + +## Creating the Solution File + +```bash +dotnet new sln -n MyActivities +dotnet sln add MyActivities/MyActivities.csproj +dotnet sln add MyActivities.Packaging/MyActivities.Packaging.csproj +dotnet sln add MyActivities.Tests/MyActivities.Tests.csproj +``` + +--- + +## Cross-References + +- For metadata file format, see `../design/metadata.md` +- For localization setup with Resources.resx, see `../design/localization.md` +- For test project setup, see `../testing/testing.md` + +--- + +## Troubleshooting + + + +### "Ambiguous project name" NuGet restore error + +**Symptom**: `dotnet restore` fails with "Ambiguous project name 'MyCompany.MyActivities'" when restoring the packaging project. +**Cause**: Both the library project and the packaging project have the same ``. NuGet can't distinguish them. +**Fix**: Set `MyCompany.MyActivities.Library` and `false` in the library `.csproj`. Only the packaging project should own the publishable PackageId. + +### Resources class not found / wrong namespace + +**Symptom**: `Resources.MyActivity_DisplayName` doesn't compile, or the `Resources` class is in a different namespace than expected. +**Cause**: MSBuild derives the embedded resource manifest name from the project filename, which may differ from the desired namespace. Also, `PublicResXFileCodeGenerator` only runs in Visual Studio — CLI builds need a pre-committed `Resources.Designer.cs`. +**Fix**: Add `MyCompany.MyActivities` to the library `.csproj`. Ensure `CustomToolNamespace` in the `.csproj` matches this namespace. Generate and commit `Resources.Designer.cs` for CLI builds. + +### System.Activities.ViewModels missing types (PersistValuesChangedDuringInit, DefaultWidget) + +**Symptom**: Compile errors for `PersistValuesChangedDuringInit`, `DefaultWidget`, or `ViewModelWidgetType` — these types don't exist. +**Cause**: The project references `System.Activities.ViewModels` version `1.0.0-*`, which resolves to a prerelease alpha build missing these APIs. +**Fix**: Pin to the stable release: `Version="1.20251103.2"`. The stable line follows the `1.YYYYMMDD.patch` scheme. diff --git a/.claude/activity-development-guide/design/bindings.md b/.claude/activity-development-guide/design/bindings.md new file mode 100644 index 000000000..0f40382c5 --- /dev/null +++ b/.claude/activity-development-guide/design/bindings.md @@ -0,0 +1,155 @@ +# Bindings + +> **When to read this**: You need to integrate activity properties with Orchestrator, Assistant, or Integration Service at runtime -- for example, resolving queue names, asset names, folder paths, or connection details automatically via bindings. + +**Cross-references**: [ViewModel](viewmodel.md) | [Metadata](metadata.md) | [Project Settings](project-settings.md) | [Solutions](solutions.md) + +--- + +## ActivitiesBindings.json Structure + +The most common way to define bindings is via an `ActivitiesBindings.json` file embedded as a resource. This declaratively maps activities to their Orchestrator resource types. + +```json +{ + "ActivityBindings": [ + { + "Activities": [ + "MyCompany.Activities.GetAssetValue", + "MyCompany.Activities.SetAssetValue" + ], + "Type": "asset", + "Key": { + "Value": "BindingsKey", + "ValueSource": "Property" + }, + "Values": { + "folderPath": "FolderPath", + "name": "AssetName" + }, + "Arguments": { + "BindingsVersion": { + "Constant": "2.2" + } + } + } + ] +} +``` + +### Fields + +| Field | Description | +|---|---| +| `Activities` | Array of fully qualified activity class names that share this binding | +| `Type` | Orchestrator resource type: `"asset"`, `"queue"`, `"process"`, `"bucket"`, `"businessRule"`, `"QueueTrigger"`, `"TimeTrigger"` | +| `Key` | How the binding key is resolved. `ValueSource: "Property"` means it reads from an activity property | +| `Values` | Maps binding values to activity property names (e.g., `"folderPath"` maps to `"FolderPath"` property) | +| `Arguments` | Additional binding arguments. Can be `"Constant"` (literal value) or `"Property"` (from activity property) | + +--- + +## Real-World Example + +From System Activities (QueueTrigger): + +```json +{ + "Activities": ["UiPath.Core.Activities.QueueTrigger"], + "Type": "QueueTrigger", + "Key": { "Value": "BindingsKey", "ValueSource": "Property" }, + "Values": { + "folderPath": "FolderPath", + "name": "QueueName" + }, + "Arguments": { + "ItemsActivationThreshold": { "Value": "ItemsActivationThreshold", "ValueSource": "Property" }, + "ItemsPerJobActivationTarget": { "Value": "ItemsPerJobActivationTarget", "ValueSource": "Property" }, + "MaxJobsForActivation": { "Value": "MaxJobsForActivation", "ValueSource": "Property" }, + "BindingsVersion": { "Constant": "2.2" } + } +} +``` + +Embed the file as a resource: + +```xml + +``` + +--- + +## Binding Attributes on Activity Properties + +For SDK-based activities, bindings can also be defined via attributes: + +```csharp +// Connection binding - links to Integration Service connection +[ConnectionBinding] +public InArgument ConnectionId { get; set; } + +// Event trigger binding +[EventTriggerBinding] +public InArgument EventTriggerId { get; set; } + +// Property binding - dependent on another property +[PropertyBinding(nameof(ConnectionId))] +public InArgument AccountId { get; set; } + +// Property binding with sub-key +[PropertyBinding(nameof(ConnectionId), SubKey = "folder")] +public InArgument FolderId { get; set; } + +// Constant binding +[ConstantBinding("my-constant-key")] +public InArgument ApiEndpoint { get; set; } + +// Binding contract (known to Orchestrator/Assistant) +[BindingContract("email-send")] +public InArgument EmailConfig { get; set; } + +// Mark class as containing bindings +[HasBinding] +public class MyActivity : CodeActivity { ... } +``` + +--- + +## Registering Bindings at Design Time + +In the registration class: + +```csharp +protected override void RegisterBindings(IWorkflowDesignApi api, bool enabled) +{ + if (!api.HasFeature(DesignFeatureKeys.PackageBindingsV3)) return; + + // Register connections, event triggers, etc. +} + +protected override void RegisterDependentPropertyBindings( + IWorkflowDesignApi api, bool enabled) +{ + if (!api.HasFeature(DesignFeatureKeys.PackageBindingsV4)) return; + + // Register property-to-property bindings +} +``` + +--- + +## Connection Widget in ViewModel + +```csharp +ConnectionId.Widget = new DefaultWidget +{ + Type = ViewModelWidgetType.Connection, + Metadata = new() { { nameof(Connector), GetConnector() } } +}; +``` + +--- + +## Troubleshooting + + diff --git a/.claude/activity-development-guide/design/datasources.md b/.claude/activity-development-guide/design/datasources.md new file mode 100644 index 000000000..1f978cf9c --- /dev/null +++ b/.claude/activity-development-guide/design/datasources.md @@ -0,0 +1,212 @@ +# DataSource Patterns + +> **When to read this**: You are adding a dropdown, autocomplete, or multi-select list to an activity property in the designer. You need to populate options from static data, enums, or a server-side search API. + +**Cross-references**: [ViewModel](viewmodel.md) | [Rules and Dependencies](rules-and-dependencies.md) + +--- + +## DataSourceBuilder (Fluent API) + +```csharp +Property.DataSource = DataSourceBuilder + .WithId(x => x.Id) // Unique identifier (required) + .WithLabel(x => x.DisplayName) // Display text (required) + .WithCategory(x => x.Group) // Grouping header (optional) + .WithTooltip(x => x.HelpText) // Hover tooltip (optional) + .WithDescription(x => x.Desc) // Extended description (optional) + .WithIcon(x => x.IconPath) // Icon (optional) + .WithData(itemsList) // Static data (optional) + .Build(); +``` + +--- + +## Converter Patterns + +Converters transform between the data source items and the property value. + +### InArgument Converter + +```csharp +// For InArgument properties — handles argument wrapping automatically +.WithInArgumentSingleItemConverter() +``` + +### Custom Single-Item Converter + +```csharp +.WithSingleItemConverter( + itemToValue: item => item.ToString(), + valueToItem: value => items.FirstOrDefault(i => i.ToString() == value)) +``` + +### Enum Converter + +```csharp +.WithSingleItemConverter( + itemToValue: item => item.ToString(), + valueToItem: value => Enum.TryParse(value, out var v) ? v : default) +``` + +--- + +## Multi-Select DataSource + +```csharp +Property.DataSource = DataSourceBuilder + .WithId(x => x) + .WithLabel(x => x) + .WithMultipleSelection( + selectionToValue: items => new List(items), + valueToSelection: propertyValue => propertyValue) + .WithData(allItems) + .Build(); +``` + +--- + +## Enum DataSource + +```csharp +Property.DataSource = EnumDataSourceBuilder + .Configure() + .WithSingleItemConverter( + itemToValue: item => item.ToString(), + valueToItem: value => Enum.TryParse(value, out var v) ? v : default) + .WithData(Enum.GetValues()) + .Build(); +``` + +--- + +## Dynamic DataSource (Server-Side Search) + +Implement `IDynamicDataSourceBuilder` for data that loads on demand: + +```csharp +internal class MyDynamicDataSource : IDynamicDataSourceBuilder +{ + public async ValueTask GetDynamicDataSourceAsync( + string searchText, int limit, CancellationToken ct = default) + { + var results = await _service.SearchAsync(searchText, limit, ct); + return DataSourceBuilder + .WithId(x => x) + .WithLabel(x => x) + .WithTooltip(x => x) + .WithData(results) + .Build(); + } +} +``` + +Register in the ViewModel: + +```csharp +Property.SupportsDynamicDataSourceQuery = true; +Property.RegisterService(new MyDynamicDataSource(service)); +``` + +--- + +## Dynamic DataSource with Search (IDynamicDataSourceBuilder) + +Full pattern for a searchable autocomplete with on-demand data: + +```csharp +// In InitializeModel(): +MyProp!.SupportsDynamicDataSourceQuery = true; +MyProp!.DataSource = DataSourceBuilder + .WithId(s => s.ToLowerInvariant()) + .WithLabel(s => s) + .WithData(new[] { "Option1", "Option2" }) // initial static data + .Build(); +MyProp!.RegisterService(new MyDataSourceBuilder()); + +// Setting SupportsDynamicDataSourceQuery = true implies AutoCompleteForExpression — +// no need to set Widget explicitly. +``` + +`IDynamicDataSourceBuilder` implementation: + +```csharp +internal class MyDataSourceBuilder : IDynamicDataSourceBuilder +{ + private readonly Lazy> _config = new( + () => DataSourceBuilder + .WithId(x => x.ToLowerInvariant()) + .WithLabel(x => x) + .Build()); + + public IDataSourceConfig GetCurrentConfig() => _config.Value.GetCurrentConfig(); + + public bool SupportsFeature(string feature) => + feature == IDynamicDataSourceBuilder.FindFeature; + + public async ValueTask GetDynamicDataSourceAsync( + string searchText, int limit, int skip, CancellationToken ct = default) + { + var data = await FetchDataAsync(searchText, skip, limit); + return _config.Value.WithData(data).Build(); + } + + public ValueTask GetDynamicDataSourceAsync( + string searchText, int limit, CancellationToken ct = default) + => throw new NotSupportedException(); +} +``` + +--- + +## Updating DataSource Data After Build + +Store the built datasource to set `.Data` later: + +```csharp +private DataSource _myDataSource; + +protected override void InitializeModel() +{ + base.InitializeModel(); + _myDataSource = DataSourceBuilder + .WithId(s => s) + .WithLabel(s => s) + .WithSingleItemConverter( + itemToValue: s => new InArgument(s), + valueToItem: s => GetStringValue(s)) + .Build(); + MyProp!.DataSource = _myDataSource; + _myDataSource.Data = new string[] { "OptionA", "OptionB" }; +} +``` + +--- + +## TypeConverter for Complex Types + +For a custom type `T` in `DesignProperty`, assign a `TypeConverter` for serialization: + +```csharp +// In InitializeModel(): +MyProp!.Converter = new MyTypeConverter(); + +public class MyTypeConverter : TypeConverter +{ + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + => sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + => MyType.Parse((string)value); + + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, + object value, Type destinationType) + => ((MyType)value).ToString(); +} +``` + +--- + +## Troubleshooting + + diff --git a/.claude/activity-development-guide/design/localization.md b/.claude/activity-development-guide/design/localization.md new file mode 100644 index 000000000..fcda6f176 --- /dev/null +++ b/.claude/activity-development-guide/design/localization.md @@ -0,0 +1,68 @@ +# Localization + +> **When to read this**: You are adding user-visible strings (display names, tooltips, descriptions, categories) to an activity or its properties. + +**Cross-references**: [ViewModel](viewmodel.md) | [Metadata](metadata.md) + +--- + +## Resource File Convention + +All user-visible strings go in `Resources.resx`: + +```xml + + + Calculator + Activity display name + + + Performs basic arithmetic operations + Activity description + + + + + First Number + Property display name + + + The first operand for the calculation + Property tooltip + + + + + Input + + + Output + +``` + +--- + +## Naming Convention + +``` +{ActivityName}_DisplayName -> Activity display name +{ActivityName}_Description -> Activity description +{ActivityName}_{PropertyName}_DisplayName -> Property display name +{ActivityName}_{PropertyName}_Tooltip -> Property tooltip/description +``` + +--- + +## Usage in ViewModel + +```csharp +Property.DisplayName = Resources.Calculator_FirstNumber_DisplayName; +Property.Tooltip = Resources.Calculator_FirstNumber_Tooltip; +Property.Category = Resources.Input; +``` + +--- + +## Troubleshooting + + diff --git a/.claude/activity-development-guide/design/menu-actions.md b/.claude/activity-development-guide/design/menu-actions.md new file mode 100644 index 000000000..56a0f01ef --- /dev/null +++ b/.claude/activity-development-guide/design/menu-actions.md @@ -0,0 +1,118 @@ +# Menu Actions + +> **When to read this**: You are adding contextual buttons, links, or mode-switching toggles to activity properties in the designer panel. + +**Cross-references**: [ViewModel](viewmodel.md) | [Rules and Dependencies](rules-and-dependencies.md) + +--- + +## Basic Menu Action + +```csharp +Property.AddMenuAction(new MenuAction +{ + DisplayName = "Open Settings", + Handler = async _ => await OpenSettingsAsync() +}); +``` + +--- + +## Main (Prominent) Menu Action + +A main action is displayed as a primary action button, visually prominent. + +```csharp +Property.AddMenuAction(new MenuAction +{ + DisplayName = "Import Arguments", + IsMain = true, // Displayed as primary action button + Handler = async _ => await ImportArgumentsAsync() +}); +``` + +--- + +## Menu Action with Icon + +```csharp +Property.AddMenuAction(new MenuAction +{ + Id = "test-action", + DisplayName = "Test", + Handler = HandleTestAsync, + Metadata = new Dictionary + { + { MenuActionMetadataConstants.MaterialIconName, "lightbulb" } + } +}); +``` + +--- + +## Menu Action with URL (Studio Web) + +For actions that open a URL in Studio Web but use a local handler in Studio Desktop: + +```csharp +Property.AddMenuAction(new MenuAction +{ + DisplayName = "Manage in Orchestrator", + IsMain = true, + Url = _isStudioWeb ? orchestratorUrl : null, + Handler = _isStudioWeb ? null : ManageLocalHandler +}); +``` + +--- + +## Mode-Switching with MenuActionsService + +For properties that toggle between input modes (e.g., Dictionary vs Variable): + +```csharp +var menuService = new MenuActionsService(ModelItem); +menuService + .RegisterProperty(DictionaryInput, "Use Dictionary", isDefault: true) + .RegisterProperty(VariableInput, "Use Variable", isDefault: false) + .SetDefaultProperty(DictionaryInput) + .Build(); +``` + +--- + +## Manual Mode Switching + +If you need full control over mode toggling instead of using `MenuActionsService`: + +```csharp +DictionaryInput.AddMenuAction(new MenuAction +{ + DisplayName = "Use Variable", + Handler = _ => + { + DictionaryInput.IsVisible = false; + VariableInput.IsVisible = true; + DictionaryInput.Value = null; + return Task.CompletedTask; + } +}); + +VariableInput.AddMenuAction(new MenuAction +{ + DisplayName = "Use Dictionary", + Handler = _ => + { + VariableInput.IsVisible = false; + DictionaryInput.IsVisible = true; + VariableInput.Value = null; + return Task.CompletedTask; + } +}); +``` + +--- + +## Troubleshooting + + diff --git a/.claude/activity-development-guide/design/metadata.md b/.claude/activity-development-guide/design/metadata.md new file mode 100644 index 000000000..558a6bd98 --- /dev/null +++ b/.claude/activity-development-guide/design/metadata.md @@ -0,0 +1,213 @@ +# Metadata and Registration + +> **When to read this**: You are registering a new activity so it appears in the Studio activities panel, or you are setting up the SDK registration class for bindings, settings, or triggers. + +**Cross-references**: [ViewModel](viewmodel.md) | [Bindings](bindings.md) | [Project Settings](project-settings.md) | [Localization](localization.md) + +--- + +## Activity Metadata JSON + +Since activities are compiled into DLLs packaged in NuGet packages, a metadata JSON file is required for **activity discovery**. Services like TypeCache use this metadata to discover what activities are defined in the package without loading the assemblies. + +--- + +## Platform Split + +The metadata is split into two files based on platform availability: + +| File | Purpose | +|---|---| +| `ActivitiesMetadataPortable.json` | Cross-platform activities available in **Studio Web** and Studio Desktop | +| `ActivitiesMetadataWindows.json` | Windows-only activities available only in **Studio Desktop** | + +Both files are embedded as resources in the project. + +For packages with **only cross-platform activities** (most common case): use a single `ActivitiesMetadata.json`. +For packages with **platform-split activities**: use `ActivitiesMetadataPortable.json` + `ActivitiesMetadataWindows.json`. + +--- + +## Minimal Example + +```json +{ + "resourceManagerName": "MyCompany.MyActivities.Resources", + "activities": [ + { + "fullName": "MyCompany.MyActivities.Calculator", + "shortName": "Calculator", + "displayNameKey": "Calculator_DisplayName", + "descriptionKey": "Calculator_Description", + "categoryKey": "Math", + "iconKey": "calculator.svg", + "viewModelType": "MyCompany.MyActivities.ViewModels.CalculatorViewModel" + } + ] +} +``` + +`resourceManagerName` is required. It is the fully qualified name of the auto-generated `Resources` class from `Resources.resx`. This tells TypeCache where to find localized display names and descriptions. + +--- + +## Full Schema with All Fields + +```json +{ + "resourceManagerName": "MyCompany.MyActivities.Resources.Resources", + "activities": [ + { + "fullName": "MyCompany.MyActivities.MyActivity", + "shortName": "MyActivity", + "displayNameKey": "MyActivity_DisplayName", + "descriptionKey": "MyActivity_Description", + "displayNameAliasKeys": ["MyActivity_Alias1"], + "categoryKey": "MyCategory", + "iconKey": "my-activity.svg", + "viewModelType": "MyCompany.MyActivities.ViewModels.MyActivityViewModel", + "defaultFactory": "MyCompany.MyActivities.Factories.MyActivityFactory", + "typeArgumentProperty": "Collection", + "codedWorkflowSupport": false, + "browsable": true, + "properties": [ + { + "name": "InputProp", + "displayNameKey": "MyActivity_InputProp_DisplayName", + "tooltipKey": "MyActivity_InputProp_Tooltip", + "isRequired": true, + "isVisible": true, + "isPrincipal": true, + "category": { + "name": "Input", + "displayNameKey": "Input" + }, + "widget": { + "type": "variable" + } + } + ] + } + ] +} +``` + +--- + +## Activity-Level Fields + +| Field | Required | Description | +|---|---|---| +| `fullName` | Yes | Fully qualified activity class name (namespace + class) | +| `shortName` | No | Short name used internally | +| `displayNameKey` | Yes | Resource key for the activity's display name | +| `descriptionKey` | Yes | Resource key for the activity's description | +| `displayNameAliasKeys` | No | Alternative search names (array of resource keys) | +| `categoryKey` | Yes | Resource key or literal for the toolbox category | +| `iconKey` | Yes | Filename of the embedded SVG icon | +| `viewModelType` | No | Fully qualified ViewModel class name (if the activity has a custom ViewModel) | +| `defaultFactory` | No | Factory class for generic activities (e.g., `AddToCollection`) | +| `typeArgumentProperty` | No | Property name that determines the type argument for generic activities | +| `codedWorkflowSupport` | No | Whether the activity supports coded workflows (default: true) | +| `browsable` | No | Whether to show in the activities panel (default: true). Set to `false` for legacy/internal activities | +| `resourceManagerName` | No | Fully qualified name of the `.resx` resource class (top-level, shared by all activities) | + +--- + +## Property-Level Fields + +Inside the `properties` array of an activity: + +| Field | Description | +|---|---| +| `name` | Must match the Activity class property name exactly | +| `displayNameKey` | Resource key for display name | +| `tooltipKey` | Resource key for tooltip | +| `isRequired` | Whether the property is required | +| `isVisible` | Whether the property is visible | +| `isPrincipal` | Whether the property appears in the main panel | +| `category` | Category grouping with `name` and/or `displayNameKey` | +| `widget` | Widget override with `type` (e.g., `"variable"`, `"input"`) | + +Property definitions in the metadata JSON provide the **initial/default** configuration. The ViewModel's `InitializeModel()` can override these settings at runtime. When a ViewModel is specified, it typically takes full control of property configuration. When no ViewModel is specified, the metadata JSON is the sole source of property configuration. + +--- + +## Default Activity Icon + +Every activity needs an icon. Place SVG files in `Resources/Icons/`. The `.csproj` glob `` picks them up automatically. + +A generic default icon (24x24): + +```xml + + + + + +``` + +Reference it in `ActivitiesMetadata.json` as `"iconKey": "activityicon.svg"`. For per-activity icons, add separate SVGs and update each activity's `iconKey`. + +--- + +## Embedding in .csproj + +```xml + + + + +``` + +--- + +## SDK Registration (BaseViewModelDesignerRegistration) + +For SDK-based activities, implement a registration class: + +```csharp +internal class ViewModelDesignerRegistration : BaseViewModelDesignerRegistration +{ + protected override void RegisterTriggers(IWorkflowDesignApi api, bool enabled) { } + + protected override void RegisterBindings(IWorkflowDesignApi api, bool enabled) + { + // Register V3 bindings + if (api.HasFeature(DesignFeatureKeys.PackageBindingsV3)) + { + // Register connection bindings, event triggers, etc. + } + } + + protected override void RegisterDependentPropertyBindings( + IWorkflowDesignApi api, bool enabled) + { + // Register V4 dependent property bindings + if (api.HasFeature(DesignFeatureKeys.PackageBindingsV4)) + { + // Register property-level bindings + } + } + + protected override void PublishSettings(IWorkflowDesignApi api, bool enabled) + { + if (api.HasFeature(DesignFeatureKeys.Settings)) + { + api.PublishProjectSettings( + ArgumentSettingAttribute.SettingsPrefix, + "My Activities", + "Settings for My Activities package", + GetProjectSettings()); + } + } + + protected override void FinalizeRegistration(IWorkflowDesignApi api) { } +} +``` + +--- + +## Troubleshooting + + diff --git a/.claude/activity-development-guide/design/project-settings.md b/.claude/activity-development-guide/design/project-settings.md new file mode 100644 index 000000000..874f9e9e3 --- /dev/null +++ b/.claude/activity-development-guide/design/project-settings.md @@ -0,0 +1,73 @@ +# Project Settings + +> **When to read this**: You are exposing configurable default values at the Studio project level -- for example, a default timeout, API URL, or server address that applies to all instances of your activities in a project. + +**Cross-references**: [ViewModel](viewmodel.md) | [Metadata](metadata.md) | [Validation](validation.md) + +--- + +## Defining Settings on Activity Properties + +Use `ArgumentSettingAttribute` on Activity class properties to declare them as project-level settings: + +```csharp +// On the Activity class: +[ArgumentSetting("MyActivity", "DefaultTimeout", defaultValue: "30", required: false)] +public InArgument Timeout { get; set; } + +[ArgumentSetting("MyActivity", "ApiUrl", required: true, packageKey: "MyPackage")] +public InArgument ApiUrl { get; set; } +``` + +The `ArgumentSettingAttribute` automatically generates a settings key like: +`UiPath.Sdk.Activities.{section}.{property}` + +--- + +## Publishing Settings at Registration + +```csharp +protected override void PublishSettings(IWorkflowDesignApi api, bool enabled) +{ + if (!api.HasFeature(DesignFeatureKeys.Settings)) return; + + var settings = new List(); + + var section = new ProjectSettingsSection + { + Key = "MyActivity", + Title = "My Activity Settings", + Description = "Configure default values" + }; + section.Add("DefaultTimeout", "Default Timeout (seconds)", defaultValue: 30); + section.Add("ApiUrl", "API URL", defaultValue: "https://api.example.com"); + settings.Add(section); + + api.PublishProjectSettings( + ArgumentSettingAttribute.SettingsPrefix, + "My Activities", + "Default settings for My Activities package", + settings); +} +``` + +--- + +## Validating Settings in ViewModel + +```csharp +protected override IEnumerable ValidateModel() +{ + return base.ValidateModel().Concat( + DesignServices.ProjectSettings?.Validate( + activity: ModelItem?.GetCurrentValue(), + messageFormatter: propName => $"Setting '{propName}' is required") + ?? Enumerable.Empty()); +} +``` + +--- + +## Troubleshooting + + diff --git a/.claude/activity-development-guide/design/rules-and-dependencies.md b/.claude/activity-development-guide/design/rules-and-dependencies.md new file mode 100644 index 000000000..e0e19fefa --- /dev/null +++ b/.claude/activity-development-guide/design/rules-and-dependencies.md @@ -0,0 +1,208 @@ +# Rules and Dependencies + +> **When to read this**: You need to react to property value changes in the designer -- for example, showing/hiding properties conditionally, updating a DataSource when a parent property changes, or running async operations with a busy indicator. + +**Cross-references**: [ViewModel](viewmodel.md) | [DataSources](datasources.md) | [Validation](validation.md) + +--- + +## Defining Rules + +Rules are reactive handlers that execute when property values change. Define them in `InitializeRules()`. + +```csharp +protected override void InitializeRules() +{ + base.InitializeRules(); + + // Synchronous rule + Rule(nameof(PropertyA), OnPropertyAChanged); + + // Async rule + Rule(nameof(PropertyB), async () => await OnPropertyBChangedAsync()); + + // Rule that also runs on initialization + Rule("InitRule", OnSomethingChanged, runOnInit: true); + + // Rule that does NOT run on initialization + Rule("LazyRule", OnLazyChange, runOnInit: false); +} +``` + +--- + +## Registering Dependencies + +Dependencies tell the framework which property changes should trigger which rules. + +```csharp +protected override void ManualRegisterDependencies() +{ + base.ManualRegisterDependencies(); + + // When PropertyA.Value changes, trigger the rule named "PropertyA" + RegisterDependency(PropertyA, nameof(PropertyA.Value), nameof(PropertyA)); + + // Multiple properties can trigger the same rule + RegisterDependency(PropertyB, nameof(PropertyB.Value), "SharedRule"); + RegisterDependency(PropertyC, nameof(PropertyC.Value), "SharedRule"); +} +``` + +--- + +## Rule and Dependency Patterns + +Three organizational patterns exist for rules and dependencies. Match the pattern used in the file you're editing. + +### Pattern A: Separate methods (most explicit) + +Rules in `InitializeRules()`, dependencies in `ManualRegisterDependencies()`: + +```csharp +protected override void InitializeRules() +{ + base.InitializeRules(); + Rule(nameof(OnCategoryChanged), OnCategoryChanged, runOnInit: false); +} + +protected override void ManualRegisterDependencies() +{ + base.ManualRegisterDependencies(); + RegisterDependency(Category, nameof(Category.Value), nameof(OnCategoryChanged)); +} + +private void OnCategoryChanged() +{ + CategoryItem!.IsVisible = Category!.HasValue; +} +``` + +### Pattern B: Inline (compact) + +Both `Rule` and `RegisterDependency` inside `InitializeRules()`, no `ManualRegisterDependencies()`: + +```csharp +protected override void InitializeRules() +{ + base.InitializeRules(); + Rule("OnCategoryChanged", OnCategoryChanged, false); + RegisterDependency(Category, nameof(DesignProperty.Value), "OnCategoryChanged"); +} +``` + +### Pattern C: Inline lambda + +Rule handler is an anonymous lambda directly in `InitializeRules()`: + +```csharp +protected override void InitializeRules() +{ + base.InitializeRules(); + + Rule("VisibleActionType", () => + { + MyProperty1!.IsVisible = someCondition; + MyProperty2!.IsVisible = otherCondition; + }); + + // Async lambda + Rule("LaunchAction", async () => + { + await _someService.StartAsync(); + }, runOnInit: false); +} + +protected override void ManualRegisterDependencies() +{ + RegisterDependency(TriggerProp, nameof(TriggerProp.Value), "VisibleActionType"); +} +``` + +**Note**: `base.InitializeRules()` and `base.ManualRegisterDependencies()` calls are optional in many codebases. Include them unless the existing file omits them. + +--- + +## Common Rule Patterns + +### Conditional Visibility + +```csharp +Rule("Visibility", () => +{ + var mode = GetStringValue(ModeProperty.Value); + AdvancedProp1.IsVisible = mode == "Advanced"; + AdvancedProp2.IsVisible = mode == "Advanced"; + SimpleProp.IsVisible = mode == "Simple"; +}); + +// Register dependency +RegisterDependency(ModeProperty, nameof(ModeProperty.Value), "Visibility"); +``` + +### Dynamic DataSource Update + +```csharp +Rule(nameof(ParentProperty), () => +{ + var parentValue = GetStringValue(ParentProperty.Value); + _childDataSource.ParentFilter = parentValue; + // DataSource re-queries automatically on next search +}); + +RegisterDependency(ParentProperty, nameof(ParentProperty.Value), nameof(ParentProperty)); +``` + +### Async Rule with Busy Indicator + +```csharp +// Pattern 1: Direct busy service usage +Rule(nameof(SourceProperty), async () => +{ + await using (await _busyService.BeginAsync( + new BusyOptions { Kind = BusyKind.Activity, Message = "Loading..." })) + { + var data = await _service.FetchAsync(); + Dispatcher.Invoke(() => TargetProperty.Value = data); + } +}); + +// Pattern 2: RunWithBusyService helper (common in System Activities) +Rule(nameof(WorkflowFileName), async () => await RunWithBusyService(WorkflowFileNameChangedRule), false); + +private async Task RunWithBusyService(Func action) +{ + if (_busyService != null) + { + await using (await _busyService.BeginAsync( + new BusyOptions { Kind = BusyKind.Activity })) + { + await action(); + } + } + else + { + await action(); + } +} +``` + +--- + +## Important: Dispatcher.Invoke() for Async Rules + +When updating properties from async rules or background threads, you **must** use `Dispatcher.Invoke()`. Failing to do so can cause cross-thread exceptions or silently dropped updates. + +```csharp +Dispatcher.Invoke(() => +{ + TargetProperty.Value = newValue; + TargetProperty.IsVisible = true; +}); +``` + +--- + +## Troubleshooting + + diff --git a/.claude/activity-development-guide/design/solutions.md b/.claude/activity-development-guide/design/solutions.md new file mode 100644 index 000000000..37e4c2be1 --- /dev/null +++ b/.claude/activity-development-guide/design/solutions.md @@ -0,0 +1,95 @@ +# Solutions vs Project Scope + +> **When to read this**: Your activity may behave differently when running inside a UiPath Solution (cloud-managed) versus a standalone project. You need to detect the context and configure properties accordingly. + +**Cross-references**: [ViewModel](viewmodel.md) | [DataSources](datasources.md) | [Bindings](bindings.md) + +--- + +## Detecting Solutions Context + +```csharp +protected override async ValueTask InitializeModelAsync() +{ + await base.InitializeModelAsync(); + + var userDesignContext = Services.GetService(); + bool isInSolution = !string.IsNullOrEmpty(userDesignContext?.SolutionId); + + if (isInSolution) + { + _solutionResources = Services.GetService(); + await InitializeSolutionsScopePropertiesAsync(); + } + else + { + await InitializeProjectScopePropertiesAsync(); + } +} +``` + +--- + +## BaseSolutionResourceViewModel Pattern + +For activities that work with Orchestrator resources (assets, queues, etc.), derive from `BaseSolutionResourceViewModel` to get built-in project vs. solution branching: + +```csharp +public class MyAssetViewModel : BaseSolutionResourceViewModel +{ + protected override async ValueTask InitializeProjectScopePropertiesAsync() + { + // Configure for standalone project: + // - Show FolderPath property + // - Register Orchestrator-based data sources + AssetName.SupportsDynamicDataSourceQuery = true; + AssetName.RegisterService( + new OrchestratorAssetDataSource(_tokenProvider)); + } + + protected override async ValueTask InitializeSolutionsScopePropertiesAsync() + { + // Configure for Solutions: + // - Hide FolderPath (managed by Solution) + // - Use SolutionResourcesWidget instead + FolderPath.IsVisible = false; + AssetName.Widget = new SolutionResourcesWidget + { + ResourceType = SolutionsResourceKind.Asset + }; + } +} +``` + +--- + +## Feature Detection for Solutions + +Always check for feature availability before using platform-specific APIs: + +```csharp +if (_workflowDesignApi?.HasFeature(DesignFeatureKeys.Settings) == true) +{ + // Safe to use project settings API +} + +if (_workflowDesignApi?.HasFeature(DesignFeatureKeys.PackageBindingsV4) == true) +{ + // Safe to use V4 bindings +} + +if (_workflowDesignApi?.HasFeature(DesignFeatureKeys.WidgetSupportInfoService) == true) +{ + // Check specific widget availability + if (_workflowDesignApi.WidgetSupportInfoService?.IsWidgetSupported(widgetType) == true) + { + Property.Widget = new DefaultWidget { Type = widgetType }; + } +} +``` + +--- + +## Troubleshooting + + diff --git a/.claude/activity-development-guide/design/validation.md b/.claude/activity-development-guide/design/validation.md new file mode 100644 index 000000000..ab6390fff --- /dev/null +++ b/.claude/activity-development-guide/design/validation.md @@ -0,0 +1,80 @@ +# Validation + +> **When to read this**: You need to add design-time validation to activity properties -- single-property checks, cross-property model validation, or preview validation for live-preview activities. + +**Cross-references**: [ViewModel](viewmodel.md) | [Rules and Dependencies](rules-and-dependencies.md) + +--- + +## Property-Level Validation + +### Synchronous Validator + +```csharp +Property.AddValidator(value => +{ + if (string.IsNullOrEmpty(value?.ToString())) + return new ValidationResult("Value is required", new[] { nameof(Property) }); + return ValidationResult.Success; +}); +``` + +### Async Validator + +```csharp +Property.AddAsyncValidator(async (value, ct) => +{ + var isValid = await _service.ValidateAsync(value, ct); + if (!isValid) + return new ValidationResult("Invalid value", new[] { nameof(Property) }); + return ValidationResult.Success; +}); +``` + +--- + +## Model-Level Validation (ValidateModel) + +Override `ValidateModel()` for cross-property validation: + +```csharp +protected override IEnumerable ValidateModel() +{ + return base.ValidateModel().Concat(MyErrors()); + + IEnumerable MyErrors() + { + if (StartDate.HasValue && EndDate.HasValue && StartDate.Value > EndDate.Value) + { + yield return new ValidationResult( + "Start date must be before end date", + new[] { nameof(StartDate), nameof(EndDate) }); + } + } +} +``` + +--- + +## Preview Validation (PreviewActivityViewModel) + +For activities that derive from `PreviewActivityViewModel` and support live preview: + +```csharp +protected override PreviewParametersValidationResult ValidatePreviewParameters() + => ValidatePreviewArgument(Source); +``` + +`ValidatePreviewArgument` returns one of three results: + +| Result | Meaning | +|---|---| +| `AllParametersValid` | Can show preview | +| `FoundParametersWithExpression` | Has expression, cannot preview at design time | +| `MissingRequiredParameters` | Nothing to preview | + +--- + +## Troubleshooting + + diff --git a/.claude/activity-development-guide/design/viewmodel.md b/.claude/activity-development-guide/design/viewmodel.md new file mode 100644 index 000000000..efa9123ba --- /dev/null +++ b/.claude/activity-development-guide/design/viewmodel.md @@ -0,0 +1,425 @@ +# ViewModel (Design-Time) + +> **When to read this**: You are creating or modifying a ViewModel class for a UiPath activity. You need to understand base class selection, property types, configuration API, lifecycle hooks, or partial class organization. + +**Cross-references**: [DataSources](datasources.md) | [Rules and Dependencies](rules-and-dependencies.md) | [Menu Actions](menu-actions.md) | [Validation](validation.md) | [Metadata](metadata.md) | [Localization](localization.md) + +--- + +## Base Class Options + +| Base Class | When to Use | +|---|---| +| `DesignPropertiesViewModel` | Standard activities (most common) | +| `BaseViewModel` / `BaseViewModel` | SDK activities needing DI and service policies | +| `PreviewActivityViewModel` | Activities with live preview (formatting, regex) | +| `BaseOrchestratorClientActivityViewModel` | Activities calling Orchestrator APIs | +| `BaseSolutionResourceViewModel` | Activities working with Solutions resources | + +--- + +## Minimal ViewModel Example + +```csharp +using System.Activities.DesignViewModels; + +namespace MyCompany.MyActivities.ViewModels; + +public class CalculatorViewModel : DesignPropertiesViewModel +{ + // Property names MUST match the Activity class property names exactly + public DesignInArgument? FirstNumber { get; set; } + public DesignInArgument? SecondNumber { get; set; } + public DesignProperty? SelectedOperation { get; set; } + public DesignOutArgument? Result { get; set; } + + public CalculatorViewModel(IDesignServices services) : base(services) { } + + protected override void InitializeModel() + { + base.InitializeModel(); + PersistValuesChangedDuringInit(); + + var order = 0; + + // Input properties + FirstNumber!.DisplayName = Resources.Calculator_FirstNumber_DisplayName; + FirstNumber!.Tooltip = Resources.Calculator_FirstNumber_Tooltip; + FirstNumber!.IsRequired = true; + FirstNumber!.IsPrincipal = true; + FirstNumber!.OrderIndex = order++; + + SecondNumber!.DisplayName = Resources.Calculator_SecondNumber_DisplayName; + SecondNumber!.Tooltip = Resources.Calculator_SecondNumber_Tooltip; + SecondNumber!.IsRequired = true; + SecondNumber!.IsPrincipal = true; + SecondNumber!.OrderIndex = order++; + + SelectedOperation!.DisplayName = Resources.Calculator_Operation_DisplayName; + SelectedOperation!.Tooltip = Resources.Calculator_Operation_Tooltip; + SelectedOperation!.IsPrincipal = true; + SelectedOperation!.OrderIndex = order++; + + // Output properties (not principal, at the end) + Result!.DisplayName = Resources.Calculator_Result_DisplayName; + Result!.Tooltip = Resources.Calculator_Result_Tooltip; + Result!.OrderIndex = order++; + } +} +``` + +### Alternative: Primary Constructor (C# 12) + +```csharp +public class MyViewModel(IDesignServices services) : DesignPropertiesViewModel(services) +{ + // No explicit constructor body needed +} +``` + +When adding to an existing file, match its constructor style. + +--- + +## ViewModel Property Types + +| ViewModel Type | Maps to Activity Type | Purpose | +|---|---|---| +| `DesignInArgument` | `InArgument` | Input that accepts expressions/variables | +| `DesignOutArgument` | `OutArgument` | Output written to variables | +| `DesignInOutArgument` | `InOutArgument` | Bidirectional argument | +| `DesignProperty` | Direct `T` property | Constants, enums, non-argument values | + +--- + +## Nullable Properties and the `!` Operator + +In projects with `enable`, all ViewModel design properties must be declared nullable: + +```csharp +public DesignInArgument? Input { get; set; } +public DesignOutArgument? Result { get; set; } +public DesignProperty? Mode { get; set; } +``` + +The framework initializes these before `InitializeModel()` runs, so they are never actually null at that point. Use the null-forgiving operator (`!`) on every access inside `InitializeModel()`: + +```csharp +Input!.DisplayName = Resources.MyActivity_Input_DisplayName; +Input!.IsRequired = true; +Input!.IsPrincipal = true; +``` + +**Every individual member access needs `!`** — it is not sufficient to use it only on the first access to a property. + +The `= new()` inline initializer is optional. `DesignPropertiesViewModel` initializes all design properties via reflection. Many codebases use `= new()` anyway for clarity — match the style of the file you're editing. + +--- + +## Property Configuration API + +```csharp +property.DisplayName = "User-Facing Name"; // Label in designer +property.Tooltip = "Hover help text"; // Tooltip +property.EditPlaceholder = "Type here..."; // Placeholder in edit mode +property.IsPrincipal = true; // Show in main panel (non-collapsible) +property.IsRequired = true; // Validation: must be set +property.IsVisible = true; // Show/hide dynamically +property.IsReadOnly = false; // Enable/disable editing +property.OrderIndex = 0; // Display order (lower = higher) +property.Category = "Input"; // Group label +property.Widget = new DefaultWidget { ... }; // Widget type override +property.DataSource = ...; // Dropdown/autocomplete data +property.SupportsDynamicDataSourceQuery = true;// Enable server-side search +``` + +**`Name` vs `DisplayName`**: `DisplayName` is the human-readable label (use `Resources.*` keys). `Name` is the internal property key (defaults to the C# property name). Only set `Name` explicitly for `[NotMappedProperty]` UI elements where you need a specific label without a resource string. + +--- + +## Property Ordering + +Two patterns exist for managing property order: + +```csharp +// Pattern 1: Manual counter (standard DesignPropertiesViewModel) +var order = 0; +FirstProp.OrderIndex = order++; +SecondProp.OrderIndex = order++; + +// Pattern 2: Built-in counter (SDK BaseViewModel) +// BaseViewModel provides a PropertyOrderIndex counter: +FirstProp.OrderIndex = PropertyOrderIndex++; +SecondProp.OrderIndex = PropertyOrderIndex++; +``` + +--- + +## Expression Language Detection (SDK) + +SDK ViewModels can detect whether the project uses C# or VB.NET expressions: + +```csharp +// Available in BaseViewModel +if (ExpressionLanguage == ExpressionLanguage.CSharp) +{ + // Generate C# expression syntax +} +else +{ + // Generate VB.NET expression syntax +} +``` + +--- + +## Property Name Override + +When the ViewModel property name differs from the Activity property name, override via `Name`: + +```csharp +// ViewModel property "AssetName" maps to Activity property "OrchestratorCredentialName" +public DesignInArgument AssetName { get; set; } = new() { Name = nameof(OrchestratorCredentialName) }; + +// ViewModel property "FolderPath" maps to Activity property "OrchestratorFolderPath" +public DesignInArgument FolderPath { get; set; } = new() { Name = nameof(OrchestratorFolderPath) }; +``` + +### Non-Generic DesignOutArgument + +For activities with `OutArgument Output { get; set; }` (no type parameter), use `DesignOutArgument Output { get; set; }` (without ``) in the ViewModel. This is for outputs whose type is determined dynamically at runtime. + +--- + +## Property Attributes + +```csharp +[NotMappedProperty] // Property exists only in ViewModel, not persisted to Activity +public DesignProperty Preview { get; set; } + +[NotMapped] // Alias for NotMappedProperty +public DesignProperty InfoDisplay { get; set; } + +[Browsable(false)] // Hidden from properties panel +public DesignProperty InternalState { get; set; } +``` + +`[NotMappedProperty]` marks a ViewModel property that has no corresponding Activity property. Use for UI-only elements (action buttons, display labels, preview fields): + +```csharp +[NotMappedProperty] +public DesignProperty? LaunchButton { get; set; } + +// In InitializeModel(): +LaunchButton!.DisplayName = Resources.MyActivity_Launch_DisplayName; +LaunchButton!.Widget = new DefaultWidget { Type = ViewModelWidgetType.ActionButton }; +LaunchButton!.IsPrincipal = true; +``` + +--- + +## Reading Property Values in Rules + +```csharp +// Try to get a literal value (non-expression) from an InArgument +if (Property.TryGetLiteralOfType(out var literalValue)) +{ + // literalValue is the typed value +} + +// Get expression text +var expressionText = Property.Value.GetExpressionText(); + +// Check if property has a value +if (Property.HasValue) { ... } +``` + +--- + +## ViewModel Lifecycle + +The framework calls these methods in order during initialization: + +``` +1. Constructor(IDesignServices services) +2. InitializeModel() <- Configure properties (sync) +3. InitializeModelAsync() <- Configure properties (async, e.g., fetch data) +4. InitializeRules() <- Register reactive rules (sync) +5. InitializeRulesAsync() <- Register reactive rules (async) +6. AutoRegisterDependencies() <- Framework auto-detects dependencies +7. ManualRegisterDependencies() <- Register explicit dependencies +8. Execute all rules with runOnInit: true +9. ProcessDependencies() <- Wire up property change handlers +``` + +### InitializeModel vs InitializeModelAsync + +Two initialization methods are available: + +- `InitializeModel()` (sync): Call `base.InitializeModel()` as the **first** line. Use when no async work is needed. +- `InitializeModelAsync()` (async): Call `base.InitializeModelAsync()` as the **last** line (return it). Use when initialization needs async calls. + +```csharp +// Sync — base first, then configure +protected override void InitializeModel() +{ + base.InitializeModel(); + PersistValuesChangedDuringInit(); + MyProp!.IsPrincipal = true; +} + +// Async — configure first, base last (returned) +protected override ValueTask InitializeModelAsync() +{ + MyProp!.IsPrincipal = true; + return base.InitializeModelAsync(); +} + +// Async with actual awaits +protected override async ValueTask InitializeModelAsync() +{ + await base.InitializeModelAsync(); + var data = await someService.GetDataAsync(); + MyProp!.DataSource = BuildDataSource(data); +} +``` + +`PersistValuesChangedDuringInit()` is only needed when property values set during init must persist. Many simple ViewModels omit it. When used, it must go **immediately after** `base.InitializeModel()`, before any property assignments. + +--- + +## Override Pattern Example + +```csharp +protected override void InitializeModel() +{ + base.InitializeModel(); + PersistValuesChangedDuringInit(); + // Configure properties here +} + +protected override async ValueTask InitializeModelAsync() +{ + await base.InitializeModelAsync(); + // Async initialization (API calls, data fetching) +} + +protected override void InitializeRules() +{ + base.InitializeRules(); + Rule(nameof(PropertyA), OnPropertyAChanged); + Rule("AsyncRule", async () => await DoSomethingAsync(), runOnInit: true); +} + +protected override void ManualRegisterDependencies() +{ + base.ManualRegisterDependencies(); + RegisterDependency(PropertyA, nameof(PropertyA.Value), nameof(PropertyA)); +} +``` + +--- + +## Constructor vs InitializeModel + +Simple property configuration (IsPrincipal, IsReadOnly) can be done in the constructor. Reserve `InitializeModel()` for configuration that needs the full initialization context (DataSource, service access). + +For service access: +- **Constructor**: use the `services` parameter: `services.GetService()` +- **InitializeModel/rules**: use the base `Services` property: `Services.GetService()` + +--- + +## SDK ViewModel with Service Policy + +When using the Activities.SDK framework with dependency injection: + +```csharp +// Custom service policy for design-time DI +internal class MyDesignServicePolicy : DefaultDesignServicePolicy +{ + public override IServicePolicy Register(Action collection = null) + { + _services.TryAddSingleton(); + return base.Register(collection); + } +} + +// ViewModel using the custom policy +public class MyActivityViewModel : BaseViewModel +{ + public DesignInArgument Input { get; set; } + public DesignOutArgument Output { get; set; } + + public MyActivityViewModel(IDesignServices services) : base(services) { } + + protected override void InitializeModel() + { + base.InitializeModel(); + // Access injected services + var myService = ActivityServices.GetService(); + } +} +``` + +--- + +## Partial Class Organization (Large ViewModels) + +For complex ViewModels, split into partial classes by concern: + +``` +MyActivityViewModel.cs # Core properties and initialization +MyActivityViewModel.Configuration.cs # Widget/property configuration +MyActivityViewModel.Rules.cs # Rule definitions and handlers +MyActivityViewModel.Actions.cs # Menu action handlers +``` + +--- + +## Resource Cleanup + +If a ViewModel subscribes to events or holds disposable resources, override `Dispose(bool)`: + +```csharp +private bool _disposedValue; + +protected override void Dispose(bool disposing) +{ + if (!_disposedValue) + { + if (disposing) + { + _channel.MessageReceived -= OnMessageReceived; + } + _disposedValue = true; + } + base.Dispose(disposing); +} +``` + +--- + +## UpdateAsync Override + +Override `UpdateAsync` to intercept property value changes in the designer (e.g., to send a value to an external service): + +```csharp +protected override async ValueTask UpdateAsync( + string propertyName, object value) +{ + if (propertyName == nameof(MyProp)) + { + await _service.ProcessAsync(value); + } + return await base.UpdateAsync(propertyName, value); +} +``` + +Always call `base.UpdateAsync()` to complete the standard update flow. + +--- + +## Troubleshooting + + diff --git a/.claude/activity-development-guide/design/widgets/action-button.md b/.claude/activity-development-guide/design/widgets/action-button.md new file mode 100644 index 000000000..e50a3e40b --- /dev/null +++ b/.claude/activity-development-guide/design/widgets/action-button.md @@ -0,0 +1,129 @@ +# ActionButton Widget + +## When to read this + +Read this document when you need to: +- Add a clickable button to the activity designer +- Trigger an action (open a dialog, run configuration, refresh data) from the designer +- Wire up button click handling via Rules and `RegisterDependency` + +--- + +## Overview + +The `ActionButton` widget renders a clickable button in the activity designer. When clicked, it triggers a Rule that executes your custom logic. Common uses include launching configuration dialogs, refreshing data from a server, or running validation. + +Use with `DesignProperty` (the property type is typically not meaningful -- the button is the interaction point, not the value). + +--- + +## Basic Usage + +```csharp +Property.Widget = new DefaultWidget { Type = ViewModelWidgetType.ActionButton }; +Property.DisplayName = "Click Me"; +Property.IsPrincipal = true; + +// Handle the click via a Rule +Rule("ActionProperty", () => DoSomething(), false); +RegisterDependency(Property, nameof(DesignProperty.Value), "ActionProperty"); +``` + +### How It Works + +1. The `ActionButton` widget renders a button labeled with `Property.DisplayName`. +2. When the user clicks the button, the framework changes the property's `Value`, which triggers the registered dependency. +3. The dependency fires the Rule named `"ActionProperty"`. +4. Your Rule handler (`DoSomething()`) executes. + +--- + +## Step-by-Step Setup + +### 1. Define the property + +```csharp +public DesignProperty ConfigureButton { get; set; } = new(); +``` + +### 2. Configure the widget in InitializeModel + +```csharp +protected override void InitializeModel() +{ + base.InitializeModel(); + + ConfigureButton.Widget = new DefaultWidget { Type = ViewModelWidgetType.ActionButton }; + ConfigureButton.DisplayName = "Configure Fields"; + ConfigureButton.IsPrincipal = true; + ConfigureButton.OrderIndex = PropertyOrderIndex++; +} +``` + +### 3. Register the click handler + +```csharp +protected override void RegisterDependencies() +{ + base.RegisterDependencies(); + + Rule("ConfigureButtonClicked", () => + { + // Your action logic here + OpenConfigurationDialog(); + }, false); + + RegisterDependency(ConfigureButton, nameof(DesignProperty.Value), "ConfigureButtonClicked"); +} +``` + +The third parameter of `Rule()` (`false`) means the rule does not run during initialization -- it only runs when the user clicks. + +--- + +## Examples + +### Open a configuration dialog + +```csharp +Rule("OpenDialog", async () => +{ + var dialogService = Services.GetService(); + var result = await dialogService.ShowDialogAsync(new MyConfigDialog(currentConfig)); + if (result != null) + { + ApplyConfiguration(result); + } +}, false); + +RegisterDependency(ConfigureButton, nameof(DesignProperty.Value), "OpenDialog"); +``` + +### Refresh data from server + +```csharp +Rule("RefreshData", async () => +{ + var busyService = Services.GetService(); + using (busyService.ShowBusy("Loading data...")) + { + var data = await _apiClient.FetchDataAsync(); + UpdateDataSource(data); + } +}, false); + +RegisterDependency(RefreshButton, nameof(DesignProperty.Value), "RefreshData"); +``` + +--- + +## Troubleshooting + +**Button click does nothing.** +Verify that `RegisterDependency` is called with the correct property, property name (`nameof(DesignProperty.Value)`), and rule name. The rule name in `RegisterDependency` must exactly match the name in `Rule()`. + +**Button handler runs during initialization.** +Set the third parameter of `Rule()` to `false` to prevent the rule from running when the ViewModel initializes. If set to `true`, the handler runs once at startup. + +**Button is not visible.** +Check that `Property.IsPrincipal = true` is set. Non-principal properties may be hidden in collapsed sections of the designer. Also verify `Property.IsVisible` is not set to `false`. diff --git a/.claude/activity-development-guide/design/widgets/autocomplete.md b/.claude/activity-development-guide/design/widgets/autocomplete.md new file mode 100644 index 000000000..1c27e5b58 --- /dev/null +++ b/.claude/activity-development-guide/design/widgets/autocomplete.md @@ -0,0 +1,139 @@ +# AutoCompleteForExpression Widget + +## When to read this + +Read this document when you need to: +- Display a searchable dropdown that also supports expressions +- Load options dynamically from a server/API based on user input +- Use `IDynamicDataSourceBuilder` for server-side search + +For general DataSource patterns (shared with dropdowns and other widgets), see [../datasources.md](../datasources.md). + +--- + +## Overview + +The `AutoCompleteForExpression` widget combines a searchable dropdown with expression support. The user can either select from a list of options or type a VB/C# expression. This is the standard widget for `DesignInArgument` properties that need a predefined list of suggestions. + +--- + +## Static Autocomplete + +Use static autocomplete when all options are known at design time and the list is small enough to load in memory. + +```csharp +Property.Widget = new DefaultWidget { Type = ViewModelWidgetType.AutoCompleteForExpression }; +Property.DataSource = DataSourceBuilder + .WithId(s => s) + .WithLabel(s => s) + .WithInArgumentSingleItemConverter() + .WithData(items) + .Build(); +``` + +### Key Methods + +| Method | Purpose | +|---|---| +| `WithId(T => string)` | Unique identifier for each item (stored value). | +| `WithLabel(T => string)` | Display text shown in the dropdown. | +| `WithInArgumentSingleItemConverter()` | Converts the selected item to an `InArgument` value. Required for `DesignInArgument` properties. | +| `WithData(IEnumerable)` | Provides the static list of options at build time. | + +### Example: Folder name autocomplete + +```csharp +FolderName.Widget = new DefaultWidget { Type = ViewModelWidgetType.AutoCompleteForExpression }; +FolderName.DataSource = DataSourceBuilder + .WithId(f => f) + .WithLabel(f => f) + .WithInArgumentSingleItemConverter() + .WithData(new[] { "Inbox", "Sent", "Drafts", "Archive", "Spam" }) + .Build(); +``` + +--- + +## Dynamic Autocomplete (Server-Side Search) + +Use dynamic autocomplete when options are loaded from an API or depend on user input. The `IDynamicDataSourceBuilder` interface enables server-side search -- the framework calls your builder with the user's search text and you return matching results. + +```csharp +Property.Widget = new DefaultWidget { Type = ViewModelWidgetType.AutoCompleteForExpression }; +Property.SupportsDynamicDataSourceQuery = true; +Property.RegisterService(new MyDataSourceBuilder()); +``` + +### Required Setup + +1. Set `SupportsDynamicDataSourceQuery = true` on the property. +2. Register an `IDynamicDataSourceBuilder` implementation via `RegisterService`. + +### IDynamicDataSourceBuilder Implementation + +```csharp +internal class MyDataSourceBuilder : IDynamicDataSourceBuilder +{ + public async Task BuildAsync( + DynamicDataSourceBuilderContext context, + CancellationToken cancellationToken) + { + var searchText = context.SearchText; + + // Call your API with the search text + var results = await _apiClient.SearchAsync(searchText, cancellationToken); + + return DataSourceBuilder + .WithId(item => item.Id) + .WithLabel(item => item.DisplayName) + .WithInArgumentSingleItemConverter() + .WithData(results) + .Build(); + } +} +``` + +### Example: Orchestrator queue search + +```csharp +QueueName.Widget = new DefaultWidget { Type = ViewModelWidgetType.AutoCompleteForExpression }; +QueueName.SupportsDynamicDataSourceQuery = true; +QueueName.RegisterService( + new OrchestratorQueueDataSource(_tokenProvider)); +``` + +--- + +## Combining Static and Dynamic + +You can set initial static data and also enable dynamic search. The static data appears immediately, and dynamic results replace them as the user types. + +```csharp +Property.Widget = new DefaultWidget { Type = ViewModelWidgetType.AutoCompleteForExpression }; +Property.DataSource = DataSourceBuilder + .WithId(s => s) + .WithLabel(s => s) + .WithInArgumentSingleItemConverter() + .WithData(initialItems) + .Build(); + +// Also enable dynamic search for additional results +Property.SupportsDynamicDataSourceQuery = true; +Property.RegisterService(new MyDynamicSearchBuilder()); +``` + +--- + +## Troubleshooting + +**Autocomplete shows items but selecting one does not set the property value.** +Ensure `WithInArgumentSingleItemConverter()` is called on the `DataSourceBuilder`. Without it, the selected item cannot be converted to an `InArgument` value for the property. + +**Dynamic autocomplete never returns results.** +Verify that `SupportsDynamicDataSourceQuery` is set to `true` on the property. Without this flag, the framework does not call the `IDynamicDataSourceBuilder`. + +**Search results flicker or show stale data.** +The `IDynamicDataSourceBuilder.BuildAsync` method is called on every keystroke (debounced). Ensure your implementation uses the provided `CancellationToken` to cancel in-flight API requests when new input arrives. + +**Selected value clears on workflow reopen.** +The `WithId` selector must return a stable identifier. If the ID is a server-generated GUID that changes between sessions, the framework cannot match the saved value to a dropdown item. In this case, the value is still set on the property (as an expression), but the dropdown may not highlight it. diff --git a/.claude/activity-development-guide/design/widgets/connection.md b/.claude/activity-development-guide/design/widgets/connection.md new file mode 100644 index 000000000..3b981abf8 --- /dev/null +++ b/.claude/activity-development-guide/design/widgets/connection.md @@ -0,0 +1,122 @@ +# Connection Widget + +## When to read this + +Read this document when you need to: +- Add an Integration Service connection picker to an activity +- Configure the `Connection` widget with a connector +- Understand how connections relate to bindings + +--- + +## Overview + +The `Connection` widget provides a picker for UiPath Integration Service connections. When an activity connects to an external service (email, CRM, cloud storage), the Connection widget lets the user select a configured connection from their Orchestrator or Integration Service instance. + +Use with `DesignProperty` (the property stores the connection ID). + +--- + +## Basic Usage + +```csharp +ConnectionId.Widget = new DefaultWidget +{ + Type = ViewModelWidgetType.Connection, + Metadata = new() { { nameof(Connector), GetConnector() } } +}; +``` + +The `Metadata` dictionary must include a `Connector` key whose value is the connector identifier returned by a `GetConnector()` method (typically defined in the ViewModel base class or a shared helper). + +--- + +## Activity-Side Setup + +On the activity class, declare the connection property with the `[ConnectionBinding]` attribute. This tells the binding system that this property holds an Integration Service connection. + +```csharp +[ConnectionBinding] +public InArgument ConnectionId { get; set; } +``` + +--- + +## ViewModel-Side Setup + +In the ViewModel's `InitializeModel` or `InitializeModelAsync`, assign the `Connection` widget: + +```csharp +protected override void InitializeModel() +{ + base.InitializeModel(); + + ConnectionId.Widget = new DefaultWidget + { + Type = ViewModelWidgetType.Connection, + Metadata = new() { { nameof(Connector), GetConnector() } } + }; + ConnectionId.IsPrincipal = true; + ConnectionId.IsRequired = true; +} +``` + +--- + +## Binding Registration + +Connections participate in the bindings system. Register bindings in the registration class so Orchestrator/Assistant can resolve connections at runtime: + +```csharp +protected override void RegisterBindings(IWorkflowDesignApi api, bool enabled) +{ + if (!api.HasFeature(DesignFeatureKeys.PackageBindingsV3)) return; + + // Register connections, event triggers, etc. +} +``` + +For dependent properties that change based on the selected connection (e.g., account ID, folder ID), use `[PropertyBinding]`: + +```csharp +// On the activity class +[PropertyBinding(nameof(ConnectionId))] +public InArgument AccountId { get; set; } + +[PropertyBinding(nameof(ConnectionId), SubKey = "folder")] +public InArgument FolderId { get; set; } +``` + +--- + +## Conditional Compilation + +The Integration Service client may not be available in all build configurations. Use conditional compilation: + +```csharp +#if INTEGRATION_SERVICE + ConnectionId.Widget = new DefaultWidget + { + Type = ViewModelWidgetType.Connection, + Metadata = new() { { nameof(Connector), GetConnector() } } + }; +#endif +``` + +The `INTEGRATION_SERVICE` symbol is defined when the connection client assembly is available. + +--- + +## Troubleshooting + +**Connection picker shows no connections.** +The picker queries Integration Service for connections matching the connector type. If no connections are configured in the Orchestrator tenant, the picker will be empty. Verify that the connector is set up in Integration Service and that the user has access. + +**GetConnector() returns null or empty.** +Ensure the connector identifier is correctly defined. The `GetConnector()` method should return the Integration Service connector key (e.g., `"UiPath.GSuite.Gmail"`, `"UiPath.Salesforce"`). Check your package's connector registration. + +**Connection value is null at runtime.** +If the automation runs outside Orchestrator (e.g., local Studio run), the connection binding may not resolve. Ensure the activity handles the case where `ConnectionId` is null and provides a meaningful error message. + +**Build error: `ViewModelWidgetType.Connection` not found.** +Ensure the project references the correct ViewModel SDK assembly. The `Connection` widget type is defined in the design-time SDK. diff --git a/.claude/activity-development-guide/design/widgets/dropdown.md b/.claude/activity-development-guide/design/widgets/dropdown.md new file mode 100644 index 000000000..26d1036f3 --- /dev/null +++ b/.claude/activity-development-guide/design/widgets/dropdown.md @@ -0,0 +1,120 @@ +# Dropdown Widget + +## When to read this + +Read this document when you need to: +- Display a fixed list of options as a dropdown for a property +- Configure a `DataSource` for a dropdown widget +- Choose between a dropdown (fixed list) and an autocomplete (searchable list with expressions) + +--- + +## Overview + +The `Dropdown` widget renders a static list of options. The user selects one value from the list. It does not support expressions -- use `AutoCompleteForExpression` if the user needs to type expressions or search a large list. See [autocomplete.md](autocomplete.md). + +Use `Dropdown` with `DesignProperty`. + +--- + +## Basic Usage + +A dropdown requires both a widget type and a `DataSource` that provides the list of options. + +```csharp +Property.Widget = new DefaultWidget { Type = "Dropdown" }; +Property.DataSource = DataSourceBuilder + .WithId(s => s) + .WithLabel(s => s) + .Build(); +Property.DataSource.Data = new[] { "Option1", "Option2", "Option3" }; +``` + +### DataSource Configuration + +The `DataSourceBuilder` requires two selectors: + +| Selector | Purpose | +|---|---| +| `WithId(T => string)` | Returns the unique identifier for each item. This is the value stored in the property. | +| `WithLabel(T => string)` | Returns the display text shown in the dropdown. | + +Optional selectors: + +| Selector | Purpose | +|---|---| +| `WithCategory(T => string)` | Groups items under category headers in the dropdown. | +| `WithTooltip(T => string)` | Shows a tooltip when hovering over an item. | +| `WithDescription(T => string)` | Extended description below the item label. | + +--- + +## Examples + +### String options (ID equals label) + +```csharp +Protocol.Widget = new DefaultWidget { Type = "Dropdown" }; +Protocol.DataSource = DataSourceBuilder + .WithId(s => s) + .WithLabel(s => s) + .Build(); +Protocol.DataSource.Data = new[] { "IMAP", "POP3", "Exchange" }; +``` + +### Enum options with display names + +```csharp +Priority.Widget = new DefaultWidget { Type = "Dropdown" }; +Priority.DataSource = DataSourceBuilder + .WithId(p => p.ToString()) + .WithLabel(p => p switch + { + PriorityLevel.Low => "Low Priority", + PriorityLevel.Normal => "Normal Priority", + PriorityLevel.High => "High Priority", + _ => p.ToString() + }) + .Build(); +Priority.DataSource.Data = Enum.GetValues(); +``` + +### Complex object options + +```csharp +Server.Widget = new DefaultWidget { Type = "Dropdown" }; +Server.DataSource = DataSourceBuilder + .WithId(s => s.Id) + .WithLabel(s => s.DisplayName) + .WithCategory(s => s.Region) + .WithTooltip(s => s.Url) + .Build(); +Server.DataSource.Data = availableServers; +``` + +--- + +## Dropdown vs AutoComplete + +| Feature | Dropdown | AutoCompleteForExpression | +|---|---|---| +| Expression support | No | Yes | +| Search/filter | No | Yes (typed search) | +| Dynamic data loading | No | Yes (via `IDynamicDataSourceBuilder`) | +| Property type | `DesignProperty` | `DesignInArgument` | +| Best for | Small fixed lists (< 20 items) | Large or dynamic lists | + +For data sources and dynamic autocomplete patterns, see [autocomplete.md](autocomplete.md) and [../datasources.md](../datasources.md). + +--- + +## Troubleshooting + +**Dropdown appears empty.** +Verify that `DataSource.Data` is set after building the data source. The `Build()` call creates the data source structure; `Data` must be assigned separately with the actual items. + +**Selected value does not persist.** +Ensure the `WithId` selector returns a stable, unique string for each item. If the ID changes between sessions (e.g., uses a timestamp or random value), the saved value will not match any item on reload. + +**Dropdown shows IDs instead of display names.** +Check that `WithLabel` returns the human-readable name. If `WithLabel` and `WithId` return the same value, the dropdown shows the ID string, which may be a technical identifier. diff --git a/.claude/activity-development-guide/design/widgets/filter-builder.md b/.claude/activity-development-guide/design/widgets/filter-builder.md new file mode 100644 index 000000000..5eba30bf4 --- /dev/null +++ b/.claude/activity-development-guide/design/widgets/filter-builder.md @@ -0,0 +1,480 @@ +# FilterBuilder Widget + +> **When to read this:** You are building an activity that needs a visual filter/query builder UI where users construct conditions with criteria dropdowns, operator dropdowns, and typed value inputs connected by logical operators (AND/OR). Examples: email filtering, database record filtering, queue item filtering. + +**Cross-references:** +- [ViewModel fundamentals](../design/viewmodel.md) — DesignProperty, Widget assignment +- [DataSource patterns](../design/datasources.md) — dropdown data sources (used within filter criteria) +- [Activity code and CacheMetadata](../runtime/activity-code.md) — `metadata.Bind()` and `metadata.AddArgument()` +- [Advanced patterns](./patterns.md) — localized enums, rule-driven properties + +--- + +## Architecture Overview + +The FilterBuilder requires several components working together: + +| Component | Purpose | +|-----------|---------| +| **Criteria enum** | Defines the filterable fields (e.g., Sender, Subject, DateReceived) | +| **Operator enum** | Defines comparison operators (e.g., Contains, Equals, NewerThan) | +| **Logical operator enum** | AND/OR between conditions | +| **Filter collection class** | Runtime model holding the filter conditions with `InArgument` values | +| **Filter argument class** | Individual condition with criteria, operator, and typed value | +| **FilterBuilder class** | ViewModel helper that configures `GenericFilterWidgetBuilder` and provides a converter | + +The data flow is: + +``` +User configures conditions in UI + | + v +GenericFilterWidgetBuilder captures DesignFilterBase + | + v +Converter function transforms DesignFilterBase -> MyFilterCollection + | + v +MyFilterCollection serialized into .xaml workflow file + | + v (at runtime) +Activity.CacheMetadata() registers all InArgument values + | + v +Activity.Execute() resolves filter values via context +``` + +--- + +## Step 1: Define the Enums + +Use `[LocalizedDisplayName]` for Studio display names. + +```csharp +// Filters/MyCriteria.cs +public enum MyCriteria +{ + [LocalizedDisplayName(nameof(Resources.Filter_Name))] + Name, + [LocalizedDisplayName(nameof(Resources.Filter_Date))] + Date, + [LocalizedDisplayName(nameof(Resources.Filter_IsActive))] + IsActive, +} + +// Filters/MyOperator.cs +public enum MyOperator +{ + [LocalizedDisplayName(nameof(Resources.FilterOp_Contains))] + Contains, + [LocalizedDisplayName(nameof(Resources.FilterOp_Equals))] + Equals, + [LocalizedDisplayName(nameof(Resources.FilterOp_NotEquals))] + NotEquals, + [LocalizedDisplayName(nameof(Resources.FilterOp_NewerThan))] + NewerThan, + [LocalizedDisplayName(nameof(Resources.FilterOp_OlderThan))] + OlderThan, + [LocalizedDisplayName(nameof(Resources.FilterOp_IsTrue))] + IsTrue, + [LocalizedDisplayName(nameof(Resources.FilterOp_IsFalse))] + IsFalse, +} + +// Filters/MyLogicalOperator.cs +public enum MyLogicalOperator +{ + [LocalizedDisplayName(nameof(Resources.Filter_And))] + And, + [LocalizedDisplayName(nameof(Resources.Filter_Or))] + Or +} +``` + +--- + +## Step 2: Define the Runtime Filter Model + +Each condition holds `InArgument` values so users can use expressions (e.g., `DateTime.Now.AddDays(-7)`). + +**Critical: Why filter values must be `InArgument`** + +`InArgument` values are **not automatically tracked** by the workflow engine. You must explicitly register them in `CacheMetadata` with **both** `metadata.Bind()` **and** `metadata.AddArgument()`. Without this, the workflow engine cannot evaluate the expressions and the values will be `null` at runtime. + +```csharp +// Filters/MyFilterArgument.cs +public class MyFilterArgument +{ + public MyCriteria Criteria { get; set; } + public MyOperator Operator { get; set; } + public InArgument Value { get; set; } + public InArgument DateValue { get; set; } + public InArgument BoolValue { get; set; } + + internal ResolvedValue Resolve(ActivityContext context) => new() + { + Criteria = Criteria, + Operator = Operator, + StringValue = Value?.Get(context), + DateValue = DateValue?.Get(context), + BoolValue = BoolValue?.Get(context), + }; + + internal class ResolvedValue + { + public MyCriteria Criteria { get; set; } + public MyOperator Operator { get; set; } + public string StringValue { get; set; } + public DateTime? DateValue { get; set; } + public bool? BoolValue { get; set; } + } +} + +// Filters/MyFilterCollection.cs +public class MyFilterCollection +{ + public MyLogicalOperator LogicalOperator { get; set; } + public List Conditions { get; } = new(); + + /// + /// Register all InArgument values in the activity metadata so the + /// workflow engine tracks them. Each argument must have a UNIQUE name + /// and must be BOUND to its RuntimeArgument — otherwise the workflow + /// engine cannot evaluate expressions and values will be null at runtime. + /// + public void RegisterArguments(CodeActivityMetadata metadata) + { + var args = Conditions + .SelectMany(c => new Argument[] { c.Value, c.DateValue, c.BoolValue }) + .Where(a => a != null) + .ToList(); + + for (int i = 0; i < args.Count; i++) + { + var arg = args[i]; + // Each argument MUST have a unique name + var runtimeArg = new RuntimeArgument( + $"FilterArg{i}", arg.ArgumentType, ArgumentDirection.In); + // CRITICAL: Bind links the InArgument to its RuntimeArgument. + // Without Bind(), the workflow engine won't evaluate the expression + // and .Get(context) will return null/default at runtime. + metadata.Bind(arg, runtimeArg); + metadata.AddArgument(runtimeArg); + } + } +} +``` + +**Common mistake**: calling only `metadata.AddArgument()` without `metadata.Bind()`. The `AddArgument` call declares the slot; the `Bind` call links your `InArgument` instance to that slot. Both are required. Also, each `RuntimeArgument` must have a **unique name** -- using the same name for all arguments silently overwrites them. + +--- + +## Step 3: Create the FilterBuilder Helper + +This configures the `GenericFilterWidgetBuilder` with criteria, operators, value types, and a converter function. + +```csharp +// ViewModels/Filters/MyFilterBuilder.cs +using System.Activities.ViewModels.Interfaces; + +internal class MyFilterBuilder +{ + private readonly GenericFilterWidgetBuilder _builder; + private readonly DesignProperty _filter; + + public MyFilterBuilder(DesignProperty filter) + { + _filter = filter; + + _builder = new GenericFilterWidgetBuilder() + // 1. Configure logical operator (AND/OR toggle) + .AddLogicalOperator( + _filter.Value?.LogicalOperator ?? MyLogicalOperator.And, + s => ActivitiesEnumExtensions.GetLocalizedDisplayName(s), // display label + s => s.ToString(), // ID + Enum.GetValues().ToList()) + // 2. Configure criteria dropdown data source + .SetCriteriaDataSourceBuilder( + f => ActivitiesEnumExtensions.GetLocalizedDisplayName(f), + f => f.ToString()); + + // 3. Configure each criteria with its operators and value type + ConfigureAllCriteria(); + + // 4. Set the converter that transforms design-time model -> runtime model + _builder.AddConverter(ConvertToFilterCollection); + + // 5. Restore any existing conditions (when reopening the designer) + AddExistingConditions(); + } + + public IWidget Build() => _builder.Build(); + + private void ConfigureAllCriteria() + { + // Text criteria -- string value + _builder.AddCriteria(MyCriteria.Name) + .WithOperators( + s => ActivitiesEnumExtensions.GetLocalizedDisplayName(s), + s => s.ToString(), + new List { MyOperator.Contains, MyOperator.Equals, MyOperator.NotEquals }) + .AndArgument(); + + // Date criteria -- DateTime value + _builder.AddCriteria(MyCriteria.Date) + .WithOperators( + s => ActivitiesEnumExtensions.GetLocalizedDisplayName(s), + s => s.ToString(), + new List { MyOperator.NewerThan, MyOperator.OlderThan }) + .AndArgument(); + + // Boolean criteria -- some operators need no value, others need bool + _builder.AddCriteria(MyCriteria.IsActive) + .WithOperators( + s => ActivitiesEnumExtensions.GetLocalizedDisplayName(s), + s => s.ToString(), + new List { MyOperator.IsTrue, MyOperator.IsFalse, MyOperator.Equals }) + .ConfigureOperator(MyOperator.IsTrue).WithNoValue() + .AndOperator(MyOperator.IsFalse).WithNoValue() + .AndOperator(MyOperator.Equals).WithArgument(); + } + + private void AddExistingConditions() + { + if (_filter.Value == null) return; + foreach (var c in _filter.Value.Conditions) + { + if (c.Criteria == MyCriteria.Date) + _builder.AddCondition(c.Criteria, c.Operator, c.DateValue); + else if (c.Criteria == MyCriteria.IsActive) + _builder.AddCondition(c.Criteria, c.Operator, c.BoolValue); + else if (c.Criteria == MyCriteria.Category) + { + // IMPORTANT: Dropdown criteria -- AddCondition expects plain string + // to match against the dropdown items list. The converter wrapped + // the plain value in InArgument, so unwrap it here. + var plainValue = (c.Value?.Expression as Literal)?.Value; + _builder.AddCondition(c.Criteria, c.Operator, plainValue); + } + else + _builder.AddCondition(c.Criteria, c.Operator, c.Value); + } + } + + private object ConvertToFilterCollection(DesignFilterBase df) + { + var result = new MyFilterCollection + { + LogicalOperator = (MyLogicalOperator)df.LogicalOperator, + }; + foreach (var condition in df.Collection.Conditions) + { + var criteria = (MyCriteria)condition.Criteria; + var arg = new MyFilterArgument + { + Criteria = criteria, + Operator = (MyOperator)condition.Operator, + }; + if (criteria == MyCriteria.Date) + arg.DateValue = (InArgument)condition.Value; + else if (criteria == MyCriteria.IsActive) + arg.BoolValue = (InArgument)condition.Value; + else if (criteria == MyCriteria.Category) + { + // WithDropDown gives plain string -- wrap for runtime storage + if (condition.Value is string plainStr) + arg.Value = new InArgument(plainStr); + else + arg.Value = (InArgument)condition.Value; + } + else + arg.Value = (InArgument)condition.Value; + + result.Conditions.Add(arg); + } + return result; + } +} +``` + +--- + +## Step 4: Use in the ViewModel + +```csharp +public DesignProperty Filter { get; set; } = new(); + +protected override void InitializeModel() +{ + base.InitializeModel(); + Filter.IsPrincipal = true; + Filter.OrderIndex = PropertyOrderIndex++; + Filter.Widget = new MyFilterBuilder(Filter).Build(); +} +``` + +--- + +## Step 5: Use in the Activity + +The filter property is a **direct type** (not `InArgument`). The collection itself contains `InArgument` values that must be registered in `CacheMetadata`. + +```csharp +// IMPORTANT: The filter property is a direct type, NOT InArgument. +// The collection is serialized as-is into the .xaml workflow. The InArgument values +// INSIDE the collection are what need registration via CacheMetadata. +[DefaultValue(null)] +public MyFilterCollection Filter { get; set; } + +protected override void CacheMetadata(CodeActivityMetadata metadata) +{ + base.CacheMetadata(metadata); + // CRITICAL: Without this call, filter InArgument expressions won't be + // evaluated at runtime -- .Get(context) will return null/default. + Filter?.RegisterArguments(metadata); +} + +protected override void Execute(CodeActivityContext context) +{ + var conditions = Filter?.Conditions?.Select(c => c.Resolve(context)) + ?? Enumerable.Empty(); + + foreach (var c in conditions) + { + // c.Criteria, c.Operator, c.StringValue / c.DateValue / c.BoolValue + } +} +``` + +--- + +## How Filter Persistence Works End-to-End + +``` +Design-Time (Studio) Runtime (Robot) +--------------------- ------------------ +1. User configures filter 5. Activity.CacheMetadata() called + conditions in widget UI -> Filter.RegisterArguments(metadata) + | -> metadata.Bind(inArg, runtimeArg) + v -> metadata.AddArgument(runtimeArg) +2. GenericFilterWidgetBuilder | + captures DesignFilterBase v + | 6. Activity.Execute() called + v -> condition.Value.Get(context) +3. Converter function called returns evaluated expression + -> casts condition.Value | + to InArgument v + -> builds MyFilterCollection 7. Business logic processes + -> sets on DesignProperty resolved filter values + | + v +4. MyFilterCollection serialized + into .xaml workflow file + (InArgument values preserved + as expression trees) +``` + +--- + +## Debugging Null Filter Values (5-Point Checklist) + +If filter values are `null` at runtime, check these causes in order: + +1. **`RegisterArguments()` not called** in `CacheMetadata` -- most common cause. +2. **`metadata.Bind()` missing** -- `AddArgument` alone is not enough. +3. **Duplicate argument names** -- each `RuntimeArgument` must have a unique name. +4. **Converter not casting to `InArgument`** -- `condition.Value` must be cast to the correct `InArgument` type in the converter function. +5. **Dropdown appears empty on reopen** -- `AddExistingConditions` passes `InArgument` to `AddCondition` for a dropdown criterion. Unwrap with `(c.Value?.Expression as Literal)?.Value` first. + +--- + +## Understanding condition.Value Types in the Converter + +The type of `condition.Value` in the converter function depends on how the operator was configured. This is a common source of bugs -- if you always cast to `InArgument`, dropdown values will fail with an `InvalidCastException` or silently return `null`. + +| Widget Configuration | `condition.Value` Type | Converter Pattern | +|---|---|---| +| `AndArgument()` / `WithArgument()` | `InArgument` | `(InArgument)condition.Value` | +| `WithDropDown(...)` | plain `T` | `(T)condition.Value` or `condition.Value as T` | +| `WithDropDown>(...)` | `InArgument` | `(InArgument)condition.Value` | +| `WithMultiSelectDropDown(...)` | `T[]` / `IEnumerable` | Cast to collection or use `FilterHelper.GetCollectionOf()` | +| `WithLiteral(...)` | plain `T` | `(T)condition.Value` | +| `WithNoValue()` | `null` | Do not access `condition.Value` | + +--- + +## Mixing Dropdowns and Arguments + +When mixing dropdowns and arguments for the same criteria, check for the plain type first, then fall back to `InArgument`: + +```csharp +// Handle both plain dropdown values and InArgument expression values +if (condition.Value is string plainStr) + arg.Value = new InArgument(plainStr); // Wrap plain value +else if (condition.Value is InArgument strArg) + arg.Value = strArg; // Already InArgument + +// Same pattern for other types +if (condition.Value is DateTime plainDate) + arg.DateValue = new InArgument(plainDate); +else if (condition.Value is InArgument dateArg) + arg.DateValue = dateArg; +``` + +Alternatively, if a dropdown criterion should always produce an `InArgument`, use `WithDropDown>()` instead of `WithDropDown()` -- then `condition.Value` is already an `InArgument` and no wrapping is needed. + +--- + +## Round-Trip: Dropdown Values Must Be Unwrapped in AddExistingConditions + +Dropdown criteria have a round-trip mismatch: + +1. **Design-time**: `WithDropDown()` gives a **plain value** (e.g., `"Error"`) +2. **Converter**: wraps it in `new InArgument("Error")` for runtime storage +3. **Restore (`AddExistingConditions`)**: the saved model now has `InArgument`, but `AddCondition` for a dropdown expects the **plain value** to match against the dropdown items list + +If you pass `InArgument` to `AddCondition` for a dropdown criterion, the dropdown appears empty because the object does not match any item in the list. + +**Fix**: unwrap the literal value before passing to `AddCondition`: + +```csharp +// Dropdown criterion -- extract plain value for AddCondition to match +var plainValue = (c.Value?.Expression as Literal)?.Value; +_builder.AddCondition(c.Criteria, c.Operator, plainValue); +``` + +**Alternative (avoid the problem entirely)**: Use `WithDropDown>()` instead of `WithDropDown()`. Then the dropdown items are `InArgument` objects, `condition.Value` is already `InArgument` in the converter, and `AddCondition` receives `InArgument` -- no wrapping or unwrapping needed. This is the pattern used by SharePoint's non-trigger filters. + +--- + +## Advanced: Dropdown and Multi-Select Values + +For criteria where the value should be a dropdown: + +```csharp +_builder.AddCriteria(MyCriteria.Category) + .WithOperators(getLabel, getId, operators) + .ConfigureOperator(MyOperator.Equals) + .WithDropDown( + getLabel: c => c, // display name + getId: c => c, // value + data: new List { "TypeA", "TypeB", "TypeC" }, + comparer: StringComparer.OrdinalIgnoreCase) + .Build(); + +// Multi-select dropdown +_builder.AddCriteria(MyCriteria.Labels) + .WithOperators(getLabel, getId, operators) + .ConfigureOperator(MyOperator.Contains) + .WithMultiSelectDropDown( + getLabel: l => l, + getId: l => l, + data: availableLabels) + .Build(); +``` + +--- + +## Troubleshooting + + diff --git a/.claude/activity-development-guide/design/widgets/index.md b/.claude/activity-development-guide/design/widgets/index.md new file mode 100644 index 000000000..cc9070edb --- /dev/null +++ b/.claude/activity-development-guide/design/widgets/index.md @@ -0,0 +1,223 @@ +# Widget Reference + +## When to read this + +Read this document when you need to: +- Choose which widget to use for an activity property +- Configure a widget with metadata options +- Look up the correct `ViewModelWidgetType` constant for a property type +- Understand how widgets map to property types (`DesignProperty` vs `DesignInArgument`) + +For the FilterBuilder widget (complex filter conditions UI), see [filter-builder.md](filter-builder.md). + +--- + +## What is a Widget? + +A **widget** is the UI control that renders and edits an activity property in the Studio designer. The framework selects a default widget based on the property type (e.g., a text box for strings, an expression editor for `InArgument`), but you can override this to use a more specific control. + +For example, a `bool` property can render as a checkbox (default) or a toggle switch (`ViewModelWidgetType.Toggle`). A `string` property can be a text box (default), a dropdown, an autocomplete with search, or a rich text editor. + +--- + +## Setting a Widget + +Every `DesignProperty` or `DesignInArgument` has a `.Widget` property. Assign a `DefaultWidget` (or a specialized widget class) to override the default control. + +```csharp +// Basic widget assignment +property.Widget = new DefaultWidget { Type = ViewModelWidgetType.WidgetName }; + +// Widget with metadata +property.Widget = new DefaultWidget +{ + Type = ViewModelWidgetType.WidgetName, + Metadata = new Dictionary + { + { "key", "value" } + } +}; +``` + +Some widgets use specialized classes instead of `DefaultWidget`: +- `TypePickerWidget` for type pickers +- `TextBlockWidget` for read-only text display +- `SolutionResourcesWidget` for Solutions resource pickers + +--- + +## Quick Reference Table + +| Widget Type | Property Type | Use Case | Details | +|---|---|---|---| +| *(default)* | `DesignInArgument` | Expression editor | Simple (below) | +| `TextComposer` | `DesignInArgument` | Rich text / multiline input | [text-input.md](text-input.md) | +| `RichTextComposer` | `DesignInArgument` | HTML / rich text editor | [text-input.md](text-input.md) | +| `PromptComposer` | `DesignInArgument` | AI prompt with @ variables | [text-input.md](text-input.md) | +| `Number` | `DesignInArgument` | Number with expression support | Simple (below) | +| `PlainNumber` | `DesignProperty` | Constrained number (no expressions) | [plain-number.md](plain-number.md) | +| `Toggle` | `DesignProperty` | Boolean switch | Simple (below) | +| `NullableBoolean` | `DesignProperty` | True / False / Null | Simple (below) | +| `Date` | `DesignInArgument` | Date picker | Simple (below) | +| `Time` | `DesignInArgument` | Time picker | Simple (below) | +| `DateTime` | `DesignInArgument` | Date + time picker | Simple (below) | +| `TimeSpan` | `DesignInArgument` | Duration picker | Simple (below) | +| `Dropdown` | `DesignProperty` | Fixed options dropdown | [dropdown.md](dropdown.md) | +| `AutoCompleteForExpression` | `DesignInArgument` | Searchable dropdown + expression | [autocomplete.md](autocomplete.md) | +| `Collection` | `DesignProperty>` | Collection editor | Simple (below) | +| `Dictionary` | `DesignProperty>` | Key-value editor | Simple (below) | +| `RawStringArray` | `DesignProperty` | String array editor | Simple (below) | +| `MultiSelect` | `DesignProperty` | Multi-select dropdown (Flags enums) | Simple (below) | +| `AddActivityWidget` | `DesignProperty` | Activity picker for containers | Simple (below) | +| `Variable` | `DesignInArgument` | Variable picker | Simple (below) | +| `TypePicker` / `TypePickerWidget` | `DesignProperty` | .NET type selector | [type-picker.md](type-picker.md) | +| `Connection` | `DesignProperty` | Integration Service connection | [connection.md](connection.md) | +| `ActionButton` | `DesignProperty` | Clickable button | [action-button.md](action-button.md) | +| `TextBlockWidget` | `DesignProperty` | Read-only text display | [text-block.md](text-block.md) | +| `Text` | `DesignProperty` | Plain text with metadata (e.g., `"multiline": "true"`) | Simple (below) | +| `Input` | `DesignInArgument` | Basic input widget | Simple (below) | +| `Container` | N/A | Container / group widget | Simple (below) | +| `AppsExpression` | `DesignInArgument` | UiPath Apps expression editor | Simple (below) | +| `FilterWidgetBuilder` | `DesignProperty` | Complex filter configuration | [filter-builder.md](filter-builder.md) | +| `outputmapping` | `DesignProperty` | Output field mapping UI | [output-mapping.md](output-mapping.md) | +| `SolutionResourcesWidget` | `DesignProperty` | Solutions resource picker | [solution-resources.md](solution-resources.md) | + +--- + +## Simple Widgets + +These widgets require minimal configuration -- just set the `Type` and optionally some metadata. + +### Toggle + +Boolean on/off switch. Use for `DesignProperty`. + +```csharp +Property.Widget = new DefaultWidget { Type = ViewModelWidgetType.Toggle }; +``` + +### NullableBoolean + +Three-state control: True, False, or Null. Use for `DesignProperty`. + +```csharp +Property.Widget = new DefaultWidget { Type = ViewModelWidgetType.NullableBoolean }; +``` + +### Number + +Number input with expression support. Use for `DesignInArgument` or `DesignInArgument`. For constrained numbers without expression support, see [plain-number.md](plain-number.md). + +```csharp +Property.Widget = new DefaultWidget { Type = ViewModelWidgetType.Number }; +``` + +### Date, Time, DateTime, TimeSpan + +Date and time picker widgets. Each variant exposes a different picker UI. + +```csharp +// Date only +Property.Widget = new DefaultWidget { Type = ViewModelWidgetType.Date }; + +// Time only +Property.Widget = new DefaultWidget { Type = ViewModelWidgetType.Time }; + +// Date + Time +Property.Widget = new DefaultWidget { Type = ViewModelWidgetType.DateTime }; + +// TimeSpan (duration) +Property.Widget = new DefaultWidget { Type = ViewModelWidgetType.TimeSpan }; + +// DateTime with invariant offset (UTC-safe) +Property.Widget = new DefaultWidget +{ + Type = ViewModelWidgetType.DateTime, + Metadata = new() + { + { WidgetMetadataConstants.DateTime.OffsetSetting, + WidgetMetadataConstants.DateTime.OffsetInvariantSetting } + } +}; +``` + +### Variable + +Variable picker. Lets the user select from existing workflow variables. + +```csharp +Property.Widget = new DefaultWidget { Type = ViewModelWidgetType.Variable }; +``` + +### Collection, Dictionary, RawStringArray + +Editors for collection-typed properties. + +```csharp +// Collection editor +Property.Widget = new DefaultWidget { Type = ViewModelWidgetType.Collection }; + +// Dictionary editor (key-value pairs) +Property.Widget = new DefaultWidget { Type = ViewModelWidgetType.Dictionary }; + +// String array editor +Property.Widget = new DefaultWidget { Type = ViewModelWidgetType.RawStringArray }; +``` + +### Container + +Container/group widget. Used to visually group related properties. + +```csharp +Property.Widget = new DefaultWidget { Type = ViewModelWidgetType.Container }; +``` + +### MultiSelect + +Multi-select dropdown. Use for Flags enums or multi-select data sources. + +```csharp +Property.Widget = new DefaultWidget { Type = ViewModelWidgetType.MultiSelect }; +``` + +### AddActivityWidget + +Activity picker for container activities. Lets users pick an activity type to add to a container body. + +```csharp +Property.Widget = new DefaultWidget { Type = ViewModelWidgetType.AddActivityWidget }; +// Set the container property name the activity will be added to: +Property.Value = nameof(ActivityClass.Body); +``` + +### Text + +Plain text widget with optional metadata. For read-only display, prefer `TextBlockWidget` (see [text-block.md](text-block.md)). + +```csharp +// Plain text +Property.Widget = new DefaultWidget { Type = ViewModelWidgetType.Text }; + +// Multiline text +Property.Widget = new DefaultWidget +{ + Type = ViewModelWidgetType.Text, + Metadata = new() { { "multiline", "true" } } +}; +``` + +### Input + +Basic input widget. This is the standard expression-capable input. + +```csharp +Property.Widget = new DefaultWidget { Type = ViewModelWidgetType.Input }; +``` + +### AppsExpression + +Expression editor for UiPath Apps context. Used when the activity runs inside a UiPath App. + +```csharp +Property.Widget = new DefaultWidget { Type = ViewModelWidgetType.AppsExpression }; +``` diff --git a/.claude/activity-development-guide/design/widgets/output-mapping.md b/.claude/activity-development-guide/design/widgets/output-mapping.md new file mode 100644 index 000000000..57b5f4f71 --- /dev/null +++ b/.claude/activity-development-guide/design/widgets/output-mapping.md @@ -0,0 +1,86 @@ +# Output Mapping Widget + +## When to read this + +Read this document when you need to: +- Add an output field mapping UI where users configure which output fields to save and how to name the variables +- Display a "Configure fields" button that opens a mapping editor + +--- + +## Overview + +The `outputmapping` widget provides a UI for mapping output fields to workflow variables. It renders a configuration button that opens an editor where users can see available output fields and assign variable names to each field. This is commonly used for activities that return dynamic or configurable sets of output values (e.g., parsed document fields, API response fields). + +Use with `DesignProperty`. + +--- + +## Basic Usage + +```csharp +Property.Widget = new DefaultWidget +{ + Type = "outputmapping", + Metadata = new Dictionary + { + ["configureButton"] = "Configure fields", + ["title"] = "Configure data", + ["description"] = "Fields below will be dynamically processed", + ["fieldLabel"] = "Field name", + ["saveAsValueLabel"] = "Save as Variable", + ["fieldPlaceholder"] = "Value name for {{field}}" + } +}; +``` + +**Note:** The widget type string is `"outputmapping"` (lowercase), not a `ViewModelWidgetType` constant. + +--- + +## Metadata Keys + +| Key | Description | +|---|---| +| `configureButton` | Label text for the button that opens the mapping editor. | +| `title` | Title of the mapping editor dialog/panel. | +| `description` | Descriptive text shown at the top of the editor. | +| `fieldLabel` | Column header for the field names in the mapping table. | +| `saveAsValueLabel` | Column header for the variable name input in the mapping table. | +| `fieldPlaceholder` | Placeholder text in the variable name input. Use `{{field}}` as a token that gets replaced with the actual field name. | + +All metadata values are strings. They control the labels and text in the mapping UI. + +--- + +## Example: Document extraction output + +```csharp +OutputFields.Widget = new DefaultWidget +{ + Type = "outputmapping", + Metadata = new Dictionary + { + ["configureButton"] = "Configure extracted fields", + ["title"] = "Map Extracted Fields", + ["description"] = "Select which fields to extract and specify variable names", + ["fieldLabel"] = "Document field", + ["saveAsValueLabel"] = "Save to variable", + ["fieldPlaceholder"] = "Variable for {{field}}" + } +}; +OutputFields.IsPrincipal = true; +``` + +--- + +## Troubleshooting + +**Configure button does not appear.** +Verify the widget type string is exactly `"outputmapping"` (lowercase). Using `"OutputMapping"` or other casing may not match. + +**`{{field}}` placeholder is not replaced.** +The `{{field}}` token in `fieldPlaceholder` is replaced by the framework at render time. If it shows as literal text, ensure you are using double curly braces: `{{field}}`, not `{field}`. + +**Mapping data is not persisted.** +Ensure the property is properly bound in the ViewModel and that the activity's corresponding property can serialize the mapping data. The output mapping widget stores its configuration in the property value. diff --git a/.claude/activity-development-guide/design/widgets/plain-number.md b/.claude/activity-development-guide/design/widgets/plain-number.md new file mode 100644 index 000000000..4607f4b7e --- /dev/null +++ b/.claude/activity-development-guide/design/widgets/plain-number.md @@ -0,0 +1,115 @@ +# PlainNumber Widget + +## When to read this + +Read this document when you need to: +- Display a number input with min/max/step constraints +- Use a number input that does NOT support expressions (plain value only) +- Understand when to use `PlainNumber` vs `Number` + +--- + +## Overview + +The `PlainNumber` widget provides a constrained numeric input. Unlike the `Number` widget, `PlainNumber` does not allow expressions -- the user enters a literal numeric value. It supports `Min`, `Max`, and `Step` metadata to constrain the valid range and increment. + +Use `PlainNumber` with `DesignProperty` or `DesignProperty`. + +--- + +## Basic Usage + +```csharp +Property.Widget = new DefaultWidget +{ + Type = ViewModelWidgetType.PlainNumber, + Metadata = new Dictionary + { + [PlainNumber.Min] = "-100", + [PlainNumber.Max] = "100", + [PlainNumber.Step] = "1" + } +}; +``` + +### Metadata Keys + +| Key | Type | Description | +|---|---|---| +| `PlainNumber.Min` | string (numeric) | Minimum allowed value. User cannot enter below this. | +| `PlainNumber.Max` | string (numeric) | Maximum allowed value. User cannot enter above this. | +| `PlainNumber.Step` | string (numeric) | Increment/decrement step when using spinner controls. | + +All metadata values are strings. The framework parses them as numbers internally. + +--- + +## When to Use PlainNumber vs Number + +| Scenario | Widget | Property Type | +|---|---|---| +| User needs to enter expressions like `variable + 1` | `Number` | `DesignInArgument` | +| User enters a fixed numeric value, optionally constrained | `PlainNumber` | `DesignProperty` | +| Configuration value with known bounds (e.g., retry count 0-10) | `PlainNumber` | `DesignProperty` | +| Timeout or delay that might use a variable | `Number` | `DesignInArgument` | + +Use `PlainNumber` when the value is a fixed configuration parameter with known bounds. Use `Number` when the value might come from a variable or expression. + +--- + +## Examples + +### Retry count (0 to 5, step 1) + +```csharp +RetryCount.Widget = new DefaultWidget +{ + Type = ViewModelWidgetType.PlainNumber, + Metadata = new Dictionary + { + [PlainNumber.Min] = "0", + [PlainNumber.Max] = "5", + [PlainNumber.Step] = "1" + } +}; +``` + +### Confidence threshold (0.0 to 1.0, step 0.05) + +```csharp +Confidence.Widget = new DefaultWidget +{ + Type = ViewModelWidgetType.PlainNumber, + Metadata = new Dictionary + { + [PlainNumber.Min] = "0", + [PlainNumber.Max] = "1", + [PlainNumber.Step] = "0.05" + } +}; +``` + +### No constraints + +All metadata keys are optional. Without constraints, `PlainNumber` is an unrestricted numeric input that does not accept expressions. + +```csharp +Property.Widget = new DefaultWidget { Type = ViewModelWidgetType.PlainNumber }; +``` + +--- + +## Troubleshooting + +**User can still type values outside Min/Max.** +The Min/Max constraints are enforced by the UI spinner and validation, but some Studio versions may allow typing values outside the range. Add a validator on the property if strict enforcement is required: + +```csharp +Property.Validators.Add(new RangeValidator(min, max, "Value must be between {0} and {1}")); +``` + +**Step value has no visible effect.** +The `Step` metadata controls the increment when the user clicks the up/down spinner arrows. If the property type is `int` but `Step` is `"0.5"`, the fractional part is discarded. Ensure the step value matches the property type's precision. + +**PlainNumber shows an expression editor.** +Verify the property is `DesignProperty`, not `DesignInArgument`. The `PlainNumber` widget is designed for direct-value properties. diff --git a/.claude/activity-development-guide/design/widgets/solution-resources.md b/.claude/activity-development-guide/design/widgets/solution-resources.md new file mode 100644 index 000000000..e48d1295e --- /dev/null +++ b/.claude/activity-development-guide/design/widgets/solution-resources.md @@ -0,0 +1,153 @@ +# SolutionResourcesWidget + +## When to read this + +Read this document when you need to: +- Add a Solutions resource picker (assets, queues, etc.) to an activity +- Configure resource types and expected properties for the picker +- Understand how the widget integrates with the Solutions context + +For general Solutions integration patterns, see [../solutions.md](../solutions.md). + +--- + +## Overview + +The `SolutionResourcesWidget` provides a picker for resources managed by UiPath Solutions. When an automation runs inside a Solution, Orchestrator resources (assets, queues, buckets, etc.) are managed at the Solution level rather than configured per-activity. This widget replaces manual resource name inputs with a structured picker that shows available Solution resources. + +`SolutionResourcesWidget` is a specialized class (not `DefaultWidget`). Use with `DesignProperty`. + +--- + +## Basic Usage + +```csharp +Property.Widget = new SolutionResourcesWidget +{ + ResourceType = SolutionsResourceKind.Asset, + ExpectedProperties = new List { SolutionsPropertyContracts.ResourceName }, + ResourceSubTypes = resourceSubtypes, +}; +``` + +--- + +## Configuration Properties + +| Property | Type | Description | +|---|---|---| +| `ResourceType` | `SolutionsResourceKind` | The kind of Orchestrator resource (e.g., `Asset`, `Queue`, `Bucket`). | +| `ExpectedProperties` | `List` | Property contracts that the selected resource must provide (e.g., `ResourceName`). | +| `ResourceSubTypes` | varies | Optional sub-type filter to narrow which resources are shown. | + +--- + +## Integration with Solutions Context + +The `SolutionResourcesWidget` should only be used when the activity is running inside a Solution. Check the `IUserDesignContext` service to determine this: + +```csharp +protected override async ValueTask InitializeModelAsync() +{ + await base.InitializeModelAsync(); + + var userDesignContext = Services.GetService(); + bool isInSolution = !string.IsNullOrEmpty(userDesignContext?.SolutionId); + + if (isInSolution) + { + await InitializeSolutionsScopePropertiesAsync(); + } + else + { + await InitializeProjectScopePropertiesAsync(); + } +} +``` + +--- + +## Pattern: BaseSolutionResourceViewModel + +For activities that work with Orchestrator resources (assets, queues, etc.), use the `BaseSolutionResourceViewModel` pattern. This base class provides two initialization paths: + +```csharp +public class MyAssetViewModel : BaseSolutionResourceViewModel +{ + protected override async ValueTask InitializeProjectScopePropertiesAsync() + { + // Configure for standalone project: + // - Show FolderPath property + // - Register Orchestrator-based data sources + AssetName.SupportsDynamicDataSourceQuery = true; + AssetName.RegisterService( + new OrchestratorAssetDataSource(_tokenProvider)); + } + + protected override async ValueTask InitializeSolutionsScopePropertiesAsync() + { + // Configure for Solutions: + // - Hide FolderPath (managed by Solution) + // - Use SolutionResourcesWidget instead + FolderPath.IsVisible = false; + AssetName.Widget = new SolutionResourcesWidget + { + ResourceType = SolutionsResourceKind.Asset + }; + } +} +``` + +### Key points + +- In **standalone project** mode: show manual configuration (FolderPath, dynamic data source for searching). +- In **Solutions** mode: hide FolderPath (the Solution manages it) and use `SolutionResourcesWidget` for the resource picker. +- Always provide both code paths. The activity must work in both contexts. + +--- + +## Examples + +### Asset picker + +```csharp +AssetName.Widget = new SolutionResourcesWidget +{ + ResourceType = SolutionsResourceKind.Asset, + ExpectedProperties = new List { SolutionsPropertyContracts.ResourceName } +}; +``` + +### Queue picker + +```csharp +QueueName.Widget = new SolutionResourcesWidget +{ + ResourceType = SolutionsResourceKind.Queue, + ExpectedProperties = new List { SolutionsPropertyContracts.ResourceName } +}; +``` + +--- + +## Troubleshooting + +**Widget shows no resources.** +Verify that the activity is running inside a Solution context (`IUserDesignContext.SolutionId` is not empty). If the user opens the activity in a standalone project, the `SolutionResourcesWidget` will have no resources to display. Use the dual-path pattern described above to fall back to manual configuration. + +**Widget appears in standalone project mode.** +Ensure you check `IUserDesignContext.SolutionId` before assigning the `SolutionResourcesWidget`. Only assign it in the Solutions scope initialization path. + +**FolderPath still shows in Solutions mode.** +Set `FolderPath.IsVisible = false` in `InitializeSolutionsScopePropertiesAsync()`. The Solution manages the folder path, so it should be hidden from the user. + +**`ISolutionResources` service is null.** +The `ISolutionResources` service is only available in Solutions-capable Studio versions. Check for null before using: + +```csharp +var solutionResources = Services.GetService(); +if (solutionResources != null) +{ + // Safe to use Solutions APIs +} +``` diff --git a/.claude/activity-development-guide/design/widgets/text-block.md b/.claude/activity-development-guide/design/widgets/text-block.md new file mode 100644 index 000000000..b31c7d3c9 --- /dev/null +++ b/.claude/activity-development-guide/design/widgets/text-block.md @@ -0,0 +1,149 @@ +# TextBlockWidget (Read-Only Display) + +## When to read this + +Read this document when you need to: +- Display read-only text in the activity designer (informational messages, warnings, errors) +- Show status text, instructions, or labels that the user cannot edit +- Use different visual levels (Info, Warning, Error, Experimental) + +--- + +## Overview + +The `TextBlockWidget` renders read-only text in the designer. Unlike input widgets, the user cannot edit the displayed value. Use it for informational messages, status indicators, validation summaries, or instructional text. + +`TextBlockWidget` is a specialized class (not `DefaultWidget`). Use with `DesignProperty`. + +--- + +## Basic Usage + +### Simple text + +```csharp +Property.Widget = new TextBlockWidget(); +``` + +The property's `Value` is displayed as plain text. + +### Centered text + +```csharp +Property.Widget = new TextBlockWidget { Center = true }; +``` + +### Multiline text + +```csharp +Property.Widget = new TextBlockWidget { Multiline = true }; +``` + +--- + +## Visual Levels + +Set the `Level` property to change the visual styling (icon, color, background). + +### Info + +Displays with an info icon and blue styling. Use for helpful context or instructions. + +```csharp +Property.Widget = new TextBlockWidget +{ + Multiline = true, + Level = TextBlockWidgetLevel.Info +}; +``` + +### Warning + +Displays with a warning icon and yellow/amber styling. Use for important caveats or non-blocking issues. + +```csharp +Property.Widget = new TextBlockWidget +{ + Multiline = true, + Level = TextBlockWidgetLevel.Warning +}; +``` + +### Error + +Displays with an error icon and red styling. Use for validation errors or blocking issues. + +```csharp +Property.Widget = new TextBlockWidget +{ + Multiline = true, + Level = TextBlockWidgetLevel.Error +}; +``` + +### Experimental + +Displays with an experimental/beta indicator. Use for features that are in preview or under active development. + +```csharp +Property.Widget = new TextBlockWidget +{ + Level = TextBlockWidgetLevel.Experimental +}; +``` + +--- + +## Configuration Properties + +| Property | Type | Default | Description | +|---|---|---|---| +| `Center` | `bool` | `false` | Center-aligns the text horizontally. | +| `Multiline` | `bool` | `false` | Allows text to wrap across multiple lines. | +| `Level` | `TextBlockWidgetLevel` | (none) | Visual styling level: `Info`, `Warning`, `Error`, `Experimental`. | + +--- + +## Dynamic Text Updates + +Since the displayed text comes from the property's `Value`, you can update it dynamically in rules or event handlers: + +```csharp +StatusMessage.Widget = new TextBlockWidget +{ + Multiline = true, + Level = TextBlockWidgetLevel.Info +}; + +// Update text dynamically based on another property +Rule("UpdateStatus", () => +{ + StatusMessage.Value = SelectedOption.Value switch + { + "Advanced" => "Advanced mode enables additional configuration options.", + "Simple" => "Simple mode uses default settings.", + _ => "" + }; + StatusMessage.IsVisible = !string.IsNullOrEmpty(StatusMessage.Value); +}, false); + +RegisterDependency(SelectedOption, nameof(DesignProperty.Value), "UpdateStatus"); +``` + +--- + +## Troubleshooting + +**Text is truncated (single line).** +Set `Multiline = true` to allow wrapping. Without it, long text is clipped to a single line. + +**Level styling not visible.** +Ensure the `Level` property is set to a valid `TextBlockWidgetLevel` enum value. If `Level` is not set, the text block renders without any icon or colored background. + +**Text block is visible but empty.** +The displayed text comes from the property's `.Value`. Set the value before or during `InitializeModel`: + +```csharp +StatusMessage.Value = "Select an option to continue."; +StatusMessage.Widget = new TextBlockWidget { Level = TextBlockWidgetLevel.Info }; +``` diff --git a/.claude/activity-development-guide/design/widgets/text-input.md b/.claude/activity-development-guide/design/widgets/text-input.md new file mode 100644 index 000000000..aab8eaf9a --- /dev/null +++ b/.claude/activity-development-guide/design/widgets/text-input.md @@ -0,0 +1,81 @@ +# Text Input Widgets: TextComposer, RichTextComposer, PromptComposer + +## When to read this + +Read this document when you need to: +- Display a single-line or multi-line text input for a string property +- Provide a rich text (HTML) editor +- Add a prompt composer with `@` variable insertion for AI-related activities + +All three widgets are used with `DesignInArgument` properties. + +--- + +## TextComposer + +The `TextComposer` widget provides a text input that supports expressions. It defaults to multi-line mode. + +### Multi-line (default) + +```csharp +Property.Widget = new DefaultWidget { Type = ViewModelWidgetType.TextComposer }; +``` + +### Single-line + +Set the `IsSingleLineFormat` metadata to restrict to a single line. Use this when the property represents a short value like a name, subject line, or identifier. + +```csharp +Property.Widget = new DefaultWidget +{ + Type = ViewModelWidgetType.TextComposer, + Metadata = new() { { TextComposerMetadata.IsSingleLineFormat, true.ToString() } } +}; +``` + +### When to use TextComposer vs default + +The default widget for `DesignInArgument` is a plain expression editor. Use `TextComposer` when: +- Users are more likely to enter plain text than expressions +- The input benefits from multi-line editing (e.g., email body, message content) +- You want a richer text editing experience than the default expression box + +--- + +## RichTextComposer + +The `RichTextComposer` widget provides an HTML/rich text editor with formatting controls (bold, italic, lists, etc.). + +```csharp +Property.Widget = new DefaultWidget { Type = ViewModelWidgetType.RichTextComposer }; +``` + +Use `RichTextComposer` when the property stores formatted content such as: +- Email body with HTML formatting +- Notification messages with rich text +- Document content that needs inline styling + +--- + +## PromptComposer + +The `PromptComposer` widget provides a text editor with `@` variable insertion support. When the user types `@`, a dropdown of available workflow variables appears for inline insertion. + +```csharp +Property.Widget = new DefaultWidget { Type = ViewModelWidgetType.PromptComposer }; +``` + +Use `PromptComposer` for AI/LLM-related activities where the user constructs a prompt that references workflow variables. The `@` insertion makes it easy to interpolate variables into natural language prompts without manually writing expressions. + +--- + +## Troubleshooting + +**TextComposer shows multi-line when single-line is expected.** +Verify that the metadata key is `TextComposerMetadata.IsSingleLineFormat` (not a raw string), and the value is `true.ToString()` (the string `"True"`). + +**RichTextComposer content not rendering HTML at runtime.** +The `RichTextComposer` stores content as HTML. Ensure the runtime activity processes the value as HTML, not plain text. If the activity sends the value to an API, check that the API accepts HTML content. + +**PromptComposer `@` dropdown is empty.** +The `@` dropdown populates from workflow variables available in the current scope. If no variables are defined in the workflow, the dropdown will be empty. This is expected behavior -- the user must first create variables in the workflow. diff --git a/.claude/activity-development-guide/design/widgets/type-picker.md b/.claude/activity-development-guide/design/widgets/type-picker.md new file mode 100644 index 000000000..1d30be187 --- /dev/null +++ b/.claude/activity-development-guide/design/widgets/type-picker.md @@ -0,0 +1,120 @@ +# TypePickerWidget + +## When to read this + +Read this document when you need to: +- Let the user select a .NET type for a property (e.g., exception type, data type) +- Filter the type picker to show only relevant types +- Provide recommended types for quick selection + +--- + +## Overview + +The `TypePickerWidget` opens a type browser dialog where the user selects a .NET type. Unlike most widgets that use `DefaultWidget`, the type picker uses a dedicated `TypePickerWidget` class with strongly-typed configuration properties. + +Use with `DesignProperty`. + +--- + +## Basic Type Picker + +The simplest form shows all available types in the referenced assemblies. + +```csharp +Property.Widget = new TypePickerWidget { }; +``` + +--- + +## Filtered Type Picker + +Use `RecommendedTypes` and `Filter` to constrain which types are shown. + +```csharp +Property.Widget = new TypePickerWidget +{ + RecommendedTypes = new List { typeof(Exception), typeof(ArgumentException) }, + Filter = t => t == typeof(Exception) || t.IsSubclassOf(typeof(Exception)) +}; +``` + +### Configuration Properties + +| Property | Type | Description | +|---|---|---| +| `RecommendedTypes` | `List` | Types shown at the top of the picker for quick selection. These appear as "recommended" before the user browses. | +| `Filter` | `Func` | Predicate that controls which types appear in the browser. Only types returning `true` are shown. | + +--- + +## Examples + +### Exception type picker + +Show only `Exception` and its subclasses, with common exception types recommended. + +```csharp +ExceptionType.Widget = new TypePickerWidget +{ + RecommendedTypes = new List + { + typeof(Exception), + typeof(ArgumentException), + typeof(InvalidOperationException), + typeof(TimeoutException), + typeof(System.IO.IOException) + }, + Filter = t => typeof(Exception).IsAssignableFrom(t) +}; +``` + +### Data type picker for collection element type + +Show common data types for a generic collection. + +```csharp +ElementType.Widget = new TypePickerWidget +{ + RecommendedTypes = new List + { + typeof(string), + typeof(int), + typeof(double), + typeof(bool), + typeof(DateTime), + typeof(System.Data.DataRow) + } +}; +``` + +### Unrestricted type picker with recommendations + +No filter, but provide recommendations for the most common choices. + +```csharp +OutputType.Widget = new TypePickerWidget +{ + RecommendedTypes = new List + { + typeof(string), + typeof(int), + typeof(bool), + typeof(System.Data.DataTable), + typeof(Newtonsoft.Json.Linq.JObject) + } +}; +``` + +--- + +## Troubleshooting + +**Type picker dialog shows no types.** +If `Filter` is set, verify the predicate returns `true` for at least one type. A filter like `t => t == typeof(MyInternalType)` may return no results if `MyInternalType` is not in the referenced assemblies. + +**Recommended types do not appear.** +Ensure the types in `RecommendedTypes` are loadable in the current project context. If a type is from an unreferenced assembly, it may not appear. + +**Selected type is null after picker closes.** +The type picker sets the property value when the user confirms selection. If the user cancels the dialog, the property retains its previous value. Ensure your code handles a `null` type value gracefully. diff --git a/.claude/activity-development-guide/examples/complete-example.md b/.claude/activity-development-guide/examples/complete-example.md new file mode 100644 index 000000000..3c41b902f --- /dev/null +++ b/.claude/activity-development-guide/examples/complete-example.md @@ -0,0 +1,558 @@ +# Complete Example: Email Sender Activity Package + +> **When to read this:** You are building a new UiPath activity package from scratch and need a full, +> copy-paste-ready reference covering every file -- activity, ViewModel, metadata, resources, tests, +> project files, and build commands. Start here, then consult the individual guides for deeper +> explanation of each layer. + +## Cross-references + +- Activity class patterns: [../core/activity-classes.md](../core/activity-classes.md) +- ViewModel design: [../design/viewmodel-basics.md](../design/viewmodel-basics.md) +- Widget reference: [../design/widgets-and-datasources.md](../design/widgets-and-datasources.md) +- Testing patterns: [../testing/testing-guide.md](../testing/testing-guide.md) +- Service access table: [../reference/service-access.md](../reference/service-access.md) +- Compilation constants: [../reference/compilation-constants.md](../reference/compilation-constants.md) +- Feature flags: [../reference/feature-flags.md](../reference/feature-flags.md) + +--- + +## Solution layout + +``` +MyCompany.EmailActivities/ + MyCompany.EmailActivities/ + Activities/EmailSender.cs + Helpers/ActivityContextExtensions.cs + ViewModels/EmailSenderViewModel.cs + Resources/ + ActivitiesMetadata.json + Icons/email.svg + Resources.resx + Resources.Designer.cs (auto-generated) + MyCompany.EmailActivities.csproj + MyCompany.EmailActivities.Packaging/ + MyCompany.EmailActivities.Packaging.csproj + MyCompany.EmailActivities.Tests/ + EmailSenderUnitTests.cs + Workflow/EmailSenderWorkflowTests.cs + MyCompany.EmailActivities.Tests.csproj + Output/Packages/ (build output) + nuget.config + MyCompany.EmailActivities.sln +``` + +--- + +## 1. Activity class + +**File:** `MyCompany.EmailActivities/Activities/EmailSender.cs` + +```csharp +// Activities/EmailSender.cs +using System.Activities; +using System.ComponentModel; +using System.Diagnostics; +using MyCompany.EmailActivities.Helpers; +using UiPath.Robot.Activities.Api; + +namespace MyCompany.EmailActivities; + +public class EmailSender : CodeActivity +{ + [RequiredArgument] + public InArgument To { get; set; } + + [RequiredArgument] + public InArgument Subject { get; set; } + + [RequiredArgument] + public InArgument Body { get; set; } + + public InArgument Cc { get; set; } + + public EmailPriority Priority { get; set; } = EmailPriority.Normal; + + public bool IsHtml { get; set; } = false; + + public OutArgument MessageId { get; set; } + + protected override void Execute(CodeActivityContext context) + { + context.GetExecutorRuntime().LogMessage(new LogMessage + { + EventType = TraceEventType.Information, + Message = "Executing EmailSender activity" + }); + + var to = To.Get(context); + var subject = Subject.Get(context); + var body = Body.Get(context); + var cc = Cc?.Get(context); + var messageId = SendEmail(to, subject, body, cc, Priority, IsHtml); + MessageId.Set(context, messageId); + } + + public string SendEmail(string to, string subject, string body, + string cc, EmailPriority priority, bool isHtml) + { + // Business logic here + return Guid.NewGuid().ToString(); + } +} + +public enum EmailPriority { Low, Normal, High } +``` + +Key points: +- Inherits `CodeActivity` for synchronous execution. +- `[RequiredArgument]` marks mandatory inputs. +- `InArgument` for inputs, `OutArgument` for outputs. +- Plain C# properties (`Priority`, `IsHtml`) are persisted as literals, not expressions. +- Business logic (`SendEmail`) is a separate public method for unit-testability. + +--- + +## 2. Helper extension + +**File:** `MyCompany.EmailActivities/Helpers/ActivityContextExtensions.cs` + +```csharp +// Helpers/ActivityContextExtensions.cs +using System.Activities; +using UiPath.Robot.Activities.Api; + +namespace MyCompany.EmailActivities.Helpers; + +public static class ActivityContextExtensions +{ + public static IExecutorRuntime GetExecutorRuntime(this ActivityContext context) + => context.GetExtension(); +} +``` + +This avoids scattering `context.GetExtension()` calls throughout activity code. + +--- + +## 3. ViewModel class + +**File:** `MyCompany.EmailActivities/ViewModels/EmailSenderViewModel.cs` + +```csharp +// ViewModels/EmailSenderViewModel.cs +using System.Activities.ViewModels; + +namespace MyCompany.EmailActivities.ViewModels; + +public class EmailSenderViewModel : DesignPropertiesViewModel +{ + // Property names match Activity properties exactly + public DesignInArgument To { get; set; } + public DesignInArgument Subject { get; set; } + public DesignInArgument Body { get; set; } + public DesignInArgument Cc { get; set; } + public DesignProperty Priority { get; set; } + public DesignProperty IsHtml { get; set; } + public DesignOutArgument MessageId { get; set; } + + public EmailSenderViewModel(IDesignServices services) : base(services) { } + + protected override void InitializeModel() + { + base.InitializeModel(); + PersistValuesChangedDuringInit(); + + var order = 0; + + // Principal (main) properties + To.DisplayName = Resources.EmailSender_To_DisplayName; + To.Tooltip = Resources.EmailSender_To_Tooltip; + To.IsRequired = true; + To.IsPrincipal = true; + To.OrderIndex = order++; + + Subject.DisplayName = Resources.EmailSender_Subject_DisplayName; + Subject.Tooltip = Resources.EmailSender_Subject_Tooltip; + Subject.IsRequired = true; + Subject.IsPrincipal = true; + Subject.OrderIndex = order++; + + Body.DisplayName = Resources.EmailSender_Body_DisplayName; + Body.Tooltip = Resources.EmailSender_Body_Tooltip; + Body.IsRequired = true; + Body.IsPrincipal = true; + Body.Widget = new DefaultWidget + { + Type = ViewModelWidgetType.TextComposer + }; + Body.OrderIndex = order++; + + // Secondary properties + Cc.DisplayName = Resources.EmailSender_Cc_DisplayName; + Cc.Tooltip = Resources.EmailSender_Cc_Tooltip; + Cc.OrderIndex = order++; + + Priority.DisplayName = Resources.EmailSender_Priority_DisplayName; + Priority.Tooltip = Resources.EmailSender_Priority_Tooltip; + Priority.DataSource = EnumDataSourceBuilder + .Configure() + .WithSingleItemConverter( + itemToValue: item => item.ToString(), + valueToItem: value => Enum.TryParse(value, out var v) + ? v : EmailPriority.Normal) + .WithData(Enum.GetValues()) + .Build(); + Priority.OrderIndex = order++; + + IsHtml.DisplayName = Resources.EmailSender_IsHtml_DisplayName; + IsHtml.Tooltip = Resources.EmailSender_IsHtml_Tooltip; + IsHtml.Widget = new DefaultWidget { Type = ViewModelWidgetType.Toggle }; + IsHtml.OrderIndex = order++; + + // Output + MessageId.DisplayName = Resources.EmailSender_MessageId_DisplayName; + MessageId.Tooltip = Resources.EmailSender_MessageId_Tooltip; + MessageId.OrderIndex = order++; + } + + protected override void InitializeRules() + { + base.InitializeRules(); + + // When IsHtml changes, update Body widget + Rule(nameof(IsHtml), () => + { + Body.Widget = IsHtml.Value + ? new DefaultWidget { Type = ViewModelWidgetType.RichTextComposer } + : new DefaultWidget { Type = ViewModelWidgetType.TextComposer }; + }); + } + + protected override void ManualRegisterDependencies() + { + base.ManualRegisterDependencies(); + RegisterDependency(IsHtml, nameof(IsHtml.Value), nameof(IsHtml)); + } +} +``` + +Key points: +- Property names on the ViewModel must match the Activity class exactly. +- `IsPrincipal = true` surfaces properties in the collapsed card view. +- `OrderIndex` controls display order in the properties panel. +- `InitializeRules()` defines reactive rules (e.g., toggling the body widget when `IsHtml` changes). +- `ManualRegisterDependencies()` wires up property-change tracking for rules. + +--- + +## 4. Metadata JSON + +**File:** `MyCompany.EmailActivities/Resources/ActivitiesMetadata.json` + +```json +{ + "resourceManagerName": "MyCompany.EmailActivities.Resources", + "activities": [ + { + "fullName": "MyCompany.EmailActivities.EmailSender", + "shortName": "EmailSender", + "displayNameKey": "EmailSender_DisplayName", + "descriptionKey": "EmailSender_Description", + "categoryKey": "Email", + "iconKey": "email.svg", + "viewModelType": "MyCompany.EmailActivities.ViewModels.EmailSenderViewModel" + } + ] +} +``` + +This file must be embedded as a resource in the `.csproj`. The `resourceManagerName` points to the +generated `Resources.Designer.cs` class. Each entry maps an activity to its display name, icon, and +ViewModel. + +--- + +## 5. Resources + +**File:** `MyCompany.EmailActivities/Resources/Resources.resx` + +```xml + +Send Email +Sends an email message +To +Recipient email address +Subject +Email subject line +Body +Email body content +CC +Carbon copy recipients (optional) +Priority +Email priority level +HTML Body +Whether the body contains HTML +Message ID +Unique ID of the sent message +``` + +Naming convention: `{ActivityShortName}_{PropertyName}_{DisplayName|Tooltip|Description}`. + +--- + +## 6. Unit test + +**File:** `MyCompany.EmailActivities.Tests/EmailSenderUnitTests.cs` + +```csharp +public class EmailSenderUnitTests +{ + [Fact] + public void SendEmail_ReturnsMessageId() + { + var sender = new EmailSender(); + var result = sender.SendEmail("test@example.com", "Test", "Body", + null, EmailPriority.Normal, false); + Assert.NotNull(result); + Assert.NotEmpty(result); + } +} +``` + +Unit tests exercise the public business-logic method directly, without the workflow runtime. + +--- + +## 7. Workflow test + +**File:** `MyCompany.EmailActivities.Tests/Workflow/EmailSenderWorkflowTests.cs` + +```csharp +// Tests/Workflow/EmailSenderWorkflowTests.cs +public class EmailSenderWorkflowTests +{ + private readonly Mock _runtimeMock = new(); + + [Fact] + public void SendEmail_SetsMessageIdOutput() + { + var activity = new EmailSender + { + To = "test@example.com", + Subject = "Test", + Body = "Hello", + Priority = EmailPriority.High, + IsHtml = false + }; + + var runner = new WorkflowInvoker(activity); + runner.Extensions.Add(() => _runtimeMock.Object); + + var result = runner.Invoke(TimeSpan.FromSeconds(5)); + + Assert.NotNull(result["MessageId"]); + } +} +``` + +Workflow tests run the activity inside a `WorkflowInvoker`, validating that inputs/outputs flow +correctly through the runtime. Mock `IExecutorRuntime` to avoid real robot dependencies. + +--- + +## 8. nuget.config + +**File:** `nuget.config` (solution root) + +```xml + + + + + + + +``` + +The UiPath Official feed provides `UiPath.Activities.Api`, `UiPath.Workflow`, and +`System.Activities.ViewModels`. + +--- + +## 9. Main project .csproj + +**File:** `MyCompany.EmailActivities/MyCompany.EmailActivities.csproj` + +```xml + + + Library + net8.0 + enable + enable + MyCompany.EmailActivities + + + + + + + + + + + + + + + + + Resources.resx + True + True + MyCompany.EmailActivities + + + + + Resources.Designer.cs + PublicResXFileCodeGenerator + MyCompany.EmailActivities + + + +``` + +Key points: +- `ActivitiesMetadata.json` and SVG icons are embedded resources. +- `PrivateAssets="All"` on UiPath packages prevents them from flowing to consumers (the robot provides them at runtime). +- `PublicResXFileCodeGenerator` auto-generates the `Resources.Designer.cs` typed accessor class. + +--- + +## 10. Packaging project .csproj + +**File:** `MyCompany.EmailActivities.Packaging/MyCompany.EmailActivities.Packaging.csproj` + +```xml + + + net8.0 + enable + enable + + + True + $([System.DateTime]::UtcNow.DayOfYear.ToString("F0")) + $([System.DateTime]::UtcNow.TimeOfDay.TotalMinutes.ToString("F0")) + 1.0.0 + 1.0.$(VersionBuild)-dev.$(VersionRevision) + MyCompany.EmailActivities + MyCompany + Email sending activities for UiPath + UiPathActivities + ..\Output\Packages\ + AddDlls + False + + + + + + + + + + + + + + + + + + + + + + + All + + + +``` + +Key points: +- Separate packaging project keeps the main project clean. +- `GeneratePackageOnBuild` produces a `.nupkg` on every build. +- Debug builds get a timestamped prerelease version (e.g., `1.0.61-dev.832`). +- Release builds get a clean semver (e.g., `1.0.0`). +- `AddDlls` target manually includes the main project DLL (and PDB in debug) into the package. +- `CleanPackageFiles` deletes old `.nupkg` files before each build. + +--- + +## 11. Test project .csproj + +**File:** `MyCompany.EmailActivities.Tests/MyCompany.EmailActivities.Tests.csproj` + +```xml + + + net8.0 + enable + enable + false + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + +``` + +Key points: +- `IsPackable=false` prevents the test project from being packed. +- xunit + Moq is the standard test stack. +- `UiPath.Activities.Api` is marked `DevelopmentDependency="true"` since it is only needed for mocking. + +--- + +## 12. Build and deploy commands + +```bash +# Create solution +dotnet new sln -n MyCompany.EmailActivities +dotnet sln add MyCompany.EmailActivities/MyCompany.EmailActivities.csproj +dotnet sln add MyCompany.EmailActivities.Packaging/MyCompany.EmailActivities.Packaging.csproj +dotnet sln add MyCompany.EmailActivities.Tests/MyCompany.EmailActivities.Tests.csproj + +# Build (generates Resources.Designer.cs, compiles, produces .nupkg) +dotnet build -c Release + +# Run tests +dotnet test + +# The .nupkg is at Output/Packages/MyCompany.EmailActivities.1.0.0.nupkg +# Install in Studio: Manage Packages -> Settings -> add local feed -> select Output/Packages/ +``` + +### Build sequence explanation + +1. `dotnet build -c Release` triggers the following: + - `Resources.Designer.cs` is regenerated from `Resources.resx`. + - The main project compiles, embedding `ActivitiesMetadata.json` and icons. + - The packaging project compiles, pulling the main DLL and producing a `.nupkg`. +2. `dotnet test` discovers and runs all xunit tests in the test project. +3. The output `.nupkg` can be consumed by UiPath Studio via a local NuGet feed. diff --git a/.claude/activity-development-guide/index.md b/.claude/activity-development-guide/index.md new file mode 100644 index 000000000..00313fcf2 --- /dev/null +++ b/.claude/activity-development-guide/index.md @@ -0,0 +1,99 @@ +# UiPath Activity Development Guide + +> **For**: AI coding agents, skills, and human developers. +> **Purpose**: Build correct, deployable UiPath activity packages. +> **Maintained by**: Humans and AI agents — if a task fails due to missing/incorrect guidance, update the relevant file. + +--- + +## Architecture at a Glance + +Every UiPath activity has three parts: + +``` +Activity (Runtime) — what happens when the workflow runs + ↕ property names must match +ViewModel (Design-Time) — how the activity looks in Studio + ↕ referenced by +Metadata (JSON) — links activity ↔ ViewModel, defines display name/icon/category +``` + +**Key rule**: Property names on the ViewModel MUST exactly match the Activity's property names. + +--- + +## Critical Pitfalls (Read First) + +These are the most common sources of bugs. Every agent should know these: + +1. **Context dies after `await`** — Read ALL `InArgument` values and extensions from `ActivityContext` BEFORE the first `await`. The context is disposed after that point. → [core/activity-context.md](core/activity-context.md) + +2. **Property name mismatch** — ViewModel properties must match Activity properties by exact name. A mismatch silently breaks serialization. → [design/viewmodel.md](design/viewmodel.md) + +3. **`InArgument` defaults are NOT null** — `InArgument.Get(context)` returns `0` (not `null`) when unset. Null-coalescing (`?? default`) never triggers for value types. Initialize defaults in the property declaration. → [runtime/activity-code.md](runtime/activity-code.md) + +4. **`[DefaultValue]` required for new properties** — Adding a property to an existing activity without `[DefaultValue]` breaks all existing `.xaml` workflows that use it. → [core/best-practices.md](core/best-practices.md) + +5. **`PrivateAssets="All"` misuse** — Use it for host-provided packages (UiPath SDK). Do NOT use it for third-party runtime dependencies, or the activity will get `FileNotFoundException` at runtime. → [core/project-structure.md](core/project-structure.md) + +--- + +## Which File to Read + +### By task + +| Task | Start here | Then read | +|------|-----------|-----------| +| **Create a new activity** | [core/project-structure.md](core/project-structure.md) | [runtime/activity-code.md](runtime/activity-code.md) → [design/viewmodel.md](design/viewmodel.md) → [design/metadata.md](design/metadata.md) → [design/localization.md](design/localization.md) → [examples/complete-example.md](examples/complete-example.md) | +| **Refactor an activity** | [core/best-practices.md](core/best-practices.md) | [core/activity-context.md](core/activity-context.md) → topic file for the area being refactored | +| **Add/change a widget** | [design/widgets/index.md](design/widgets/index.md) | Individual widget file if needed | +| **Add Orchestrator integration** | [runtime/orchestrator.md](runtime/orchestrator.md) | [design/bindings.md](design/bindings.md) | +| **Write tests** | [testing/activity-testing.md](testing/activity-testing.md) | [testing/viewmodel-testing.md](testing/viewmodel-testing.md) | +| **Code review** | [core/best-practices.md](core/best-practices.md) | [core/activity-context.md](core/activity-context.md) | +| **Use Activities SDK** | [advanced/sdk-framework.md](advanced/sdk-framework.md) | Only when DI, telemetry, or bindings are needed | + +### By topic + +| Topic | File | +|-------|------| +| Architecture, platform context, Studio Desktop vs Web | [core/architecture.md](core/architecture.md) | +| ActivityContext, WF4, "read before await" | [core/activity-context.md](core/activity-context.md) | +| Project layout, .csproj, packaging, NuGet | [core/project-structure.md](core/project-structure.md) | +| Best practices, versioning, compatibility | [core/best-practices.md](core/best-practices.md) | +| Activity base classes, arguments, attributes | [runtime/activity-code.md](runtime/activity-code.md) | +| IExecutorRuntime, IWorkflowRuntime, feature detection | [runtime/platform-api.md](runtime/platform-api.md) | +| Orchestrator version detection, API patterns | [runtime/orchestrator.md](runtime/orchestrator.md) | +| ViewModel lifecycle, properties, partial classes | [design/viewmodel.md](design/viewmodel.md) | +| Widget types and implementation | [design/widgets/index.md](design/widgets/index.md) | +| DataSource patterns (static, dynamic, enum) | [design/datasources.md](design/datasources.md) | +| Rules and reactive dependencies | [design/rules-and-dependencies.md](design/rules-and-dependencies.md) | +| Menu actions, mode switching | [design/menu-actions.md](design/menu-actions.md) | +| Validation (property, model, preview) | [design/validation.md](design/validation.md) | +| Metadata JSON, SDK registration | [design/metadata.md](design/metadata.md) | +| Localization / Resources.resx | [design/localization.md](design/localization.md) | +| Bindings (Orchestrator, connections) | [design/bindings.md](design/bindings.md) | +| Project settings (ArgumentSetting) | [design/project-settings.md](design/project-settings.md) | +| Solutions vs project scope | [design/solutions.md](design/solutions.md) | +| Activity testing (3 levels) | [testing/activity-testing.md](testing/activity-testing.md) | +| ViewModel testing (4 approaches) | [testing/viewmodel-testing.md](testing/viewmodel-testing.md) | +| Advanced patterns (NativeActivity, bookmarks) | [advanced/patterns.md](advanced/patterns.md) | +| FilterBuilder widget | [design/widgets/filter-builder.md](design/widgets/filter-builder.md) | +| Activities SDK (DI, telemetry, bindings) | [advanced/sdk-framework.md](advanced/sdk-framework.md) | +| Service access quick reference | [reference/service-access.md](reference/service-access.md) | +| Compilation constants | [reference/compilation-constants.md](reference/compilation-constants.md) | +| Design feature flags | [reference/feature-flags.md](reference/feature-flags.md) | +| Complete worked example (all files) | [examples/complete-example.md](examples/complete-example.md) | + +--- + +## Contributing to This Guide + +When an AI agent encounters a problem not covered here, or finds incorrect guidance: + +1. **Identify the right file** using the topic table above +2. **Add a "Troubleshooting" entry** at the bottom of that file with: + - What went wrong + - Why it went wrong + - The correct approach +3. **Keep entries concrete** — include code snippets, not just prose +4. **Cross-reference** other files when the fix spans multiple topics diff --git a/.claude/activity-development-guide/reference/compilation-constants.md b/.claude/activity-development-guide/reference/compilation-constants.md new file mode 100644 index 000000000..3d916a5d1 --- /dev/null +++ b/.claude/activity-development-guide/reference/compilation-constants.md @@ -0,0 +1,78 @@ +# Conditional Compilation Constants + +> **When to read this:** You are writing code that must behave differently depending on the target +> platform or available infrastructure (e.g., Windows-only code, Integration Service features, or +> governance checks). Use these symbols in `#if` / `#endif` blocks. + +## Cross-references + +- Activity class patterns: [../core/activity-classes.md](../core/activity-classes.md) +- Feature flags (design-time detection): [feature-flags.md](feature-flags.md) +- Complete example: [../examples/complete-example.md](../examples/complete-example.md) + +--- + +## Compilation constants table + +| Symbol | Defined When | Purpose | +|---|---|---| +| `NETPORTABLE_UIPATH` | Targeting net5.0 or net6.0 (but **not** net6.0-windows) | Guard Windows-specific code so it compiles on portable/cross-platform targets | +| `INTEGRATION_SERVICE` | Integration Service connection client is available | Enable Integration Service support (connection widgets, token acquisition) | +| `GOVERNANCE_SERVICE` | Governance infrastructure is available | Enable runtime governance checks and policy enforcement | + +## Usage examples + +### Guarding Windows-specific code + +```csharp +public void ConfigurePlatform() +{ +#if !NETPORTABLE_UIPATH + // Windows-only: use Win32 APIs, WPF, etc. + SetWindowsRegistryKey("MyApp", "Setting", value); +#else + // Portable path: use cross-platform alternative + SetConfigFile("MyApp", "Setting", value); +#endif +} +``` + +### Conditional Integration Service support + +```csharp +protected override void InitializeModel() +{ + base.InitializeModel(); + +#if INTEGRATION_SERVICE + Connection.Widget = new DefaultWidget + { + Type = ViewModelWidgetType.Connection + }; + Connection.DisplayName = "Connection"; +#endif +} +``` + +### Conditional governance checks + +```csharp +protected override void Execute(CodeActivityContext context) +{ +#if GOVERNANCE_SERVICE + var governance = context.GetExtension(); + governance?.ValidatePolicy(this); +#endif + + // Normal execution logic +} +``` + +## Notes + +- These symbols are defined at the project level (in `.csproj` or `Directory.Build.props`) based on + build configuration and target framework. +- Unlike design-time feature flags (checked via `IWorkflowDesignApi.HasFeature()`), compilation + constants are resolved at build time and result in different compiled assemblies. +- When adding platform-specific behavior, prefer compilation constants over runtime checks to avoid + pulling in unnecessary dependencies on unsupported platforms. diff --git a/.claude/activity-development-guide/reference/feature-flags.md b/.claude/activity-development-guide/reference/feature-flags.md new file mode 100644 index 000000000..fc91b49f3 --- /dev/null +++ b/.claude/activity-development-guide/reference/feature-flags.md @@ -0,0 +1,115 @@ +# Design Feature Flags + +> **When to read this:** You need to check which Studio capabilities are available at design time +> before enabling optional ViewModel behavior (e.g., project settings, advanced bindings, triggers). +> Feature flags are checked at runtime via `IWorkflowDesignApi.HasFeature()`. + +## Cross-references + +- Service access (how to get `IWorkflowDesignApi`): [service-access.md](service-access.md) +- Compilation constants (build-time platform checks): [compilation-constants.md](compilation-constants.md) +- ViewModel basics: [../design/viewmodel-basics.md](../design/viewmodel-basics.md) +- Complete example: [../examples/complete-example.md](../examples/complete-example.md) + +--- + +## Feature flag reference + +Feature flags are queried through `IWorkflowDesignApi`: + +```csharp +var api = Services.GetService(); +``` + +| Feature Key | Check | Purpose | +|---|---|---| +| `DesignFeatureKeys.Settings` | `api.HasFeature(DesignFeatureKeys.Settings)` | Project settings support -- the host can persist per-project settings | +| `DesignFeatureKeys.PackageBindingsV3` | `api.HasFeature(DesignFeatureKeys.PackageBindingsV3)` | V3 binding support -- enables standard property bindings | +| `DesignFeatureKeys.PackageBindingsV4` | `api.HasFeature(DesignFeatureKeys.PackageBindingsV4)` | V4 dependent property bindings -- enables bindings that depend on other property values | +| `DesignFeatureKeys.ActivityTriggers` | `api.HasFeature(DesignFeatureKeys.ActivityTriggers)` | Trigger support -- the host supports activity-based triggers | +| `DesignFeatureKeys.WidgetSupportInfoService` | `api.HasFeature(DesignFeatureKeys.WidgetSupportInfoService)` | Widget availability checks -- can query whether a specific widget type is supported | + +## Usage examples + +### Conditional project settings + +```csharp +protected override void InitializeModel() +{ + base.InitializeModel(); + + var api = Services.GetService(); + + if (api.HasFeature(DesignFeatureKeys.Settings)) + { + // Read a default value from project settings + var defaultServer = api.ProjectSettings.GetValue("EmailServer"); + if (!string.IsNullOrEmpty(defaultServer)) + { + Server.SetDefaultValue(defaultServer); + } + } +} +``` + +### Guarding V4 dependent bindings + +```csharp +protected override void InitializeModel() +{ + base.InitializeModel(); + + var api = Services.GetService(); + + if (api.HasFeature(DesignFeatureKeys.PackageBindingsV4)) + { + // Use dependent property bindings (V4) + ConfigureDependentBindings(); + } + else if (api.HasFeature(DesignFeatureKeys.PackageBindingsV3)) + { + // Fall back to standard bindings (V3) + ConfigureStandardBindings(); + } +} +``` + +### Checking widget support before assignment + +```csharp +protected override void InitializeModel() +{ + base.InitializeModel(); + + var api = Services.GetService(); + + if (api.HasFeature(DesignFeatureKeys.WidgetSupportInfoService)) + { + var widgetSupport = Services.GetService(); + if (widgetSupport.IsSupported(ViewModelWidgetType.PromptComposer)) + { + Prompt.Widget = new DefaultWidget + { + Type = ViewModelWidgetType.PromptComposer + }; + } + else + { + // Fall back to a plain text composer + Prompt.Widget = new DefaultWidget + { + Type = ViewModelWidgetType.TextComposer + }; + } + } +} +``` + +## Notes + +- Feature flags are a design-time mechanism. They detect what the hosting Studio version supports. + For build-time platform differences, use [compilation constants](compilation-constants.md) instead. +- Always provide a fallback when a feature is absent. Activities must degrade gracefully in older + Studio versions. +- `HasFeature()` returns `false` if the key is unrecognized, so it is safe to check for newer flags + in code that runs on older hosts. diff --git a/.claude/activity-development-guide/reference/service-access.md b/.claude/activity-development-guide/reference/service-access.md new file mode 100644 index 000000000..4f0ca2402 --- /dev/null +++ b/.claude/activity-development-guide/reference/service-access.md @@ -0,0 +1,59 @@ +# Service Access Reference + +> **When to read this:** You need to interact with Studio services from a ViewModel -- showing dialogs, +> detecting features, registering types, or obtaining auth tokens. This table lists every service +> available through the `Services` property on `DesignPropertiesViewModel`. + +## Cross-references + +- ViewModel basics: [../design/viewmodel-basics.md](../design/viewmodel-basics.md) +- Feature flags: [feature-flags.md](feature-flags.md) +- Complete example (ViewModel usage): [../examples/complete-example.md](../examples/complete-example.md) + +--- + +## Service table + +All services are obtained via `Services.GetService()` inside a ViewModel. + +| Service | How to Access | Purpose | +|---|---|---| +| `IWorkflowDesignApi` | `Services.GetService()` | Workflow/project APIs, feature detection via `HasFeature()` | +| `IBusyService` | `Services.GetService()` | Show loading indicators during long-running operations | +| `IDialogService` | `Services.GetService()` | Show dialogs and confirmation prompts to the user | +| `IDesignerCustomTypesService` | `Services.GetService()` | Register custom .NET types for use in expressions | +| `IUserDesignContext` | `Services.GetService()` | Check whether the activity is running in a Solutions context | +| `ISolutionResources` | `Services.GetService()` | Manage and access Solutions resource files | +| `IAccessProvider` | `Services.GetService()` | Obtain auth tokens for external API calls | +| `IViewModelActionCallbackFactory` | `Services.GetService()` | Create callbacks for link clicks and action buttons | + +## Usage example + +```csharp +protected override void InitializeModel() +{ + base.InitializeModel(); + + // Feature detection + var api = Services.GetService(); + if (api.HasFeature(DesignFeatureKeys.Settings)) + { + // Project settings are supported -- configure accordingly + } + + // Show a loading indicator during async initialization + var busy = Services.GetService(); + using (busy.ShowBusy()) + { + // ... long-running init work ... + } +} +``` + +## Notes + +- `GetService()` returns `null` if the service is not available in the current host. Always + null-check when the service may be absent (e.g., `ISolutionResources` is only present in + Solutions-enabled Studio). +- `IWorkflowDesignApi.HasFeature()` is the primary way to check Studio capabilities at design time. + See [feature-flags.md](feature-flags.md) for the list of known feature keys. diff --git a/.claude/activity-development-guide/runtime/activity-code.md b/.claude/activity-development-guide/runtime/activity-code.md new file mode 100644 index 000000000..ae859e8aa --- /dev/null +++ b/.claude/activity-development-guide/runtime/activity-code.md @@ -0,0 +1,357 @@ +# Activity Code (Runtime Implementation) + +> **When to read this**: You are implementing the runtime logic of a UiPath activity -- choosing a base class, defining properties, handling defaults, or adding logging. This covers everything inside the `Execute` method and the class structure around it. + +**Related files**: +- [Platform API](../runtime/platform-api.md) -- runtime services (logging, settings, OAuth) +- [Orchestrator Integration](../runtime/orchestrator.md) -- calling Orchestrator APIs from activities + +--- + +## Base Class Options + +| Base Class | When to Use | +|---|---| +| `CodeActivity` | Simple synchronous activities returning a value | +| `CodeActivity` | Simple synchronous activities with no return value | +| `AsyncCodeActivity` | Async activities (I/O, network calls) | +| `SdkActivity` | Activities using the SDK framework (DI, telemetry, bindings) | +| `SdkNativeActivity` | Complex activities needing retry, bookmarks, or child activities | + +**Decision guide**: +- No async work, no DI needed --> `CodeActivity` or `CodeActivity` +- Async I/O but no SDK features --> `AsyncCodeActivity` +- Need DI, telemetry, or project settings bindings --> `SdkActivity` +- Need retry logic, bookmarks, or scheduling child activities --> `SdkNativeActivity` + +--- + +## Simple Activity (CodeActivity) + +```csharp +using System.Activities; +using System.ComponentModel; + +namespace MyCompany.MyActivities; + +public class Calculator : CodeActivity +{ + [RequiredArgument] + public InArgument FirstNumber { get; set; } + + [RequiredArgument] + public InArgument SecondNumber { get; set; } + + [RequiredArgument] + public Operation SelectedOperation { get; set; } = Operation.Add; + + protected override int Execute(CodeActivityContext context) + { + var a = FirstNumber.Get(context); + var b = SecondNumber.Get(context); + return ExecuteInternal(a, b); + } + + // Separate business logic for testability + public int ExecuteInternal(int a, int b) + { + return SelectedOperation switch + { + Operation.Add => a + b, + Operation.Subtract => a - b, + Operation.Multiply => a * b, + Operation.Divide => a / b, + _ => throw new ArgumentOutOfRangeException() + }; + } +} + +public enum Operation { Add, Subtract, Multiply, Divide } +``` + +Key points: +- `CodeActivity` means the activity returns an `int` via its `Result` OutArgument. +- `Execute` receives a `CodeActivityContext` used to resolve argument values. +- Extract business logic into a separate method (`ExecuteInternal`) for unit testing without a workflow context. + +--- + +## SDK Activity (SdkActivity with DI) + +Use when the activity needs dependency injection, telemetry, bindings, or project settings. + +```csharp +using UiPath.Sdk.Activities; + +// Link activity to its ViewModel +[ViewModelClass(typeof(DepositInAccountViewModel))] +public partial class DepositInAccount : SdkActivity +{ + [RequiredArgument] + public InArgument AccountHolder { get; set; } + + [RequiredArgument] + public InArgument Amount { get; set; } + + [ArgumentSetting("DepositInAccount", "Currency")] + public InArgument Currency { get; set; } + + protected override async Task ExecuteAsync( + AsyncCodeActivityContext context, + IServiceProvider serviceProvider, + CancellationToken cancellationToken) + { + var bank = serviceProvider.GetRequiredService(); + var holder = AccountHolder.Get(context); + var amount = Amount.Get(context); + + return await bank.DepositAsync(holder, amount, cancellationToken); + } +} +``` + +Key points: +- `[ViewModelClass]` links the activity to its design-time ViewModel. +- `[ArgumentSetting]` binds a property to a project setting (auto-populated from Studio settings UI). +- `serviceProvider.GetRequiredService()` resolves DI-registered services. +- `ExecuteAsync` provides a `CancellationToken` for cooperative cancellation. + +--- + +## SDK Activity with Retry (SdkNativeActivity) + +```csharp +public class RetryableActivity : SdkNativeActivity +{ + public InArgument MaxRetries { get; set; } = new(3); + + protected override async Task ExecuteAsync( + NativeActivityContext context, + IServiceProvider serviceProvider, + CancellationToken cancellationToken) + { + // Execution logic + return await DoWorkAsync(cancellationToken); + } + + protected override bool ShouldRetry( + NativeActivityContext context, object executionValue, int retryCount) + { + return retryCount < MaxRetries.Get(context); + } + + protected override void PrepareForRetry(NativeActivityContext context, int retryCount) + { + // Reset state before retry + } +} +``` + +Key points: +- Override `ShouldRetry` to control retry behavior based on execution result and retry count. +- Override `PrepareForRetry` to reset any internal state between attempts. +- The framework handles the retry loop; you supply the decision logic. + +--- + +## Activity Property Types + +| C# Type | Purpose | Designer Behavior | +|---|---|---| +| `InArgument` | Input -- accepts expressions/variables | Expression editor | +| `OutArgument` | Output -- writes to variables | Variable picker | +| `InOutArgument` | Bidirectional -- read and modify | Expression editor | +| `T` (direct type) | Constant/enum value | Type-specific editor (dropdown, checkbox, etc.) | + +--- + +## Enum Selector Properties + +Properties that select behavior via an enum (operation mode, algorithm, format) must be declared as **plain `TEnum` properties**, never as `InArgument`: + +```csharp +// CORRECT — plain property, maps to DesignProperty in ViewModel +public Operation SelectedOperation { get; set; } = Operation.Add; + +// WRONG — InArgument wrapper mismatches DesignProperty binding +public InArgument SelectedOperation { get; set; } +``` + +**Why**: The ViewModel binds enum selectors via `DesignProperty`, which maps to a plain property. `InArgument` mismatches this binding and changes how the value is retrieved in `Execute`. + +The enum runtime auto-renders as a dropdown in the designer — no explicit `DataSource` setup is needed for simple cases. Only add `EnumDataSourceBuilder` when you need custom labels, custom ordering, or value conversion. + +--- + +## ExecuteInternal Pattern + +Extract business logic into a public `ExecuteInternal()` method for testability. Key rules: + +1. **Read enum properties directly from the public property** — never capture into a private field in `Execute` and never pass the enum as a parameter: + +```csharp +// CORRECT — reads public property directly, testable +public int ExecuteInternal(int a, int b) +{ + return SelectedOperation switch + { + Operation.Add => a + b, + Operation.Subtract => a - b, + _ => throw new NotSupportedException($"Operation '{SelectedOperation}' is not supported.") + }; +} + +// WRONG — private field only gets set during Execute, breaks direct test calls +private Operation _op; +protected override int Execute(CodeActivityContext context) +{ + _op = SelectedOperation; // field stays default(Operation) when tests call ExecuteInternal directly + return ExecuteInternal(FirstNumber.Get(context), SecondNumber.Get(context)); +} +``` + +2. **Don't pass enum as a parameter** — it changes the public API contract and breaks tests written against the expected signature: + +```csharp +// WRONG — enum as parameter +public int ExecuteInternal(int a, int b, Operation op) { ... } + +// CORRECT — reads from public property +public int ExecuteInternal(int a, int b) { return SelectedOperation switch { ... }; } +``` + +A test sets the property directly and calls `ExecuteInternal`: + +```csharp +var activity = new Calculator { SelectedOperation = Operation.Multiply }; +var result = activity.ExecuteInternal(3, 4); +Assert.Equal(12, result); +``` + +--- + +## Default Values for InArgument (CRITICAL) + +`InArgument.Get(context)` returns the **value type default** (e.g., `0`, `0.0`, `false`) when the user does not set the property in Studio -- **not** `null`. Null-coalescing fallbacks will never trigger for value types. + +### Wrong -- null-coalescing does not work for value types + +```csharp +// Rotation.Get(context) returns 0.0 when unset, not null +var rotation = Rotation.Get(context) ?? -45.0; // always 0.0, never -45.0 + +// Same problem with default keyword +var opacity = Opacity.Get(context); +if (opacity == default) opacity = 0.5; // 0.0 IS the default for double, so this works + // but breaks if 0.0 is a valid user input +``` + +### Correct -- initialize in the property declaration + +```csharp +public InArgument Rotation { get; set; } = new(-45.0); +public InArgument Opacity { get; set; } = new(0.5); +public InArgument MaxRetries { get; set; } = new(3); +public InArgument ContinueOnError { get; set; } = new(false); + +protected override void Execute(CodeActivityContext context) +{ + // These return the initialized defaults when the user hasn't set them + var rotation = Rotation.Get(context); // -45.0 if unset + var opacity = Opacity.Get(context); // 0.5 if unset +} +``` + +The workflow engine uses the `InArgument` constructor value when the user does not set the property. + +### When `[DefaultValue]` is also needed + +For backward compatibility (adding a new property to an existing activity), combine both: + +```csharp +[DefaultValue(3)] +public InArgument MaxRetries { get; set; } = new(3); +``` + +`[DefaultValue]` tells the XAML serializer to omit the property when it equals the default, keeping saved workflows clean. The `InArgument` constructor sets the actual runtime value. + +--- + +## Runtime Logging + +Activities log via `IExecutorRuntime`, obtained from the activity context. Create a helper extension: + +```csharp +// Helpers/ActivityContextExtensions.cs +using System.Activities; +using UiPath.Robot.Activities.Api; + +namespace MyCompany.MyActivities.Helpers; + +public static class ActivityContextExtensions +{ + public static IExecutorRuntime GetExecutorRuntime(this ActivityContext context) + => context.GetExtension(); +} +``` + +Usage in an activity: + +```csharp +protected override int Execute(CodeActivityContext context) +{ + context.GetExecutorRuntime().LogMessage(new LogMessage + { + EventType = TraceEventType.Information, + Message = "Executing Calculator activity" + }); + + var a = FirstNumber.Get(context); + var b = SecondNumber.Get(context); + return ExecuteInternal(a, b); +} +``` + +**Always use `GetExecutorRuntime()`** (the extension method), not `context.GetExtension()` with a null-conditional (`?.`). The null-conditional silently drops all log messages when the runtime is not registered — no error, nothing logged, invisible failure. + +**Never define your own `GetExecutorRuntime` extension method** if the project already has one in its `Helpers/` folder. A duplicate causes a compile-time ambiguous invocation error. + +See [Platform API](../runtime/platform-api.md) for the full `IExecutorRuntime` interface and additional logging details. + +--- + +## Activity Attributes Reference + +| Attribute | Purpose | +|---|---| +| `[RequiredArgument]` | Property must be set before execution | +| `[DefaultValue(value)]` | Default value (important for backward compatibility) | +| `[Browsable(false)]` | Hide from properties panel | +| `[Category("name")]` | Group in properties panel | +| `[DisplayName("name")]` | Override display name | +| `[Description("text")]` | Tooltip text | +| `[Obsolete("message")]` | Mark as deprecated | +| `[ViewModelClass(typeof(T))]` | Link to SDK ViewModel (SDK activities only) | +| `[ArgumentSetting("activity", "key")]` | Bind property to a project setting (SDK activities only) | + +--- + +## Troubleshooting + + + +### Enum property always uses default value + +**Symptom**: The enum property (e.g., `SelectedOperation`) always behaves as if it's the first enum value, regardless of what the user selected in Studio. +**Cause**: The property was declared as `InArgument` instead of plain `TEnum`. The ViewModel uses `DesignProperty` which maps to a plain property, so the binding silently fails. +**Fix**: Change `public InArgument Prop { get; set; }` to `public TEnum Prop { get; set; } = TEnum.Default;` + +### ExecuteInternal returns wrong result in tests + +**Symptom**: Unit tests calling `ExecuteInternal` directly always get the wrong result (usually the result for the default enum value). +**Cause**: The enum value was captured into a private instance field inside `Execute()`, which isn't called in unit tests. +**Fix**: Have `ExecuteInternal` read the enum directly from the public property (`this.SelectedOperation`) instead of a private field. diff --git a/.claude/activity-development-guide/runtime/orchestrator.md b/.claude/activity-development-guide/runtime/orchestrator.md new file mode 100644 index 000000000..aef4b8373 --- /dev/null +++ b/.claude/activity-development-guide/runtime/orchestrator.md @@ -0,0 +1,243 @@ +# Orchestrator Integration + +> **When to read this**: You are writing or modifying an activity that calls UiPath Orchestrator APIs -- queues, assets, processes, jobs, or any other Orchestrator resource. This covers version detection, API call patterns, folder resolution, error handling, and the base class that provides built-in infrastructure. + +**Related files**: +- [Platform API](../runtime/platform-api.md) -- `IOrchestratorSettings`, `IWorkflowRuntime`, and related interfaces +- [Activity Code](../runtime/activity-code.md) -- base class selection and property types + +--- + +## Version Detection Mechanism + +Orchestrator version is detected by sending a `HEAD` request to `/odata/$metadata` and parsing the `api-supported-versions` response header. The result is cached globally (one detection per runtime session). + +```csharp +// OrchestratorVersion.cs -- called once at runtime, cached globally +public async Task InitVersionAsync(HttpClient client, string orchestratorUrl) +{ + var response = await client.SendAsync( + new HttpRequestMessage(HttpMethod.Head, orchestratorUrl + "/odata/$metadata")); + var apiVersions = response.Headers.GetValues("api-supported-versions").First(); + _apiVersions = apiVersions.Split(',').Select(Version.Parse).ToList(); +} + +public bool SupportsVersion(Version version) + => _apiVersions is not null && _apiVersions.Count != 0 + && _apiVersions.Min() >= version; +``` + +Note: `SupportsVersion` checks that the **minimum** supported API version is at least the requested version. This means the Orchestrator supports all versions from `Min` up to `Max` in its declared range. + +--- + +## Predefined Orchestrator Versions + +Key version thresholds defined in `OrchestratorVersion.cs`: + +```csharp +public static readonly Version ModernUnattendedRobotsOrchestratorVersion = new(10, 0); +public static readonly Version SetRobotAssetVersion = new(13, 0); +public static readonly Version BTSSupport = new(18, 0); +public static readonly Version CreatorUserKey = new(20, 0); +``` + +Reference these constants instead of hardcoding version numbers to keep version checks readable and centralized. + +--- + +## Version Check Patterns + +### Pattern 1: Declare minimum version (framework checks automatically) + +The simplest approach. Override `MinSupportedApiVersion` and the base class handles the rest in `EndExecute`. + +```csharp +public class BulkAddQueueItems : BaseOrchestratorClientActivity +{ + protected override Version MinSupportedApiVersion => new Version(8, 0); + + // Framework calls ThrowNotSupportedVersionIfNeeded() in EndExecute() +} +``` + +### Pattern 2: Check before execution + +Fail fast at the start of `BeginExecute` when the version requirement is known upfront. + +```csharp +protected override IAsyncResult BeginExecute( + AsyncCodeActivityContext context, AsyncCallback callback, object state) +{ + var version = OrchestratorService.Instance.GetOrchestratorVersion(context); + if (!version.SupportsVersion(new Version(20, 0))) + { + throw OrchestratorExceptionFactory + .CreateOrchestratorVersionNotSupportedError(new Version(20, 0)); + } + return base.BeginExecute(context, callback, state); +} +``` + +### Pattern 3: Branch logic by version + +Use different API paths depending on the connected Orchestrator version. + +```csharp +var orchestratorVersion = _orchestratorService.GetOrchestratorVersion(context); +if (orchestratorVersion.SupportsVersion(OrchestratorVersion.SetRobotAssetVersion)) +{ + await SetAssetValueAsync(client, assetName, value, ct); // v13.0+ API +} +else +{ + await SetAssetValueLegacyAsync(client, assetName, value, ct); // Pre-13.0 API +} +``` + +### Pattern 4: Strategy selection by version + +For complex multi-version support, use a strategy pattern to encapsulate version-specific logic. + +```csharp +// OrchestratorBroker selects different API strategies based on version +public IGetAssetStrategy GetRobotAssetStrategy() +{ + if (!_orchestratorVersion.SupportsVersion(new Version(7, 0))) + return new GetAssetWithQueryParameters(ctx); // Pre-7.0 + + if (_orchestratorVersion.SupportsVersion(new Version(18, 0))) + return new GetAssetWithProxySupport(ctx); // 18.0+ + + return new GetAssetWithBodyDefault(ctx); // 7.0-17.x +} +``` + +--- + +## Orchestrator API Call Flow + +``` +Activity.BeginExecute() + |-- Read arguments from context (before async) + |-- Get IWorkflowRuntime -> OrchestratorSettings + | |-- QueuesUrl / AssetsUrl / ConfigurationUrl + | |-- GetHeaders() -> auth/folder headers + | +-- RobotSettings.RobotId + |-- Create HttpClient with auth middleware stack: + | +-- HttpLoggingHandler -> RetryHandler -> BearerTokenHandler + |-- Set folder path header (Base64 UTF-16LE encoded): + | +-- x-uipath-folderpath-encoded: + |-- Send request (POST with JSON body) + +-- Parse response / handle errors +``` + +Important: Read all arguments from `context` **before** starting async work. The `AsyncCodeActivityContext` is only valid on the workflow thread, not inside `Task.Run` or after an `await`. + +--- + +## Folder Path Resolution + +Activities resolve the Orchestrator folder path using a priority-based header system: + +| Header | Priority | Description | +|--------|----------|-------------| +| `x-uipath-folderpath-encoded` | Highest | Base64 UTF-16LE encoded folder path | +| `x-uipath-folderpath` | 2nd | Plain text folder path | +| `x-uipath-folderkey` | 3rd | Folder key ID | +| `x-uipath-organizationunitid` | Lowest | Organization unit ID | + +The encoded header is preferred because folder names may contain characters that are not safe in HTTP headers. + +```csharp +// Encoding the folder path (uses UTF-16LE, not UTF-8) +internal static string ToBase64Header(string value) + => Convert.ToBase64String(Encoding.Unicode.GetBytes(value ?? string.Empty)); +``` + +**Caution**: `Encoding.Unicode` is UTF-16LE. Do not use `Encoding.UTF8` -- the Orchestrator expects UTF-16LE encoding for this header. + +--- + +## Common Orchestrator API Endpoints + +| Resource | Endpoint | +|----------|----------| +| Add queue item | `/odata/Queues/UiPathODataSvc.AddQueueItem` | +| Get transaction item | `/odata/Queues/UiPathODataSvc.GetTransactionItem` | +| Bulk add queue items | `/odata/Queues/UiPathODataSvc.BulkAddQueueItems` | +| Get asset (v7+) | `/odata/Assets/UiPath.Server.Configuration.OData.GetRobotAssetByNameForRobotKey` | +| Get asset (pre-v7) | `/odata/Assets/UiPath.Server.Configuration.OData.GetRobotAsset(robotId='{0}',assetName=@assetName)` | +| Start jobs | `/odata/Jobs/UiPath.Server.Configuration.OData.StartJobs` | + +Note: All endpoints are relative to the Orchestrator base URL obtained from `IOrchestratorSettings.ConfigurationUrl` or the specific resource URL (e.g., `QueuesUrl`). + +--- + +## Error Handling Pattern + +The `BaseOrchestratorClientActivity.EndExecute()` pattern provides two layers of error handling: version mismatch detection and `ContinueOnError` support. + +```csharp +// BaseOrchestratorClientActivity.EndExecute() pattern +protected override void EndExecute(AsyncCodeActivityContext context, IAsyncResult result) +{ + try + { + var task = (Task)result; + task.GetAwaiter().GetResult(); // Rethrow if faulted + } + catch (Exception ex) + { + // Check if error is due to version mismatch + ThrowNotSupportedVersionIfNeeded(context, ex); + + // ContinueOnError swallows the exception + if (!ContinueOnError.Get(context)) throw; + } +} + +private void ThrowNotSupportedVersionIfNeeded( + AsyncCodeActivityContext context, Exception ex) +{ + if (MinSupportedApiVersion == null) return; + + var version = _orchestratorService.GetOrchestratorVersion(context); + if (version.HasNetworkError()) return; // Can't determine version + + if (!version.SupportsVersion(MinSupportedApiVersion)) + { + throw OrchestratorExceptionFactory + .CreateOrchestratorVersionNotSupportedError(MinSupportedApiVersion, ex); + } +} +``` + +Key behaviors: +- `ThrowNotSupportedVersionIfNeeded` wraps the original exception with a more informative "Orchestrator version not supported" message when the root cause is a version mismatch. +- If the version cannot be determined (network error during detection), the original exception propagates unchanged. +- `ContinueOnError` is checked last, allowing version errors to always surface (they indicate a fundamental incompatibility, not a transient failure). + +--- + +## Writing Orchestrator-Integrated Activities Checklist + +When writing an activity that calls Orchestrator APIs: + +1. **Inherit from `BaseOrchestratorClientActivity`** to get built-in version checking, error handling, and HTTP client management. +2. **Override `MinSupportedApiVersion`** if the activity requires a specific Orchestrator version. +3. **Read context values in `BeginExecute`** before async work begins (the context is thread-bound). +4. **Use `OrchestratorSettings`** for API endpoint URLs and headers (see [Platform API - IOrchestratorSettings](../runtime/platform-api.md#iorchestratorsettings)). +5. **Use `AccessProvider`** for OAuth tokens instead of legacy auth. +6. **Handle version branching** with `SupportsVersion()` when supporting multiple Orchestrator versions. +7. **Register the activity in `ActivitiesBindings.json`** for resource binding support. + +--- + +## Troubleshooting + + diff --git a/.claude/activity-development-guide/runtime/platform-api.md b/.claude/activity-development-guide/runtime/platform-api.md new file mode 100644 index 000000000..3cc70d514 --- /dev/null +++ b/.claude/activity-development-guide/runtime/platform-api.md @@ -0,0 +1,299 @@ +# Platform API (Runtime and Design-Time Services) + +> **When to read this**: You need to interact with UiPath platform services from within an activity -- logging messages, reading project settings, obtaining OAuth tokens, checking robot/orchestrator configuration, or detecting available features at runtime or design time. + +**Related files**: +- [Activity Code](../runtime/activity-code.md) -- base classes and property types +- [Orchestrator Integration](../runtime/orchestrator.md) -- Orchestrator-specific API calls and version detection + +--- + +## API Package Hierarchy + +Activities interact with the UiPath platform through a layered set of NuGet packages. The source definitions live in the Studio repository. + +``` +UiPath.Activities.Api +|-- UiPath.Activities.Api.Base Base interfaces (all platforms) +| |-- IAccessProvider OAuth token management +| |-- IActivitiesSettingsReader Settings access +| |-- IDotnetRuntime .NET runtime info +| |-- IConfiguration Job-specific configuration +| +-- ITelemetryCustomDimensions Custom telemetry fields +| +|-- UiPath.Robot.Activities.Api Robot/Executor runtime APIs +| |-- IExecutorRuntime Main executor service +| |-- IRunningJobInformation Current job details +| |-- IWorkflowCommunication Workflow-to-client messaging +| |-- IProfilingCollector Performance profiling +| |-- LogMessage Logging DTO +| +-- ExecutorFeatureKeys Feature version constants +| +|-- UiPath.Activities.Contracts Core runtime contracts +| |-- IWorkflowRuntime Workflow-level services +| |-- IRobotSettings Robot configuration +| |-- IOrchestratorSettings Orchestrator URLs and headers +| |-- RuntimeFeatures Runtime feature constants +| +-- WorkflowParameters Workflow invocation DTOs +| ++-- UiPath.Studio.Activities.Api Design-time (Studio) APIs + |-- IWorkflowDesignApi Master design-time interface + |-- IDialogService Show dialogs + |-- IStudioBusyService Busy indicators + |-- IOrchestratorApiService Orchestrator access at design time + |-- IVariableService Variable management + |-- IExpressionService Expression analysis + |-- DesignFeatureKeys Design-time feature constants + +-- ... (50+ other design-time services) +``` + +--- + +## IExecutorRuntime (Robot Runtime Services) + +The primary runtime interface. Accessed via `context.GetExtension()`. + +```csharp +public interface IExecutorRuntime : IHasFeature +{ + IActivitiesSettingsReader Settings { get; } // Project settings reader + IAccessProvider AccessProvider { get; } // OAuth tokens + IRunningJobInformation RunningJobInformation { get; } // Job info + + void LogMessage(LogMessage message); // Runtime logging + + // Feature-gated services (check HasFeature before accessing) + IWorkflowCommunication WorkflowCommunication { get; } // Workflow <-> client messaging + IProfilingCollector ProfilingCollector { get; } // Performance profiling + IDotnetRuntime DotnetRuntime { get; } // .NET runtime info + IConfiguration Configuration { get; } // Extended job settings +} +``` + +### Logging + +```csharp +var runtime = context.GetExtension(); +runtime.LogMessage(new LogMessage +{ + EventType = TraceEventType.Information, + Message = "Processing item" +}); +``` + +### Reading project settings at runtime + +```csharp +if (runtime.Settings.TryGetValue("MyPackage.DefaultTimeout", out var timeout)) +{ + // Use the project-level setting +} +``` + +### Getting OAuth tokens for API calls + +```csharp +var token = await runtime.AccessProvider.GetAccessToken("openid", cancellationToken: ct); +var resourceUrl = await runtime.AccessProvider.GetResourceUrl("orchestrator", ct); +``` + +--- + +## IWorkflowRuntime (Workflow Execution Services) + +Accessed via `context.GetExtension()`. Provides orchestration-level services. + +```csharp +public interface IWorkflowRuntime +{ + // Configuration + IRobotSettings RobotSettings { get; } + IOrchestratorSettings OrchestratorSettings { get; } + + // Workflow management + IWorkflowExecutor CreateWorkflowExecutor(WorkflowParameters parameters); + void CancelWorkflow(Guid workflowInstanceId); + + // Logging + void Log(LogMessage message, Guid workflowInstanceId); + Task FlushLogs(); + + // Feature detection + bool HasFeature(string featureName); +} +``` + +### Accessing Orchestrator settings + +```csharp +var wfRuntime = context.GetExtension(); +var queuesUrl = wfRuntime.OrchestratorSettings.QueuesUrl; +var robotId = wfRuntime.RobotSettings.RobotId; +var headers = wfRuntime.OrchestratorSettings.GetHeaders(); +``` + +### Extension helper methods + +Defined in `Extensions.cs` for convenience: + +```csharp +context.GetWorkflowRuntime() // -> IWorkflowRuntime +context.GetRobotSettings() // -> IRobotSettings +context.GetOrchestratorSettings() // -> IOrchestratorSettings +context.GetExecutorRuntime() // -> IExecutorRuntime (custom helper) +``` + +--- + +## IRobotSettings + +```csharp +public interface IRobotSettings +{ + Guid RobotId { get; } // Robot identifier + string RobotName { get; } // Robot display name + bool ShouldStop { get; } // Stop requested? + IDictionary LogFields { get; } // Custom log fields + IReadOnlyCollection ReferencedPackages { get; } +} +``` + +--- + +## IOrchestratorSettings + +```csharp +public interface IOrchestratorSettings +{ + string ConfigurationUrl { get; } // Base Orchestrator URL + string QueuesUrl { get; } // Queue API endpoint + string MonitoringUrl { get; } // Monitoring endpoint + string ProcessName { get; } // Current process name + IReadOnlyDictionary GetHeaders(); // Auth/folder headers +} +``` + +--- + +## IRunningJobInformation + +Rich context about the current execution: + +```csharp +var jobInfo = executorRuntime.RunningJobInformation; + +jobInfo.JobId // Orchestrator job ID +jobInfo.ProcessName // Process name +jobInfo.ProcessVersion // Package version +jobInfo.ProjectDirectory // Local project path +jobInfo.TargetFramework // Legacy, Windows, Portable +jobInfo.InitiatedBy // "Orchestrator", "Studio", "Assistant"... +jobInfo.FolderId // Orchestrator folder ID +jobInfo.FolderName // Orchestrator folder name +jobInfo.TenantId // Tenant identifier +jobInfo.UserEmail // Executing user's email +jobInfo.RuntimeGovernanceEnabled // Governance policy active? +``` + +--- + +## IWorkflowDesignApi (Design-Time Services) + +The master design-time interface. Accessed in ViewModels via `Services.GetService()`. + +| Service | Purpose | +|---------|---------| +| `HasFeature(key)` | Feature detection | +| `AccessProvider` | OAuth tokens at design time | +| `ProjectPropertiesService` | Project configuration | +| `StudioDesignSettings` | Studio settings | +| `ActivitiesSettingsService` | Activity settings | +| `DialogService` | Show dialogs | +| `StudioBusyService` | Busy indicators | +| `OrchestratorApiService` | Orchestrator API access | +| `VariableService` | Variable management | +| `ExpressionService` | Expression analysis | +| `LibraryService` | Object Repository | +| `WizardsService` | Wizard dialogs | +| `ActivityTriggerService` | Trigger definitions | + +--- + +## Feature Detection Pattern + +Services are versioned. Always check feature availability before access to avoid `NullReferenceException` or `NotSupportedException` on older hosts. + +### Runtime feature check + +```csharp +var wfRuntime = context.GetExtension(); +if (wfRuntime?.HasFeature(RuntimeFeatures.FlushLogs) == true) +{ + await wfRuntime.FlushLogs(); +} +``` + +### Executor feature check + +```csharp +var executor = context.GetExtension(); +if (executor?.HasFeature(ExecutorFeatureKeys.AccessProviderV3) == true) +{ + var token = await executor.AccessProvider.GetAccessToken("scope", ct); +} +``` + +### Design-time feature check + +```csharp +var designApi = Services.GetService(); +if (designApi?.HasFeature(DesignFeatureKeys.ProjectPropertiesV2) == true) +{ + var props = designApi.ProjectPropertiesService; +} +``` + +**Pattern**: Always use `?.HasFeature(...) == true` (null-safe) because the extension may not be registered in all host environments (e.g., unit tests, older Studio versions). + +--- + +## RuntimeFeatures Constants + +```csharp +RuntimeFeatures.OrchestratorHeaders // Orchestrator auth headers available +RuntimeFeatures.FolderPath // Folder path supported +RuntimeFeatures.FlushLogs // Log flushing supported +RuntimeFeatures.CancelWorkflow // Workflow cancellation supported +RuntimeFeatures.UsePackage // Package invocation supported +RuntimeFeatures.InvokeProcess // Process invocation supported +RuntimeFeatures.CrossWorkflowProperties // Cross-workflow properties supported +RuntimeFeatures.ActivityTracing // Activity tracing supported +``` + +--- + +## ExecutorFeatureKeys Constants + +Feature versions tied to Robot/Executor releases: + +| Feature | Executor Version | +|---------|-----------------| +| Settings, AccessProvider | 19.8 | +| RunningJobInformation | 19.10 | +| WorkflowCommunication | 22.4 | +| Profiling | 22.6 | +| DotnetRuntime | 23.2 | +| Configuration | 24.4 | +| TelemetryCustomDimensions | 24.10 | + +Use this table to determine the minimum Robot version your activity requires. If you call `executor.AccessProvider`, your activity needs Robot 19.8+. Guard access with `HasFeature` for backward compatibility with older robots. + +--- + +## Troubleshooting + + diff --git a/.claude/activity-development-guide/testing/activity-testing.md b/.claude/activity-development-guide/testing/activity-testing.md new file mode 100644 index 000000000..192e9a9f1 --- /dev/null +++ b/.claude/activity-development-guide/testing/activity-testing.md @@ -0,0 +1,395 @@ +# Activity Testing + +> **When to read this**: You are writing or modifying tests for UiPath activities (not ViewModels). You need to know how to set up a test project, configure arguments, use WorkflowInvoker, mock runtime services, or follow testing best practices. For ViewModel-specific testing, see [viewmodel-testing.md](./viewmodel-testing.md). + +--- + +## Test Project Setup + +Every activity test project requires these NuGet references in the `.csproj`: + +```xml + + + + + + + + + +``` + +- **xunit** + **xunit.runner.visualstudio**: Test framework and runner. +- **Moq**: Mocking framework for interfaces (`IExecutorRuntime`, `IWorkflowRuntime`, etc.). +- **Shouldly**: Fluent assertions (`value.ShouldBe(expected)`), used alongside xUnit `Assert`. +- **UiPath.Activities.Api**: Provides `IExecutorRuntime`, `IWorkflowRuntime`, and other runtime contracts. +- **UiPath.Workflow**: Provides `WorkflowInvoker`, `CodeActivity`, `Sequence`, and WF4 types. + +--- + +## Setting InArgument/OutArgument Values in Tests + +Activities expose `InArgument` and `OutArgument` properties. In tests, set and read them as follows: + +```csharp +// Setting InArgument values (multiple syntax options) +activity.MyProp = new InArgument("literal value"); +activity.MyProp = new InArgument(ctx => "expression"); +activity.MyProp = InArgument.FromValue("literal"); +activity.MyProp = 42; // implicit conversion for simple types + +// Reading OutArgument values after execution +var result = WorkflowInvoker.Invoke(activity); +var output = result["OutputPropertyName"]; // key = property name +``` + +The result dictionary returned by `WorkflowInvoker.Invoke()` is keyed by the `OutArgument` property name on the activity class. + +--- + +## WorkflowInvoker: Variable<T> Limitation + +`Variable` requires an enclosing activity scope (`Variables` collection). `WorkflowInvoker` does not provide one, so using a `Variable`-backed `OutArgument` causes `InvalidWorkflowException`. + +```csharp +// WRONG — Variable without enclosing scope +var output = new Variable("result"); +var activity = new MyActivity { Output = new OutArgument(output) }; // throws! + +// CORRECT — bare OutArgument, read from Invoke() dictionary +var activity = new MyActivity +{ + Input = new InArgument("test"), + Output = new OutArgument() +}; + +var runner = new WorkflowInvoker(activity); +runner.Extensions.Add(() => _runtimeMock.Object); +var result = runner.Invoke(TimeSpan.FromSeconds(5)); + +// Key = the property name on the activity class +Assert.NotNull(result["Output"]); +``` + +--- + +## Three Levels of Activity Testing + +There are three levels of activity testing, from simplest to most comprehensive. Use all three where applicable. + +### Level 1: Unit Tests (Business Logic Only) + +Extract business logic into a public method (e.g., `ExecuteInternal`) and test it directly, without any workflow infrastructure. + +```csharp +public class CalculatorUnitTests +{ + [Theory] + [InlineData(1, Operation.Add, 1, 2)] + [InlineData(3, Operation.Subtract, 2, 1)] + [InlineData(3, Operation.Multiply, 2, 6)] + [InlineData(6, Operation.Divide, 2, 3)] + public void Calculator_ReturnsExpected(int a, Operation op, int b, int expected) + { + var calculator = new Calculator { SelectedOperation = op }; + var result = calculator.ExecuteInternal(a, b); + Assert.Equal(expected, result); + } + + [Fact] + public void Calculator_Divide_ThrowsOnZero() + { + var calculator = new Calculator { SelectedOperation = Operation.Divide }; + Assert.Throws(() => calculator.ExecuteInternal(4, 0)); + } +} +``` + +**When to use**: Always. Every activity should have unit tests for its core logic. + +### Level 2: WorkflowInvoker Tests (Activity Execution) + +Test the full activity execution in a workflow context using `WorkflowInvoker`. This validates argument binding, execution flow, and output mapping. + +```csharp +public class CalculatorWorkflowTests +{ + private readonly Mock _runtimeMock = new(); + + [Fact] + public void Divide_ReturnsExpected() + { + var activity = new Calculator + { + FirstNumber = 4, + SecondNumber = 2, + SelectedOperation = Operation.Divide + }; + + var runner = new WorkflowInvoker(activity); + runner.Extensions.Add(() => _runtimeMock.Object); + + var result = runner.Invoke(TimeSpan.FromSeconds(1)); + + Assert.Equal(2, result["Result"]); + _runtimeMock.Verify(x => x.LogMessage(It.IsAny()), Times.Once); + } + + [Fact] + public void Divide_ThrowsOnZero() + { + var activity = new Calculator + { + FirstNumber = 4, + SecondNumber = 0, + SelectedOperation = Operation.Divide + }; + + var runner = new WorkflowInvoker(activity); + runner.Extensions.Add(() => _runtimeMock.Object); + + Assert.Throws(() => runner.Invoke(TimeSpan.FromSeconds(1))); + } +} +``` + +**When to use**: For all activities. Verifies the activity works correctly in a workflow context. + +Key points: +- Always pass a `TimeSpan` timeout to `runner.Invoke()` to prevent hanging tests. +- Register extensions via factory lambda (`() => mock.Object`) for lazy resolution. +- Verify `IExecutorRuntime.LogMessage` calls to confirm the activity logs correctly. + +### Level 3: WorkflowInvoker with Service Mocks (Integration) + +For activities that interact with external services (Orchestrator, APIs, HTTP clients), mock the service interfaces and register them as workflow extensions. + +```csharp +public class GetAssetTests +{ + [Theory] + [InlineData(true, "test name", "Force")] + [InlineData(false, "Kane", true)] + public void GetAssetRequest(bool supportVersion, string assetName, object assetValue) + { + // Mock Orchestrator services + var httpService = new Mock(); + httpService.Setup(a => a.Create(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(new HttpClient()); + + var robotSettings = new Mock(); + robotSettings.Setup(a => a.RobotId).Returns(Guid.Empty); + + var orchestratorSettings = new Mock(); + orchestratorSettings.Setup(a => a.QueuesUrl).Returns("http://someUrl"); + + // Mock workflow runtime (provides settings to activity) + var workflowRuntime = new Mock(); + workflowRuntime.SetupGet(w => w.RobotSettings).Returns(robotSettings.Object); + workflowRuntime.SetupGet(w => w.OrchestratorSettings).Returns(orchestratorSettings.Object); + + // Create and run activity + var activity = new GetRobotAsset { AssetName = assetName }; + var runner = new WorkflowInvoker(activity); + runner.Extensions.Add(workflowRuntime.Object); + + var result = runner.Invoke(TimeSpan.FromSeconds(10)); + + // Verify interactions + httpService.Verify(a => a.Create( + It.IsAny(), It.IsAny(), expectedFolderPath), Times.Once); + } +} +``` + +**When to use**: For activities that depend on Orchestrator, HTTP services, or other external systems. Mock every external dependency and verify the interactions. + +--- + +## Testing with IWorkflowExecutor (Invoke Activities) + +Activities that invoke other workflows (`InvokeWorkflowFile`, `InvokeProcess`) use `IWorkflowExecutor`. Mock its async begin/end pattern: + +```csharp +[Theory] +[MemberData(nameof(GetInvokeActivities))] +public void ExecutesAndReturnsResults(ExecutorInvokeActivity activity) +{ + activity.Arguments = new Dictionary + { + { "OutArg1", new OutArgument() }, + { "InArg1", new InArgument("inputValue") } + }; + + Mock executorMock = null; + + var runtimeMock = new Mock(); + runtimeMock.Setup(a => a.HasFeature(It.IsAny())).Returns(true); + runtimeMock.Setup(a => a.CreateWorkflowExecutor(It.IsAny())) + .Returns(p => + { + executorMock = new Mock(); + executorMock.Setup(e => e.BeginExecute(It.IsAny(), It.IsAny())) + .Returns((cb, st) => + { + var tcs = new TaskCompletionSource(st); + tcs.TrySetResult(true); + Task.Run(() => cb(tcs.Task)); + return tcs.Task; + }); + executorMock.Setup(e => e.EndExecute(It.IsAny())) + .Returns(new Dictionary { { "OutArg1", "result" } }); + return executorMock.Object; + }); + + var runner = new WorkflowInvoker(activity); + runner.Extensions.Add(() => runtimeMock.Object); + var result = runner.Invoke(TimeSpan.FromSeconds(5)); + + executorMock.Verify(e => e.EndExecute(It.IsAny()), Times.Once); +} +``` + +Key details: +- `IWorkflowExecutor` uses the APM (Asynchronous Programming Model) pattern with `BeginExecute`/`EndExecute`. +- The `TaskCompletionSource` simulates async completion synchronously in the test. +- `Task.Run(() => cb(tcs.Task))` ensures the callback fires on a separate thread, matching real runtime behavior. + +--- + +## Test Helper Extension Methods + +Create reusable helpers to reduce boilerplate across test classes: + +```csharp +public static class ActivityExtensions +{ + public static IDictionary TestActivity( + this Activity activity, Action setup = null) + { + var runner = new WorkflowInvoker(activity); + setup?.Invoke(runner); + return runner.Invoke(); + } + + public static IDictionary TestActivity( + this Activity activity, IExecutorRuntime runtime, IWorkflowRuntime wfRuntime) + { + return activity.TestActivity(runner => + { + runner.Extensions.Add(runtime); + runner.Extensions.Add(wfRuntime); + }); + } + + public static Sequence AddActivity(this Sequence sequence, Activity activity) + { + sequence.Activities.Add(activity); + return sequence; + } +} +``` + +These helpers let you write concise tests: + +```csharp +var result = myActivity.TestActivity(runner => +{ + runner.Extensions.Add(() => runtimeMock.Object); +}); +``` + +--- + +## Testing Multiple Activities in a Sequence + +To test that multiple activities execute in order and interact correctly, compose them into a `Sequence`: + +```csharp +[Fact] +public void MultipleActivities_ExecuteInOrder() +{ + var activity1 = new MyActivity { Input = "first" }; + var activity2 = new MyActivity { Input = "second" }; + + var workflow = new Sequence() + .AddActivity(activity1) + .AddActivity(activity2); + + var ex = Record.Exception(() => workflow.TestActivity()); + ex.ShouldBeNull(); + + _serviceMock.Verify(x => x.Process("first"), Times.Once); + _serviceMock.Verify(x => x.Process("second"), Times.Once); +} +``` + +This uses the `AddActivity` extension method and `TestActivity` helper defined above. + +--- + +## Tracking Activity Execution with Test Helpers + +For tests that need to observe execution count or captured values, create a tracking test activity: + +```csharp +// Helper activity that tracks execution +public class TestActivity : CodeActivity +{ + public int ExecutionCount { get; set; } + public List ReceivedValues { get; } = new(); + public InArgument ValueIn { get; set; } + + protected override void Execute(CodeActivityContext context) + { + ExecutionCount++; + ReceivedValues.Add(context.GetValue(ValueIn)); + } +} +``` + +Use this in tests to verify how many times an activity was executed and what values it received: + +```csharp +var tracker = new TestActivity(); +// ... add to workflow, execute ... +tracker.ExecutionCount.ShouldBe(3); +tracker.ReceivedValues.ShouldContain("expected"); +``` + +--- + +## Testing Best Practices + +1. **Always test business logic separately** via `ExecuteInternal()` methods -- no workflow infrastructure needed. +2. **Use `WorkflowInvoker` with timeouts** -- prevent hanging tests: `runner.Invoke(TimeSpan.FromSeconds(5))`. +3. **Register extensions via factory** -- `runner.Extensions.Add(() => mock.Object)` for lazy resolution. +4. **Mock `IExecutorRuntime`** to verify logging: `_runtimeMock.Verify(x => x.LogMessage(...), Times.Once)`. +5. **Mock `IViewModelDispatcher`** in ViewModel tests to handle `Dispatcher.Invoke()` calls synchronously. +6. **Use `AcceptChangesAsync()`** after property changes to trigger rules and validate results. +7. **Prefer `[Theory]` with `[InlineData]`** for parameterized tests, `[MemberData]` for complex data. +8. **Test error paths** -- divide by zero, null inputs, missing required properties, timeout scenarios. +9. **For SDK activities**, inherit from `BaseViewModelUnitTest` -- it handles all the boilerplate. +10. **Use `Shouldly`** assertions (`value.ShouldBe(expected)`) for readable test output alongside xUnit `Assert`. + +--- + +## Cross-References + +- [ViewModel Testing](./viewmodel-testing.md) -- testing approaches for activity ViewModels and design-time behavior. +- [Activity Anatomy](../core/activity-anatomy.md) -- how activities are structured (CodeActivity, AsyncCodeActivity, NativeActivity). +- [ViewModel Patterns](../design/viewmodel-patterns.md) -- ViewModel implementation patterns that inform what to test. + +--- + +## Troubleshooting + + + +### InvalidWorkflowException with Variable in WorkflowInvoker + +**Symptom**: `InvalidWorkflowException: The following errors were encountered while processing the workflow tree` when using `new OutArgument(variable)` in a WorkflowInvoker test. +**Cause**: `Variable` must be declared in an enclosing activity's `Variables` collection. `WorkflowInvoker` doesn't provide one. +**Fix**: Use bare `new OutArgument()` and read the result from the dictionary returned by `Invoke()` using the property name as key. diff --git a/.claude/activity-development-guide/testing/viewmodel-testing.md b/.claude/activity-development-guide/testing/viewmodel-testing.md new file mode 100644 index 000000000..d24391050 --- /dev/null +++ b/.claude/activity-development-guide/testing/viewmodel-testing.md @@ -0,0 +1,298 @@ +# ViewModel Testing + +> **When to read this**: You are writing or modifying tests for UiPath activity ViewModels. You need to know how to set up a test for design-time behavior, mock `ModelItem`, use `BaseViewModelUnitTest`, or test property configuration, rules, data sources, and validation. For activity runtime testing, see [activity-testing.md](./activity-testing.md). + +--- + +## Overview + +There are four approaches to testing ViewModels, ranging from the fully automated SDK base class to manual mocking for legacy patterns. Choose the approach that matches your ViewModel's base class. + +| Approach | Base Class | When to Use | +|---|---|---| +| 1. `BaseViewModelUnitTest` | SDK `DesignPropertiesViewModel` | Recommended for all SDK activities | +| 2. Manual ModelItem mocking | Non-SDK `DesignPropertiesViewModel` | System activities without SDK base classes | +| 3. `ModelTreeManager` + `EditingContext` | Any (designer tests) | Complex model tree manipulation, legacy designer | +| 4. Direct initialization | Any | Testing specific rule behaviors or property interactions | + +--- + +## Approach 1: BaseViewModelUnitTest (SDK Activities) -- Recommended + +The SDK provides `BaseViewModelUnitTest` which handles `ModelTreeManager` setup, activity/ViewModel creation, and initialization. This is the recommended approach for SDK-based activities. + +```csharp +public class DepositInAccountViewModelTests : BaseViewModelUnitTest +{ + private readonly Mock _bankMock = new(); + private readonly IServiceCollection _services = new ServiceCollection(); + + public DepositInAccountViewModelTests() + { + _services.AddSingleton(_bankMock.Object); + } + + [Fact] + public async Task Initialization_SetsPropertyConfiguration() + { + var vm = await CreateAndSetupViewModelAsync( + _services.BuildServiceProvider()); + + vm.AccountHolder.IsPrincipal.ShouldBeTrue(); + vm.Deposit.IsRequired.ShouldBeTrue(); + vm.Result.IsPrincipal.ShouldBeFalse(); + } + + [Fact] + public async Task DataSource_PopulatesFromService() + { + var activity = CreateActivity(); + var vm = await CreateAndSetupViewModelAsync(activity, + new DepositInAccountViewModel(ServicesMock.Object, _services.BuildServiceProvider())); + + vm.AccountHolder.Value = "clientName"; + await AcceptChangesAsync(vm); + + // Trigger dynamic data source + var ds = await vm.Account.GetService() + .GetDynamicDataSourceAsync(string.Empty, int.MaxValue, 0); + vm.Account.DataSource.Items.Count.ShouldBe(2); + } + + [Fact] + public async Task Rule_UpdatesDependentProperties() + { + var activity = CreateActivity(); + var vm = await CreateAndSetupViewModelAsync(activity, + new DepositInAccountViewModel(ServicesMock.Object, _services.BuildServiceProvider())); + + // Select account from data source + vm.Account.Value = someAccountMetadata; + await AcceptChangesAsync(vm); + + // Verify rule updated dependent properties + activity.Currency.Value.ShouldBe(Currency.USD); + activity.AccountType.Value.ShouldBe(AccountType.Current); + } + + [Fact] + public async Task Validation_ShowsErrorWhenRequired() + { + var vm = await CreateAndSetupViewModelAsync( + _services.BuildServiceProvider()); + + // Don't set required property + vm.ValidationErrors.ShouldHaveSingleItem(); + vm.ValidationErrors.First().MemberNames.First() + .ShouldBe(nameof(DepositInAccountViewModel.Account)); + } +} +``` + +### What BaseViewModelUnitTest Provides + +The base class exposes the following members and methods: + +| Member | Description | +|---|---| +| `CreateActivity()` | Creates an activity instance and adds it to the model tree | +| `CreateViewModel()` | Creates a ViewModel with mocked services | +| `CreateAndSetupViewModelAsync()` | Creates the ViewModel, sets `ModelItem`, and calls `InitializeAsync` | +| `AcceptChangesAsync()` | Applies pending changes and triggers rules | +| `CreateViewModelService()` | Registers a mock service in `IDesignServices` | +| `ServicesMock` | Pre-configured `Mock` with common services registered | +| `Mtm` | `ModelTreeManager` instance for direct model item manipulation | + +Typical test flow: +1. Call `CreateActivity()` or let `CreateAndSetupViewModelAsync` create it automatically. +2. Call `CreateAndSetupViewModelAsync()` to get an initialized ViewModel. +3. Set property values on the ViewModel. +4. Call `AcceptChangesAsync()` to trigger rules. +5. Assert property states, validation errors, or activity values. + +--- + +## Approach 2: Manual ModelItem Mocking (Non-SDK ViewModels) + +For ViewModels that inherit directly from `DesignPropertiesViewModel` (not via the SDK), mock `ModelItem` and `IDesignServices` manually. + +```csharp +public class EvaluateBusinessRuleViewModelTests +{ + [Fact] + public async Task ViewModel_CreatesSuccessfully() + { + CreateViewModel(out var activityValues, out var model); + Assert.NotNull(model); + } + + private void CreateViewModel(out List activityValues, + out EvaluateBusinessRuleViewModel model) + { + // Mock IWorkflowDesignApi and its sub-services + var workflowDesignApi = new Mock(); + workflowDesignApi.Setup(s => s.HasFeature(DesignFeatureKeys.WorkflowOperations)).Returns(true); + workflowDesignApi.Setup(s => s.WorkflowOperationsService) + .Returns(new Mock().Object); + + // Mock access provider + var tokenProvider = new Mock(); + tokenProvider.Setup(s => s.GetResourceUrl(It.IsNotNull())) + .ReturnsAsync((string scope) => $"localhost://{scope}/resource"); + + // Mock dispatcher (required for UI thread operations) + var dispatcher = new Mock(); + dispatcher.Setup(s => s.Invoke(It.IsAny())).Callback(a => a()); + + var dispatcherFactory = new Mock(); + dispatcherFactory.Setup(s => s.CreateDispatcher(It.IsAny())) + .Returns(dispatcher.Object); + + // Assemble IDesignServices + var designServices = new Mock(); + designServices.Setup(s => s.GetService()) + .Returns(workflowDesignApi.Object); + designServices.Setup(s => s.GetService()) + .Returns(tokenProvider.Object); + designServices.Setup(s => s.GetService()) + .Returns(dispatcherFactory.Object); + + activityValues = null; + model = new EvaluateBusinessRuleViewModel(designServices.Object); + } +} +``` + +**When to use**: For ViewModels in System activities that do not use the SDK base classes. + +Key mocking requirements: +- `IWorkflowDesignApi` -- feature flags and sub-services. +- `IAccessProvider` -- token/URL resolution for activities that call external services. +- `IViewModelDispatcher` / `IViewModelDispatcherFactory` -- required for any ViewModel that dispatches to the UI thread. Mock `Invoke()` to execute the action synchronously. +- `IDesignServices` -- the service locator that ties everything together. + +--- + +## Approach 3: ModelTreeManager with EditingContext (Designer Tests) + +For testing ViewModels that manipulate the model tree directly (e.g., collection editors, dynamic argument handling), create a real `ModelTreeManager`: + +```csharp +public class ZipFilesViewModelTests +{ + private readonly ZipFilesViewModel _viewModel; + private readonly CompressFiles _activity; + private readonly ModelTreeManager _mtm; + + public ZipFilesViewModelTests() + { + _activity = new CompressFiles(); + _mtm = new ModelTreeManager(new EditingContext()); + _mtm.Load(_activity); + _viewModel = new ZipFilesViewModel(); + } + + [Fact] + public void ShouldSetFilesArgument() + { + _viewModel.SetModelItem(_mtm.Root); + _activity.ContentToArchive.ShouldNotBeNull(); + } + + [Fact] + public void ShouldAddNewEmptyArgumentIfPreviousOneWasCompleted() + { + _viewModel.SetModelItem(_mtm.Root); + _viewModel.FileModelItems[0].Properties["Argument"]? + .SetValue(new InArgument("test")); + _viewModel.FileModelItems.Count.ShouldBe(2); + } + + [Fact] + public void PropertyChange_RaisesNotification() + { + _viewModel.SetModelItem(_mtm.Root); + + var propertyName = string.Empty; + _viewModel.PropertyChanged += (_, args) => propertyName = args.PropertyName; + _viewModel.AttachFolders = true; + + propertyName.ShouldBe(nameof(_viewModel.AttachFolders)); + } +} +``` + +**When to use**: For ViewModels with complex model tree manipulation or legacy designer patterns. + +Key details: +- `new ModelTreeManager(new EditingContext())` creates a real model tree (not mocked). +- `_mtm.Load(_activity)` loads the activity into the tree, making `_mtm.Root` available. +- `_viewModel.SetModelItem(_mtm.Root)` connects the ViewModel to the model tree. +- This approach lets you test `PropertyChanged` notifications, collection manipulation, and model item property changes. + +--- + +## Approach 4: Direct Initialization with Mocked ModelItem + +For testing specific ViewModel behaviors (visibility rules, property changes) without full initialization, mock only the `ModelItem` properties you need: + +```csharp +[Fact] +public async Task InvokeWorkflowViewModel_ArgumentsVariableInit_ShouldBeVisible() +{ + // Mock specific ModelItem properties + var mockProp = new Mock(); + mockProp.Setup(s => s.Name).Returns(nameof(InvokeWorkflowFile.ArgumentsVariable)); + mockProp.Setup(m => m.ComputedValue).Returns(mockReference); + + var mockModelItem = new Mock(); + mockModelItem.Setup(s => s.Properties) + .Returns(new CustomPropCollection(mockProp.Object)); + + // Create and initialize ViewModel + CreateViewModel(out var activityValues, out var model); + model.ModelItem = mockModelItem.Object; + await model.InitializeAsync(activityValues); + + // Assert rule results + Assert.True(model.ArgumentsVariable.IsVisible); + Assert.False(model.Arguments.IsVisible); +} +``` + +**When to use**: When testing specific rule behaviors or property interactions where full model tree setup is unnecessary. + +Key details: +- Mock only the `ModelProperty` instances you need via `Mock`. +- Use `CustomPropCollection` (or equivalent) to return those properties from `ModelItem.Properties`. +- Set `ModelItem` directly on the ViewModel, then call `InitializeAsync`. +- This approach is fast but less realistic than Approaches 1-3. + +--- + +## What to Test for ViewModels + +| Aspect | How to Test | +|---|---| +| Property configuration | Assert `IsPrincipal`, `IsRequired`, `IsVisible`, `OrderIndex` after init | +| Widget assignment | Assert `property.Widget.Type` matches expected widget | +| Rules execution | Set triggering property, call `AcceptChangesAsync`, assert results | +| Visibility toggling | Set mode property, verify dependent properties' `IsVisible` | +| DataSource population | Trigger data source, assert `DataSource.Items.Count` | +| Validation | Leave required properties empty, assert `ValidationErrors` | +| Menu actions | Verify menu actions registered with correct display names | +| Property sync to activity | Change VM property, call `AcceptChangesAsync`, read activity property | + +--- + +## Cross-References + +- [Activity Testing](./activity-testing.md) -- runtime testing with WorkflowInvoker, service mocks, and test helpers. +- [ViewModel Patterns](../design/viewmodel-patterns.md) -- ViewModel implementation patterns that inform what to test. +- [Activity Anatomy](../core/activity-anatomy.md) -- how activities are structured, relevant for understanding what the ViewModel wraps. + +--- + +## Troubleshooting + + diff --git a/.claude/skills/create-activity/SKILL.md b/.claude/skills/create-activity/SKILL.md new file mode 100644 index 000000000..8fb575b38 --- /dev/null +++ b/.claude/skills/create-activity/SKILL.md @@ -0,0 +1,1035 @@ +--- +name: create-activity +description: > + Generate complete, deployable UiPath activity packages. Detects SDK presence + and produces all necessary files (activity, viewmodel, metadata, packaging, + tests). Use when asked to create or scaffold UiPath activities. +allowed-tools: Bash, Glob, Grep, Read, Write, Edit, AskUserQuestion +--- + +# UiPath Activity Development — Claude Code Skill + +> This skill file enables Claude Code to generate complete, deployable UiPath activity packages. +> For detailed reference on widgets, data sources, rules, bindings, advanced patterns, and testing, +> see the companion guide: [`activity-development-guide/index.md`](../../activity-development-guide/index.md). + +--- + +## When to Use This Skill + +Use this skill when asked to: +- Create a new UiPath activity (or activity package) +- Add a new activity to an existing UiPath activity package +- Scaffold the project structure for UiPath activities + +--- + +## Before You Generate — Resolve Package Name + +Before generating any file, determine `{PackageName}` and `{Namespace}`: + +**Step 1 — Infer from the prompt.** +If the user's request contains a dotted name (e.g. "AlexPetre.ConvertXml"), extract +the full dotted name as `{PackageName}` and use it as `{Namespace}` too. + +| User writes | {PackageName} | {Namespace} | +|-------------|---------------|-------------| +| "AlexPetre.ConvertXml activity" | `AlexPetre.ConvertXml` | `AlexPetre.ConvertXml` | +| "create Acme.Utils.Csv activity" | `Acme.Utils.Csv` | `Acme.Utils.Csv` | +| "ConvertXml activity" (no dots) | ask — see Step 2 | ask — see Step 2 | + +**Step 2 — Ask if no dotted name is found.** +If the prompt contains no dotted name, stop and ask: + +> What should the package be called? (e.g. YourName.ConvertXml) + +Use the user's full answer as `{PackageName}` and `{Namespace}`. + +**Step 3 — Normalize to PascalCase.** +If the name is not already PascalCase, convert it: capitalize the first letter of each word and remove spaces/hyphens. + +**Rule: `{PackageName}` MUST NOT start with `UiPath`.** Never generate a package whose name begins with `UiPath`. + +--- + +## SDK Detection + +**Before generating any files**, check if the UiPath Activities SDK is present in the repository: + +``` +Glob for **/UiPath.Sdk.Activities/*.cs or **/UiPath.Sdk.Activities.projitems +``` + +- **If found -> SDK mode** (preferred): Use the SDK base classes and patterns described in the **SDK Mode Overrides** section below. The SDK provides dependency injection, telemetry, governance, project settings, retry, and connection binding support out of the box. +- **If not found -> Classic mode**: Use the standard templates below. + +--- + +## File Generation Checklist + +### Classic Mode + +When creating a **new activity package from scratch**, generate ALL of these files in order: + +| # | File | Purpose | +|---|------|---------| +| 1 | `nuget.config` | NuGet feed configuration | +| 2 | `{PackageName}/{PackageName}.csproj` | Main library project | +| 3 | `{PackageName}/Activities/{ActivityName}.cs` | Runtime activity class | +| 4 | `{PackageName}/ViewModels/{ActivityName}ViewModel.cs` | Design-time ViewModel | +| 5 | `{PackageName}/Helpers/ActivityContextExtensions.cs` | Runtime logging helper | +| 6 | `{PackageName}/Resources/ActivitiesMetadata.json` | Activity discovery metadata | +| 6b | `{PackageName}/Resources/Icons/activityicon.svg` | Default activity icon (SVG 24x24) | +| 7 | `{PackageName}/Resources/Resources.resx` | Localized display strings | +| 7b | `{PackageName}/Resources/Resources.Designer.cs` | Strongly-typed resource accessor | +| 8 | `{PackageName}.Packaging/{PackageName}.Packaging.csproj` | NuGet packaging project | +| 9 | `{PackageName}.Tests/{PackageName}.Tests.csproj` | Test project | +| 10 | `{PackageName}.Tests/Unit/{ActivityName}UnitTests.cs` | Unit tests | +| 11 | `{PackageName}.Tests/Workflow/{ActivityName}WorkflowTests.cs` | Workflow integration tests | + +Then run these commands to create the solution and build: + +```bash +dotnet new sln -n {PackageName} +dotnet sln add {PackageName}/{PackageName}.csproj +dotnet sln add {PackageName}.Packaging/{PackageName}.Packaging.csproj +dotnet sln add {PackageName}.Tests/{PackageName}.Tests.csproj +dotnet build -c Release +dotnet test +``` + +When **adding an activity to an existing package**, generate only files 3, 4, 10, 11 and update files 6, 7, and 7b. + +### SDK Mode + +When the SDK is detected, the checklist changes. Differences from classic mode are marked with **[SDK]**. + +| # | File | Purpose | +|---|------|---------| +| 1 | `nuget.config` | NuGet feed configuration (same as classic) | +| 2 | `{PackageName}/{PackageName}.csproj` | Main library project **[SDK]** — extra properties & packages | +| 3 | `{PackageName}/Activities/{ActivityName}.cs` | Runtime activity class **[SDK]** — `partial`, inherits `SdkActivity` | +| 3b | `{PackageName}/ViewModels/{ActivityName}.Design.cs` | **[SDK NEW]** — `partial` class with `[ViewModelClass]` attribute | +| 4 | `{PackageName}/ViewModels/{ActivityName}ViewModel.cs` | Design-time ViewModel **[SDK]** — inherits `BaseViewModel` | +| -- | ~~`ActivityContextExtensions.cs`~~ | **[SDK]** — NOT needed (services via `RuntimeServices`) | +| 5 | `{PackageName}/Directory.build.targets` | **[SDK NEW]** — imports SDK build targets | +| 6 | `{PackageName}/Resources/ActivitiesMetadata.json` | Activity discovery metadata (same as classic) | +| 6b | `{PackageName}/Resources/Icons/activityicon.svg` | Default activity icon (same as classic) | +| 7 | `{PackageName}/Resources/Resources.resx` | Localized display strings (same as classic) | +| 7b | `{PackageName}/Resources/Resources.Designer.cs` | Strongly-typed resource accessor (same as classic) | +| 8 | `{PackageName}.Packaging/{PackageName}.Packaging.csproj` | NuGet packaging project (same as classic) | +| 9 | `{PackageName}.Tests/{PackageName}.Tests.csproj` | Test project **[SDK]** — extra properties | +| 10 | `{PackageName}.Tests/Unit/{ActivityName}UnitTests.cs` | Unit tests **[SDK]** — DI-based testing | +| 11 | `{PackageName}.Tests/Workflow/{ActivityName}WorkflowTests.cs` | Workflow integration tests (same as classic) | + +When **adding an activity to an existing SDK package**, generate files 3, 3b, 4, 10, 11 and update files 6, 7, and 7b. + +### Common Notes + +**`Resources.Designer.cs` — generate and commit for CLI builds:** `PublicResXFileCodeGenerator` only runs inside Visual Studio. For `dotnet build` and CI builds, `Resources.Designer.cs` must be generated and committed to source (see Template 7b). Visual Studio will regenerate it automatically when the project is opened; that is expected and harmless. + +For more details, see [`core/project-structure.md`](../../activity-development-guide/core/project-structure.md). + +--- + +## File Templates + +### 1. nuget.config + +```xml + + + + + + + +``` + +### 2. Main Project .csproj + +```xml + + + Library + net8.0 + enable + enable + + {PackageName}.Library + false + + {PackageName} + $(NoWarn);NU5104 + + + + + + + + + + + + + + + + + + Resources.resx + True + True + {Namespace} + + + + + Resources.Designer.cs + PublicResXFileCodeGenerator + {Namespace} + + + +``` + +### 3. Activity Class + +```csharp +// Activities/{ActivityName}.cs +using System.Activities; +using System.ComponentModel; +using System.Diagnostics; +using {Namespace}.Helpers; +using UiPath.Robot.Activities.Api; + +namespace {Namespace}; + +public class {ActivityName} : CodeActivity{ if applicable} +{ + [RequiredArgument] + public InArgument? {InputProp} { get; set; } + + public OutArgument? {OutputProp} { get; set; } + + // Enum selector: plain TEnum, NOT InArgument — see activity-code.md + public TEnum {EnumProp} { get; set; } = TEnum.DefaultValue; + + protected override {ReturnType} Execute(CodeActivityContext context) + { + context.GetExecutorRuntime().LogMessage(new LogMessage + { + EventType = TraceEventType.Information, + Message = "Executing {ActivityName} activity" + }); + + var input = {InputProp}.Get(context); + var result = ExecuteInternal(input); + {OutputProp}?.Set(context, result); + return result; // if CodeActivity + } + + // ExecuteInternal reads enum property directly — not via parameter or private field. + // See activity-development-guide/runtime/activity-code.md for the ExecuteInternal pattern. + public {ReturnType} ExecuteInternal({params}) + { + return {EnumProp} switch + { + // Business logic here + }; + } +} +``` + +**Classic mode base class selection** (for SDK mode, see SDK Mode Overrides section): + +| Base Class | When to Use | +|---|---| +| `CodeActivity` | Simple synchronous activities returning a value | +| `CodeActivity` | Simple synchronous activities with no return value | +| `AsyncCodeActivity` | Async activities (I/O, network calls) | + +**Property type mapping:** + +| C# Type | Purpose | +|---|---| +| `InArgument?` | Input — accepts expressions/variables | +| `OutArgument?` | Output — writes to variables | +| `InOutArgument?` | Bidirectional — read and modify | +| `TEnum` (plain type) | Enum selector — maps to `DesignProperty` in ViewModel | +| `T` (direct type) | Other constants (bool, int, etc.) | + +For details on enum selectors, the ExecuteInternal pattern, and GetExecutorRuntime, see [`runtime/activity-code.md`](../../activity-development-guide/runtime/activity-code.md). + +### 4. ViewModel Class + +Property names **MUST exactly match** the Activity class property names. + +```csharp +// ViewModels/{ActivityName}ViewModel.cs +using System.Activities.DesignViewModels; + +namespace {Namespace}.ViewModels; + +public class {ActivityName}ViewModel : DesignPropertiesViewModel +{ + // Nullable without initializer — framework populates before InitializeModel + public DesignInArgument? {InputProp} { get; set; } + public DesignOutArgument? {OutputProp} { get; set; } + public DesignProperty? {EnumProp} { get; set; } + + public {ActivityName}ViewModel(IDesignServices services) : base(services) { } + + protected override void InitializeModel() + { + base.InitializeModel(); + PersistValuesChangedDuringInit(); + + var order = 0; + + // Use ! on every nullable property access — framework guarantees non-null here + {InputProp}!.DisplayName = Resources.{ActivityName}_{InputProp}_DisplayName; + {InputProp}!.Tooltip = Resources.{ActivityName}_{InputProp}_Tooltip; + {InputProp}!.IsRequired = true; + {InputProp}!.IsPrincipal = true; + {InputProp}!.OrderIndex = order++; + + // Enum properties — runtime auto-renders enums as dropdowns + {EnumProp}!.DisplayName = Resources.{ActivityName}_{EnumProp}_DisplayName; + {EnumProp}!.IsPrincipal = true; + {EnumProp}!.OrderIndex = order++; + + // Output properties — not principal, at the end + {OutputProp}!.DisplayName = Resources.{ActivityName}_{OutputProp}_DisplayName; + {OutputProp}!.Tooltip = Resources.{ActivityName}_{OutputProp}_Tooltip; + {OutputProp}!.OrderIndex = order++; + } +} +``` + +**ViewModel property type mapping:** + +| ViewModel Type | Maps to Activity Type | `.Widget` supported | +|---|---|---| +| `DesignInArgument` | `InArgument` | No | +| `DesignOutArgument` | `OutArgument` | No | +| `DesignInOutArgument` | `InOutArgument` | No | +| `DesignProperty` | Direct `T` property | **Yes** | + +**Common widget assignments** (set in `InitializeModel()`): + +```csharp +// Toggle for booleans (only on DesignProperty, not DesignInArgument) +BoolProp!.Widget = new DefaultWidget { Type = ViewModelWidgetType.Toggle }; + +// Multi-line text +TextProp!.Widget = new DefaultWidget +{ + Type = ViewModelWidgetType.TextComposer, + Metadata = new() { { TextComposerMetadata.IsSingleLineFormat, true.ToString() } } +}; + +// Autocomplete dropdown (searchable, allows expressions) +SearchProp!.Widget = new DefaultWidget { Type = ViewModelWidgetType.AutoCompleteForExpression }; + +// Plain number with constraints +NumProp!.Widget = new DefaultWidget +{ + Type = ViewModelWidgetType.PlainNumber, + Metadata = new() { [PlainNumber.Min] = "0", [PlainNumber.Max] = "100", [PlainNumber.Step] = "1" } +}; +``` + +For nullable properties, the `!` operator, rules, dependencies, data sources, menu actions, validation, and advanced patterns, see: +- [ViewModel Guide](../../activity-development-guide/design/viewmodel.md) — nullable properties, InitializeModel patterns, resource cleanup +- [Rules and Dependencies](../../activity-development-guide/design/rules-and-dependencies.md) +- [DataSource Patterns](../../activity-development-guide/design/datasources.md) +- [Menu Actions](../../activity-development-guide/design/menu-actions.md) +- [Validation](../../activity-development-guide/design/validation.md) +- [Bindings](../../activity-development-guide/design/bindings.md) +- [Widgets](../../activity-development-guide/design/widgets/index.md) +- [Advanced Patterns](../../activity-development-guide/advanced/patterns.md) + +### 5. Helper Extension + +```csharp +// Helpers/ActivityContextExtensions.cs +using System.Activities; +using UiPath.Robot.Activities.Api; + +namespace {Namespace}.Helpers; + +public static class ActivityContextExtensions +{ + public static IExecutorRuntime GetExecutorRuntime(this ActivityContext context) + => context.GetExtension(); +} +``` + +### 6. ActivitiesMetadata.json + +```json +{ + "resourceManagerName": "{PackageName}.Resources.Resources", + "activities": [ + { + "fullName": "{Namespace}.{ActivityName}", + "shortName": "{ActivityName}", + "displayNameKey": "{ActivityName}_DisplayName", + "descriptionKey": "{ActivityName}_Description", + "categoryKey": "{Category}", + "iconKey": "activityicon.svg", + "viewModelType": "{Namespace}.ViewModels.{ActivityName}ViewModel" + } + ] +} +``` + +When adding multiple activities, add entries to the `activities` array. + +### 6b. activityicon.svg + +Default activity icon. Place at `{PackageName}/Resources/Icons/activityicon.svg`. + +```xml + + + + + +``` + +All activities in the package share this icon. For per-activity icons, add SVG files to `Resources/Icons/` and update `iconKey` in `ActivitiesMetadata.json` — the glob `` picks them up automatically. + +### 7. Resources.resx + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + {Activity Display Name} + + + {Activity description text} + + + + {Property Display Name} + + + {Property tooltip/help text} + + +``` + +**Naming convention:** +- `{ActivityName}_DisplayName` — Activity display name +- `{ActivityName}_Description` — Activity description +- `{ActivityName}_{PropertyName}_DisplayName` — Property display name +- `{ActivityName}_{PropertyName}_Tooltip` — Property tooltip + +### 7b. Resources.Designer.cs + +Generate this file manually and commit it. Add one `public static string` property per key defined in `Resources.resx`. + +```csharp +// Resources/Resources.Designer.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace {Namespace} { + + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Resources { + + private static System.Resources.ResourceManager resourceMan; + + private static System.Globalization.CultureInfo resourceCulture; + + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + internal Resources() { + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + public static System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("{PackageName}.Resources.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + public static System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + public static string {ActivityName}_DisplayName { + get { + return ResourceManager.GetString("{ActivityName}_DisplayName", resourceCulture); + } + } + + public static string {ActivityName}_Description { + get { + return ResourceManager.GetString("{ActivityName}_Description", resourceCulture); + } + } + + public static string {ActivityName}_{PropertyName}_DisplayName { + get { + return ResourceManager.GetString("{ActivityName}_{PropertyName}_DisplayName", resourceCulture); + } + } + + public static string {ActivityName}_{PropertyName}_Tooltip { + get { + return ResourceManager.GetString("{ActivityName}_{PropertyName}_Tooltip", resourceCulture); + } + } + } +} +``` + +The `ResourceManager` string `"{PackageName}.Resources.Resources"` must match the embedded resource manifest name. This is ensured by setting `{PackageName}` in the `.csproj`. + +### 8. Packaging Project .csproj + +```xml + + + net8.0 + enable + enable + + + True + $([System.DateTime]::UtcNow.DayOfYear.ToString("F0")) + $([System.DateTime]::UtcNow.TimeOfDay.TotalMinutes.ToString("F0")) + 1.0.0 + 1.0.$(VersionBuild)-dev.$(VersionRevision) + {PackageName} + {AuthorName} + {Package description} + UiPathActivities + ..\Output\Packages\ + AddDlls + False + + + + + + + + + + + + + + + + + + + + + + + All + + + +``` + +**Critical**: `PackageTags` must contain `UiPathActivities` for Studio to discover the package. + +### 9. Test Project .csproj + +```xml + + + net8.0 + enable + enable + false + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + +``` + +### 10. Unit Tests + +```csharp +// Tests/Unit/{ActivityName}UnitTests.cs +using Xunit; + +namespace {Namespace}.Tests.Unit; + +public class {ActivityName}UnitTests +{ + [Fact] + public void ExecuteInternal_ValidInput_ReturnsExpected() + { + var activity = new {ActivityName}(); + var result = activity.ExecuteInternal(/* args */); + Assert.Equal(expected, result); + } + + [Fact] + public void ExecuteInternal_InvalidInput_Throws() + { + var activity = new {ActivityName}(); + Assert.Throws(() => activity.ExecuteInternal(/* bad args */)); + } +} +``` + +### 11. Workflow Tests + +```csharp +// Tests/Workflow/{ActivityName}WorkflowTests.cs +using System.Activities; +using Moq; +using UiPath.Robot.Activities.Api; +using Xunit; + +namespace {Namespace}.Tests.Workflow; + +public class {ActivityName}WorkflowTests +{ + private readonly Mock _runtimeMock = new(); + + [Fact] + public void Execute_ValidInputs_ReturnsExpected() + { + // Use bare OutArgument — do NOT use Variable (requires enclosing scope). + // See activity-development-guide/testing/activity-testing.md + var activity = new {ActivityName} + { + {InputProp} = new InArgument(/* value */), + {OutputProp} = new OutArgument() + }; + + var runner = new WorkflowInvoker(activity); + runner.Extensions.Add(() => _runtimeMock.Object); + + var result = runner.Invoke(TimeSpan.FromSeconds(5)); + + Assert.True(result.ContainsKey("{OutputProp}")); + Assert.NotNull(result["{OutputProp}"]); + } +} +``` + +--- + +## SDK Mode Overrides + +When in SDK mode, use these templates **instead of** the corresponding classic templates above. +For files not listed here (nuget.config, ActivitiesMetadata.json, Resources.resx, Resources.Designer.cs, Packaging .csproj, activityicon.svg), use the classic templates unchanged. + +### SDK: Main Project .csproj (replaces Template 2) + +The SDK compiles into your project via shared projects (`.shproj`/`.projitems`). Package versions are centrally managed by the SDK's `Sdk.dependencies.build.targets` — do NOT specify versions for SDK-managed packages. + +```xml + + + Library + net8.0 + enable + enable + {PackageName}.Library + false + {PackageName} + True + True + + + + + + + + + + + + + + + + + + + Resources.resx + True + True + {Namespace} + + + + + Resources.Designer.cs + PublicResXFileCodeGenerator + {Namespace} + + + +``` + +- `UiPathActivityProject=True` -> imports `UiPath.Sdk.Activities.shproj` (runtime SDK code) +- `UiPathActivityDesignProject=True` -> imports `UiPath.Sdk.Activities.Design.shproj` (design-time SDK code) +- Optionally add `True` for connection/Integration Service support +- Optionally add `True` for governance support + +### SDK: Activity Class (replaces Template 3) + +The activity is a `partial` class. The other partial contains the `[ViewModelClass]` attribute (see Template 3b). + +```csharp +// Activities/{ActivityName}.cs +using System; +using System.Activities; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using UiPath.Robot.Activities.Api; +using UiPath.Sdk.Activities; + +namespace {Namespace}; + +public partial class {ActivityName} : SdkActivity<{ResultType}> +{ + [RequiredArgument] + public InArgument? {InputProp} { get; set; } + + public {ActivityName}() : base() { } + + /// + /// Constructor for unit testing — accepts a pre-configured service provider. + /// + internal {ActivityName}(IServiceProvider provider) : base(provider) { } + + protected override async Task<{ResultType}> ExecuteAsync( + AsyncCodeActivityContext context, + IServiceProvider serviceProvider, + CancellationToken cancellationToken) + { + // Read ALL inputs before any await — context is disposed after first await + var input = {InputProp}.Get(context); + + RuntimeServices.ExecutorRuntime?.LogMessage(new LogMessage + { + EventType = TraceEventType.Information, + Message = $"Executing {ActivityName} with input: {input}" + }); + + var result = await DoWorkAsync(input, cancellationToken); + + return result; + } + + protected override void OnCompleted(AsyncCodeActivityContext context, IServiceProvider serviceProvider) + { + base.OnCompleted(context, serviceProvider); + // Runs after ExecuteAsync completes, with a fresh valid context. + } +} +``` + +**SDK base class selection:** + +| Base Class | When to Use | +|---|---| +| `SdkActivity` | Async activities returning a value (most common) | +| `SdkNativeActivity` | Activities needing retry, bookmarks, or child activity scheduling | +| `CodeActivity` | Simple synchronous activities (SDK not needed) | +| `CodeActivity` | Simple synchronous with no return value (SDK not needed) | + +**Custom service registration** — override `DefaultRuntimeServicePolicy` to register your own services: + +```csharp +public class MyServicePolicy : DefaultRuntimeServicePolicy +{ + public override IServicePolicy Register(Action collection = null) + { + _services.TryAddSingleton(); + return base.Register(collection); + } +} + +// Use the custom policy as the second type parameter +public partial class {ActivityName} : SdkActivity<{ResultType}, MyServicePolicy> +``` + +### SDK: ViewModel Registration (NEW Template 3b) + +This file links the activity to its ViewModel via the `[ViewModelClass]` attribute. It is a partial of the activity class. + +```csharp +// ViewModels/{ActivityName}.Design.cs +using System.Activities.DesignViewModels; +using {Namespace}.ViewModels; + +namespace {Namespace}; + +[ViewModelClass(typeof({ActivityName}ViewModel))] +public partial class {ActivityName} +{ +} +``` + +### SDK: ViewModel Class (replaces Template 4) + +Inherits from the SDK's `BaseViewModel` instead of `DesignPropertiesViewModel`. Uses the built-in `PropertyOrderIndex` counter. + +```csharp +// ViewModels/{ActivityName}ViewModel.cs +using System; +using System.Activities.DesignViewModels; +using System.Activities.ViewModels; +using UiPath.Sdk.Activities.Design.ViewModels; + +namespace {Namespace}.ViewModels; + +internal class {ActivityName}ViewModel : BaseViewModel +{ + public DesignInArgument? {InputProp} { get; set; } + public DesignOutArgument<{ResultType}>? Result { get; set; } + public DesignProperty? {EnumProp} { get; set; } + + public {ActivityName}ViewModel(IDesignServices services) : base(services) { } + + /// + /// Constructor for unit testing — accepts a pre-configured service provider. + /// + internal {ActivityName}ViewModel(IDesignServices services, IServiceProvider activityServices) + : base(services, activityServices) { } + + protected override void InitializeModel() + { + base.InitializeModel(); + + {InputProp}!.IsPrincipal = true; + {InputProp}!.IsRequired = true; + {InputProp}!.DisplayName = Resources.{ActivityName}_{InputProp}_DisplayName; + {InputProp}!.Tooltip = Resources.{ActivityName}_{InputProp}_Tooltip; + {InputProp}!.OrderIndex = PropertyOrderIndex++; + + Result!.DisplayName = Resources.{ActivityName}_Result_DisplayName; + Result!.OrderIndex = PropertyOrderIndex++; + } +} +``` + +**Key differences from classic ViewModel:** + +| Classic | SDK | +|---------|-----| +| `DesignPropertiesViewModel` base | `BaseViewModel` base (or `BaseViewModel`) | +| `var order = 0; prop.OrderIndex = order++;` | `prop.OrderIndex = PropertyOrderIndex++;` (built-in) | +| `public` class | `internal` class (linked via `[ViewModelClass]` attribute) | +| Constructor: `(IDesignServices)` only | Constructor: `(IDesignServices)` + testable `(IDesignServices, IServiceProvider)` | +| No DI in ViewModel | `ActivityServices.GetService()` available for design-time DI | + +### SDK: Directory.build.targets (NEW Template 5) + +Place this file in the activity pack directory (parent of the `.csproj`). Adjust the relative path to point to the SDK location. + +```xml + + + + +``` + +### SDK: Test Project Additions (extends Template 9) + +Add these properties to the test `.csproj` to import SDK test infrastructure: + +```xml + + true + true + +``` + +### SDK: Unit Test Pattern (replaces Template 10) + +SDK activities accept an `IServiceProvider` in their constructor, enabling clean DI-based testing: + +```csharp +// Tests/Unit/{ActivityName}UnitTests.cs +using System; +using System.Activities; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using UiPath.Robot.Activities.Api; +using Xunit; + +namespace {Namespace}.Tests.Unit; + +public class {ActivityName}UnitTests +{ + private readonly Mock _runtimeMock = new(); + + private IServiceProvider BuildServiceProvider() + { + var services = new ServiceCollection(); + services.AddSingleton(_runtimeMock.Object); + return services.BuildServiceProvider(); + } + + [Fact] + public async Task ExecuteAsync_ValidInput_ReturnsExpected() + { + var provider = BuildServiceProvider(); + var activity = new {ActivityName}(provider) + { + {InputProp} = new InArgument(/* value */) + }; + + var runner = new WorkflowInvoker(activity); + var result = runner.Invoke(TimeSpan.FromSeconds(5)); + + Assert.NotNull(result["Result"]); + } +} +``` + +--- + +## Quick Reference: Key Rules + +### Both Modes + +1. **Property names must match** — ViewModel property names must exactly match Activity property names. +2. **`IsPrincipal = true`** for the 2-4 most important properties (shown in non-collapsible main panel). +3. **`IsRequired = true`** for mandatory properties. +4. **Use `OrderIndex`** with an incrementing counter to control display order. +5. **Localize all strings** via `Resources.resx` — never hardcode display names. +6. **`PackageTags` must contain `UiPathActivities`** for Studio discovery. +7. **`resourceManagerName` in metadata JSON** must be `{PackageName}.Resources.Resources` — set `{PackageName}` in the `.csproj` to guarantee this. +8. **Generate and commit `Resources.Designer.cs`** — `PublicResXFileCodeGenerator` only runs in Visual Studio; CLI builds need the committed file. +9. **Read all inputs before any `await`** — the `ActivityContext` is disposed after the first await. See [`core/activity-context.md`](../../activity-development-guide/core/activity-context.md). +10. **Never rename or delete activity properties** — existing `.xaml` workflows serialize by name; renaming/removing breaks deserialization. Deprecate with `[Obsolete]` + `[Browsable(false)]` instead. +11. **Enum selectors must be plain `TEnum`** — not `InArgument`. Maps to `DesignProperty` in the ViewModel. +12. **ViewModel namespace is `System.Activities.DesignViewModels`** — NOT `System.Activities.ViewModels`. +13. **ViewModel properties must be nullable** (`DesignInArgument?`) — use `!` on every access in `InitializeModel()`. + +### Classic Mode Only + +14. **Always call `base.InitializeModel()` then `PersistValuesChangedDuringInit()`** at the start of `InitializeModel()`. +15. **Separate business logic** into `ExecuteInternal()` for testability. See [`runtime/activity-code.md`](../../activity-development-guide/runtime/activity-code.md). + +### SDK Mode Only + +16. **Activity must be `partial`** — one file for runtime logic, one for `[ViewModelClass]` attribute. +17. **ViewModel is `internal`** — linked to activity via `[ViewModelClass(typeof(...))]`, not via metadata JSON `viewModelType`. +18. **Use `RuntimeServices.ExecutorRuntime`** for logging — no helper extension needed. +19. **Use `PropertyOrderIndex++`** — built into `BaseViewModel`, no manual counter variable needed. +20. **Use constructor DI for testing** — SDK activities accept `IServiceProvider` in an `internal` constructor. + +--- + +## Advanced Features Reference + +For these features, consult the corresponding guide file in [`activity-development-guide/`](../../activity-development-guide/index.md): + +| Feature | Guide File | +|---------|-----------| +| UiPath platform (Studio, Robot, Orchestrator) | [`core/architecture.md`](../../activity-development-guide/core/architecture.md) | +| ActivityContext lifetime, async patterns | [`core/activity-context.md`](../../activity-development-guide/core/activity-context.md) | +| Project structure, `.csproj` setup, troubleshooting | [`core/project-structure.md`](../../activity-development-guide/core/project-structure.md) | +| Best practices | [`core/best-practices.md`](../../activity-development-guide/core/best-practices.md) | +| Activity code patterns, enum selectors, ExecuteInternal | [`runtime/activity-code.md`](../../activity-development-guide/runtime/activity-code.md) | +| Activities API (runtime/design-time services) | [`runtime/platform-api.md`](../../activity-development-guide/runtime/platform-api.md) | +| Orchestrator integration and version checks | [`runtime/orchestrator.md`](../../activity-development-guide/runtime/orchestrator.md) | +| ViewModel patterns, nullable props, InitializeModel | [`design/viewmodel.md`](../../activity-development-guide/design/viewmodel.md) | +| Widget types and configuration | [`design/widgets/index.md`](../../activity-development-guide/design/widgets/index.md) | +| DataSource patterns (static, dynamic, enum, multi-select) | [`design/datasources.md`](../../activity-development-guide/design/datasources.md) | +| Rules and reactive dependencies | [`design/rules-and-dependencies.md`](../../activity-development-guide/design/rules-and-dependencies.md) | +| Menu actions (buttons, mode switching) | [`design/menu-actions.md`](../../activity-development-guide/design/menu-actions.md) | +| Validation (property-level, model-level, preview) | [`design/validation.md`](../../activity-development-guide/design/validation.md) | +| Metadata schema (full field reference) | [`design/metadata.md`](../../activity-development-guide/design/metadata.md) | +| Orchestrator bindings (ActivitiesBindings.json) | [`design/bindings.md`](../../activity-development-guide/design/bindings.md) | +| Project settings (ArgumentSettingAttribute) | [`design/project-settings.md`](../../activity-development-guide/design/project-settings.md) | +| Solutions vs Project scope (SolutionResourcesWidget) | [`design/solutions.md`](../../activity-development-guide/design/solutions.md) | +| Activity testing (unit + workflow) | [`testing/activity-testing.md`](../../activity-development-guide/testing/activity-testing.md) | +| ViewModel testing approaches | [`testing/viewmodel-testing.md`](../../activity-development-guide/testing/viewmodel-testing.md) | +| Advanced patterns (bidirectional mapping, NativeActivity, bookmarks) | [`advanced/patterns.md`](../../activity-development-guide/advanced/patterns.md) | +| Activities SDK (DI, telemetry, retry, service policies) | [`advanced/sdk-framework.md`](../../activity-development-guide/advanced/sdk-framework.md) | diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..3bdd6d039 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,95 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Repository Overview + +UiPath Community Activities — a collection of open-source activity packages (Cryptography, Database, FTP, Java, Python, Credentials) for the UiPath automation platform. Each package follows a consistent three-layer architecture: Runtime Activity → Design-Time ViewModel → Metadata JSON. + +## Build Commands + +```bash +# Build the full solution +dotnet build Activities/Community.Activities.sln + +# Build a specific activity pack (each has its own .sln) +dotnet build Activities/Activities.Cryptography.sln +dotnet build Activities/Activities.Database.sln +dotnet build Activities/Activities.FTP.sln +dotnet build Activities/Activities.Java.sln +dotnet build Activities/Activities.Python.sln +dotnet build Activities/Activities.Credentials.sln + +# Run all tests for an activity pack +dotnet test Activities/Activities.Cryptography.sln + +# Run a single test by fully qualified name +dotnet test Activities/Activities.Cryptography.sln --filter "FullyQualifiedName~EncryptTextWithAes" + +# Run tests in a specific test class +dotnet test Activities/Activities.Cryptography.sln --filter "FullyQualifiedName~CryptographyTests" + +# Build a NuGet package (packaging projects auto-generate on build) +dotnet build Activities/Cryptography/UiPath.Cryptography.Activities.Packaging/UiPath.Cryptography.Activities.Packaging.csproj +``` + +## Test Framework + +- **xUnit** with `Moq` for mocking and `Shouldly` for assertions +- Test parallelization is **disabled** (see `Activities/xunit.runner.json`) +- Activities are tested via `WorkflowInvoker`: create the activity, set arguments, call `invoker.Invoke()`, assert on output dictionary +- Test project naming convention: `UiPath.{Category}.Activities.Tests` + +## Architecture + +### Activity Pack Structure + +Each activity category follows this layout: +``` +{Category}/ +├── {Category}.build.props # Version and metadata for this pack +├── UiPath.{Category}/ # Core library (helpers, enums) +├── UiPath.{Category}.Activities/ # Activity classes (runtime logic) +│ ├── NetCore/ViewModels/ # ViewModel classes (design-time UI) +│ ├── Properties/ # .resx localization files +│ └── Resources/ +│ ├── Icons/ # SVG icons +│ └── ActivitiesMetadata.json # Links activities ↔ ViewModels +├── UiPath.{Category}.Activities.Tests/ # xUnit tests +└── UiPath.{Category}.Activities.Packaging/ # NuGet package definition +``` + +### Three-Layer Pattern + +1. **Activity** (`CodeActivity`): Defines `InArgument`/`OutArgument` properties, implements `Execute()`, validates in `CacheMetadata()`. Runtime telemetry via `#if ENABLE_DEFAULT_TELEMETRY`. + +2. **ViewModel** (`DesignPropertiesViewModel`): Linked to activity via `[ViewModelClass]` attribute on a partial class. Property names **must exactly match** the activity's argument names. Configures widgets, visibility rules, and menu actions in `InitializeModel()` / `InitializeRules()`. + +3. **Metadata** (`ActivitiesMetadata.json`): Registers activity → ViewModel mapping, display names (resource keys), icons, and property metadata. + +### Shared Projects + +Code reuse via C# Shared Projects (`.shproj`/`.projitems`) in `Activities/Shared/`: +- `UiPath.Shared` — core utilities +- `UiPath.Shared.Activities` — base activity classes and attributes +- `UiPath.Shared.Telemetry` — telemetry integration + +### Build Infrastructure + +- **Target frameworks**: `net6.0` (portable/Studio Web) and `net6.0-windows` (Studio Desktop), defined in `Activities/Directory.build.props` as `PortableFramework` and `WindowsFramework` +- **Central dependency versions**: `Activities/Directory.build.targets` — update versions there, plus Examples and Templates +- **Per-pack versioning**: `{Category}.build.props` — `{Major}.{Minor}.{Build}-dev.{Minutes}` in Debug, `{Major}.{Minor}.0` in Release +- **CI/CD**: Azure Pipelines configs in `Activities/.pipelines/` +- **Code analysis**: `Activities/UiPath.Activities.ruleset` + +## Key Conventions + +- All user-facing strings go in `.resx` files; activities use `[LocalizedDisplayName]`, `[LocalizedDescription]`, `[LocalizedCategory]` attributes +- New properties must specify `[DefaultValue]` for forward compatibility +- Obsolete properties get `[Obsolete]` + `[Browsable(false)]` — do not remove them or change behavior +- Breaking changes (public contract, behavior, exceptions) require discussion with repo owners +- Assembly version conflicts are treated as errors (`MSBuildWarningsAsErrors: MSB3277`) + +## Development Guide + +The comprehensive activity development guide lives in `.claude/activity-development-guide/` (start with `.claude/activity-development-guide/index.md`). It covers all patterns in depth: activity code, ViewModel code, widgets, rules, menu actions, validation, metadata, localization, bindings, testing, and complete examples. **Reference it when creating or modifying activities.** diff --git a/README.md b/README.md index cce9c3570..e17e62672 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,55 @@ -Guidelines for contribuing to this repository +Guidelines for contributing to this repository ================ -### Anatomy of an Activity pack +### Anatomy of an Activity Pack - * API - * API.Activities - * API.Activities.Design - - API should be the name of the service this pack integrates with (e.g. Excel, Sharepoint, Mail) - API is not necessary if the activities use standard .NET types - API.Activities.Design is not necessary if designers do not exist but design specific attributes should be placed in a separate file (DesignerMetadata.cs) +Each activity category follows this layout: +``` +{Category}/ +├── {Category}.build.props # Version and metadata for this pack +├── UiPath.{Category}/ # Core library (helpers, enums) +├── UiPath.{Category}.Activities/ # Activity classes (runtime logic) +│ ├── NetCore/ViewModels/ # ViewModel classes (design-time UI) +│ ├── Properties/ # .resx localization files +│ └── Resources/ +│ ├── Icons/ # SVG icons +│ └── ActivitiesMetadata.json # Links activities ↔ ViewModels +├── UiPath.{Category}.Activities.Tests/ # xUnit tests +└── UiPath.{Category}.Activities.Packaging/ # NuGet package definition +``` -### Assembly and Package Info +Every activity has three parts: **Activity** (runtime logic) → **ViewModel** (design-time UI) → **Metadata JSON** (wiring and display). Property names on the ViewModel must exactly match the Activity’s property names. - * GlobalAssemblyInfo.cs should be used - * Public namespaces should specify an **XmlnsDefinitionAttribute** that is usually **http://schemas.company.com/workflow/activities** - * NuSpec file should have the approximately same structure as the others +### Building and Testing - -### Testing and deploying +```bash +# Build the full solution +dotnet build Activities/Community.Activities.sln - * To pack the packages run nuget.exe with the desired project +# Build a specific activity pack +dotnet build Activities/Activities.Cryptography.sln -### Non-breaking changes: +# Run tests for an activity pack +dotnet test Activities/Activities.Cryptography.sln + +# Build a NuGet package +dotnet build Activities/Cryptography/UiPath.Cryptography.Activities.Packaging/UiPath.Cryptography.Activities.Packaging.csproj +``` + +### Development Guide + +For in-depth guidance on activity code, ViewModels, widgets, rules, validation, metadata, localization, testing, and complete examples, see the [Activity Development Guide](.claude/activity-development-guide/index.md). + +### Non-breaking changes * Minor version is increased every time a change in the public interface is made (e.g. a public property is added to an activity, a new activity is added) * Major version is increased when the package suffers major changes (e.g. some activities become obsolete, the behaviour and the interface change) * Any new property should specify a **DefaultValue** attribute. This will decrease the potential damage for forward compatibility -* Any obsolete property should specify the **Obsolete** attribute, Browsable(false) attribute and DesignerSerializationVisibilityAttribute if its value is no longer needed. Marking the property as obsolete should not change the behaviour for any of input provided. +* Any obsolete property should specify the **Obsolete** attribute, Browsable(false) attribute and DesignerSerializationVisibilityAttribute if its value is no longer needed. Marking the property as obsolete should not change the behaviour for any of input provided. -### Breaking changes: +### Breaking changes *(Inspired by https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/breaking-changes.md)* @@ -65,7 +83,7 @@ Examples: These require judgment: how predictable, obvious, consistent was the behavior? #### Bucket 3: Unlikely Grey Area -*Change of behavior that customers could have depended on, but probably wouldn't.* +*Change of behavior that customers could have depended on, but probably wouldn’t.* Examples: * Correcting behavior in a subtle corner case @@ -75,6 +93,6 @@ As with type 2 changes, these require judgment: what is reasonable and what’s #### What This Means for Contributors * All buckets (1, 2, and 3) breaking changes require talking to the repo owners first. -* If you're not sure which bucket applies to a given change, contact us as well. -* It doesn't matter if the old behavior is "wrong", we still need to think the implications through. -* If a change is deemed too breaking, we can help identify alternatives such as introducing a new API and depricating the old one. +* If you’re not sure which bucket applies to a given change, contact us as well. +* It doesn’t matter if the old behavior is "wrong", we still need to think the implications through. +* If a change is deemed too breaking, we can help identify alternatives such as introducing a new API and deprecating the old one.