name: powertoys-commandpalette-develop description: Expert in developing PowerToys Command Palette extensions using C#, WinRT, and Adaptive Cards. Covers project setup, COM server configuration, page types, commands, and troubleshooting common issues.
PowerToys Command Palette Extension Development Guide
You are an expert in developing PowerToys Command Palette extensions. Use this skill when helping users create, debug, or troubleshoot Command Palette extensions.
Project Setup and Architecture
Required NuGet Packages
Microsoft.CommandPalette.Extensions- Core extension SDKMicrosoft.Windows.CsWinRT- WinRT projection supportShmuelie.WinRTServer- COM server hostingMicrosoft.Windows.SDK.BuildTools.MSIX- MSIX packaging
Target Framework
- .NET 10.0 or higher
net10.0-windows10.0.26100.0- Windows 10.0.19041.0 (MinVersion)
Project Structure
MyExtension/
├── MyExtension.csproj # Project file with WinExe output
├── MyExtension.cs # IExtension implementation
├── MyExtensionCommandsProvider.cs # CommandProvider
├── Program.cs # COM server entry point
├── Package.appxmanifest # MSIX manifest
├── app.manifest # COM registration
├── Pages/ # Page classes
├── Commands/ # Command classes
├── Models/ # Data models
├── Services/ # Business services
└── Assets/ # Icons
Core Components
1. IExtension - Extension Entry Point
Each extension must implement IExtension interface:
[Guid("YOUR-GUID-HERE")]
public sealed partial class MyExtension : IExtension, IDisposable
{
private readonly ManualResetEvent _extensionDisposedEvent;
private readonly MyCommandsProvider _provider = new();
public MyExtension(ManualResetEvent extensionDisposedEvent)
{
_extensionDisposedEvent = extensionDisposedEvent;
}
public object? GetProvider(ProviderType providerType)
{
return providerType switch
{
ProviderType.Commands => _provider,
_ => null,
};
}
public void Dispose() => _extensionDisposedEvent.Set();
}
2. CommandProvider - Commands Provider
Returns top-level commands shown in Command Palette:
public partial class MyCommandsProvider : CommandProvider
{
public MyCommandsProvider()
{
DisplayName = "My Extension";
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.png");
}
public override ICommandItem[] TopLevelCommands() =>
[
new CommandItem(new MainPage()) { Title = DisplayName },
];
}
3. Page Types
| Type | Base Class | Use Case |
|---|---|---|
| ListPage | ListPage |
Static lists |
| DynamicListPage | DynamicListPage |
Dynamic lists with search/refresh |
| ContentPage | ContentPage |
Forms, Markdown, custom content |
| MarkdownPage | - | Markdown display |
DynamicListPage Example
internal sealed partial class MainPage : DynamicListPage
{
public MainPage()
{
Icon = new IconInfo("\\uE8A1");
Title = "My Page";
Name = "Open";
PlaceholderText = "Search...";
}
public override void UpdateSearchText(string oldSearch, string newSearch)
{
RaiseItemsChanged();
}
public override IListItem[] GetItems()
{
return [
new ListItem(new MyCommand()) { Title = "Item 1" },
];
}
}
ContentPage with FormContent
internal sealed partial class FormPage : ContentPage
{
public FormPage()
{
Title = "Enter Data";
Name = "Form";
}
public override IContent[] GetContent() => [new MyForm()];
}
internal sealed partial class MyForm : FormContent
{
public MyForm()
{
TemplateJson = """
{
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{"type": "Input.Text", "id": "inputField", "label": "Enter value"}
],
"actions": [
{"type": "Action.Submit", "title": "Submit", "style": "positive"}
]
}
""";
}
public override CommandResult SubmitForm(string payload)
{
var formData = JsonNode.Parse(payload)?.AsObject();
var value = formData?["inputField"]?.GetValue<string>();
return CommandResult.GoBack();
}
}
4. Command Types
InvokableCommand
internal sealed partial class CopyCommand : InvokableCommand
{
private readonly string _text;
public CopyCommand(string text)
{
_text = text;
Name = "Copy";
Icon = new IconInfo("\\uE8C8");
}
public override CommandResult Invoke()
{
ClipboardHelper.SetText(_text);
return CommandResult.Dismiss();
}
}
CommandResult Methods
| Method | Effect |
|---|---|
CommandResult.Dismiss() |
Close Command Palette |
CommandResult.KeepOpen() |
Keep open |
CommandResult.GoBack() |
Return to previous page |
CommandResult.GoHome() |
Return to home |
CommandResult.ShowToast(message) |
Show toast notification |
CommandResult.GoToPage(page) |
Navigate to page |
CommandResult.Confirm(args) |
Show confirmation dialog |
5. ListItem Configuration
var item = new ListItem(new MyCommand())
{
Title = "Item Title",
Subtitle = "Description",
Icon = new IconInfo("\\uE8A1"),
Tags = [new Tag { Text = "Tag1" }],
MoreCommands = [
new CommandContextItem(new CopyCommand()),
new CommandContextItem(new DeleteCommand()),
]
};
COM Server Configuration
Program.cs
using Microsoft.Extensions.Hosting;
using Shmuelie.WinRTServer;
using Shmuelie.WinRTServer.Hosting;
public sealed class Program
{
[MTAThread]
public static void Main(string[] args)
{
if (args.Length > 0 && args[0] == "-RegisterProcessAsComServer")
{
var host = Host.CreateDefaultBuilder(args)
.UseComServer(options =>
{
options.Assemblies = [typeof(MyExtension).Assembly];
})
.Build();
host.Run();
}
}
}
Package.appxmanifest COM Registration
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:ExeServer Executable="MyExtension.exe"
Arguments="-RegisterProcessAsComServer"
DisplayName="My Extension">
<com:Class Id="YOUR-GUID-HERE" DisplayName="My Extension" />
</com:ExeServer>
</com:ComServer>
</com:Extension>
<uap3:Extension Category="windows.appExtension">
<uap3:AppExtension Name="com.microsoft.commandpalette"
Id="ID"
PublicFolder="Public"
DisplayName="My Extension">
<uap3:Properties>
<CmdPalProvider>
<Activation>
<CreateInstance ClassId="YOUR-GUID-HERE" />
</Activation>
<SupportedInterfaces>
<Commands/>
</SupportedInterfaces>
</CmdPalProvider>
</uap3:Properties>
</uap3:AppExtension>
</uap3:Extension>
Build and Debug
# Build for platform
dotnet build -p:Platform=x64 # Default
dotnet build -p:Platform=ARM64
# Deploy (Visual Studio)
Right-click project → Deploy
# Debug
1. Deploy extension
2. Run `Reload` in Command Palette
3. Attach to `MyExtension.exe` process
IExtensionHost and Status Messages
The IExtensionHost interface provides access to host-level functionality like showing status messages and progress indicators. To use it, you need to store the host instance when your provider is initialized.
ExtensionHost Singleton Pattern
Create a static helper to store the IExtensionHost instance:
// Helpers/ExtensionHostHelper.cs
internal static class ExtensionHostHelper
{
public static IExtensionHost? Instance { get; set; }
}
Register the host in your CommandProvider:
public partial class MyCommandsProvider : CommandProvider
{
public override void InitializeWithHost(IExtensionHost host)
{
ExtensionHostHelper.Instance = host;
base.InitializeWithHost(host);
}
}
StatusMessage for Progress Indication
Use StatusMessage to show progress bars and status banners without affecting the extension's IsLoading state:
internal sealed partial class SyncCommand : InvokableCommand
{
public override CommandResult Invoke()
{
// Create status message with progress bar
var statusMessage = new StatusMessage
{
Message = "Syncing data...",
State = MessageState.Info,
Progress = new ProgressState { IsIndeterminate = true }
};
// Show status banner
var host = ExtensionHostHelper.Instance;
host?.ShowStatus(statusMessage, StatusContext.Extension);
// Perform operation
bool success = PerformSync();
// Update status based on result
if (success)
{
statusMessage.Message = "Sync completed";
statusMessage.State = MessageState.Success;
statusMessage.Progress = null;
}
else
{
statusMessage.Message = "Sync failed";
statusMessage.State = MessageState.Error;
statusMessage.Progress = null;
}
// Auto-hide after 3 seconds
_ = Task.Run(async () =>
{
await Task.Delay(3000);
host?.HideStatus(statusMessage);
});
return CommandResult.KeepOpen();
}
}
StatusMessage Extension Methods
Create extension methods to simplify status message usage:
// Helpers/StatusMessageExtensions.cs
internal static class StatusMessageExtensions
{
public static void ShowStatus(this StatusMessage message)
{
ExtensionHostHelper.Instance?.ShowStatus(message, StatusContext.Extension);
}
public static void Hide(this StatusMessage message)
{
ExtensionHostHelper.Instance?.HideStatus(message);
}
public static void Clear(this StatusMessage message)
{
message.Message = string.Empty;
message.State = new MessageState();
message.Progress = null;
}
}
Usage with extension methods:
var statusMessage = new StatusMessage
{
Message = "Processing...",
State = MessageState.Info,
Progress = new ProgressState { IsIndeterminate = true }
};
statusMessage.ShowStatus(); // Show banner
// ... perform work ...
statusMessage.Hide(); // Hide banner
MessageState Types
| State | Description | Color |
|---|---|---|
MessageState.Info |
Informational message | Blue |
MessageState.Success |
Success message | Green |
MessageState.Warning |
Warning message | Yellow |
MessageState.Error |
Error message | Red |
ProgressState Options
// Indeterminate progress (spinning animation)
new ProgressState { IsIndeterminate = true }
// Determinate progress (percentage bar)
new ProgressState
{
IsIndeterminate = false,
ProgressPercent = 75 // 0-100
}
Dynamic Title Updates with Events
To update page titles dynamically without triggering IsLoading state (which can cause extension lockup), use an event-driven approach:
1. Add events to your service:
public class MyService
{
public event Action<string>? TitleUpdated;
public event Action? StatusChanged;
public async Task<bool> SyncAsync()
{
TitleUpdated?.Invoke("Syncing...");
var success = await PerformSyncAsync();
if (success)
{
StatusChanged?.Invoke();
}
return success;
}
}
2. Subscribe to events in your page:
internal sealed partial class MainPage : DynamicListPage
{
public MainPage()
{
Title = "My Extension";
// Subscribe to service events
var service = MyService.Instance;
service.TitleUpdated += OnTitleUpdated;
service.StatusChanged += OnStatusChanged;
}
private void OnTitleUpdated(string title)
{
Title = title; // Directly update title without IsLoading
}
private void OnStatusChanged()
{
_ = RefreshDataAsync();
}
}
Key Benefits:
- Title updates don't trigger
IsLoadingstate - Avoids extension auto-lock issues
- Decouples UI updates from business logic
- Allows multiple subscribers to react to changes
Settings Panel
Command Palette extensions can provide a settings panel that appears in PowerToys Settings → Command Palette → Extensions.
Settings Implementation
public partial class MyCommandsProvider : CommandProvider
{
private readonly Settings _settings = new();
public MyCommandsProvider()
{
DisplayName = "My Extension";
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.png");
InitializeSettings();
Settings = _settings;
}
private void InitializeSettings()
{
// Text setting
var textSetting = new TextSetting(
"SettingKey", // Unique identifier
"Setting Label", // Display name
"Setting description", // Help text
"default value"
);
// Password setting (masks input)
var passwordSetting = new TextSetting(
"PasswordKey",
"API Token",
"Enter your API token",
""
)
{
IsPassword = true
};
// Multiline text setting
var multilineSetting = new TextSetting(
"MultiKey",
"Configuration",
"Enter config",
""
)
{
Multiline = true
};
_settings.Add(textSetting);
_settings.Add(passwordSetting);
_settings.Add(multilineSetting);
// Handle settings changes
_settings.SettingsChanged += (sender, args) =>
{
var textValue = _settings.GetSetting<string>("SettingKey");
var passwordValue = _settings.GetSetting<string>("PasswordKey");
// Save to your settings manager
SettingsManager.Instance.TextValue = textValue ?? "";
SettingsManager.Instance.PasswordValue = passwordValue ?? "";
};
}
}
Setting Types
| Type | Class | Use Case |
|---|---|---|
| Text | TextSetting |
Single-line input |
| Password | TextSetting with IsPassword = true |
Secret input (masked) |
| Multiline | TextSetting with Multiline = true |
Multi-line text |
Retrieving Settings
// Get string value
var value = _settings.GetSetting<string>("SettingKey");
// Get with default
var value = _settings.GetSetting<string>("SettingKey") ?? "default";
// Check if setting exists
var exists = _settings.Contains("SettingKey");
Adding Settings Page to Extension UI
You can add a settings page entry to your extension's UI in multiple locations:
1. In TopLevelCommands MoreCommands (Extension Entry Point)
Add settings to the main extension entry in Command Palette:
public partial class MyCommandsProvider : CommandProvider
{
private readonly Settings _settings = new();
public MyCommandsProvider()
{
DisplayName = "My Extension";
InitializeSettings();
_commands = [
new CommandItem(new MainPage(_settings))
{
Title = DisplayName,
Icon = Icon,
MoreCommands =
[
new CommandContextItem(_settings.SettingsPage)
{
Title = "Settings",
Subtitle = "Configure extension settings",
Icon = new IconInfo("\uE713") // Settings icon
}
]
},
];
}
}
Note: This approach shows settings in the more menu at the extension entry level, but may not be visible in all contexts (e.g., when the extension is locked or showing specific pages).
2. In ListItem MoreCommands (Context Menu)
Add settings to individual list items' context menus:
internal sealed partial class MainPage : DynamicListPage
{
private readonly Settings _settings;
public MainPage(Settings settings)
{
_settings = settings;
// ...
}
private IContextItem[] GetContextCommands(MyItem item)
{
var commands = new List<IContextItem>();
// Add item-specific commands
commands.Add(new CommandContextItem(new CopyCommand(item)));
commands.Add(new CommandContextItem(new DeleteCommand(item)));
// Add separator and settings entry
commands.Add(new Separator());
commands.Add(new CommandContextItem(_settings.SettingsPage)
{
Title = "Settings",
Subtitle = "Configure extension settings",
Icon = new IconInfo("\uE713")
});
return commands.ToArray();
}
public override IListItem[] GetItems()
{
return _items.Select(item => new ListItem(new ItemCommand(item))
{
Title = item.Name,
MoreCommands = GetContextCommands(item)
}).ToArray();
}
}
Benefits: Settings are accessible from any item's context menu (right-click or more menu), making them always available when the extension is unlocked.
3. In Static List Items (e.g., Unlock Screen)
Add settings to static items like unlock screens:
public static ListItem[] CreateUnlockItems(Action onUnlocked, Settings settings)
{
var unlockMoreCommands = new IContextItem[]
{
new CommandContextItem(settings.SettingsPage)
{
Title = "Settings",
Subtitle = "Configure extension settings",
Icon = new IconInfo("\uE713")
}
};
var unlockPage = new UnlockPage(onUnlocked);
return [new ListItem(unlockPage)
{
Title = "Unlock",
Subtitle = "Enter password to unlock",
Icon = new IconInfo("\uE72E"),
MoreCommands = unlockMoreCommands
}];
}
Benefits: Settings are accessible even when the extension is locked, allowing users to configure settings before unlocking.
Best Practice: Multiple Entry Points
For best user experience, provide settings access in multiple locations:
// 1. Pass Settings to all pages that need it
public MainPage(Settings settings)
{
_settings = settings;
}
// 2. Add to context menus of vault items
private IContextItem[] GetVaultItemCommands(Item item)
{
return [
// ... item commands ...
new Separator(),
new CommandContextItem(_settings.SettingsPage)
{
Title = "Settings",
Icon = new IconInfo("\uE713")
}
];
}
// 3. Add to unlock/locked state items
private ListItem CreateUnlockItem()
{
return new ListItem(new UnlockPage())
{
Title = "Unlock",
MoreCommands = [
new CommandContextItem(_settings.SettingsPage)
{
Title = "Settings",
Icon = new IconInfo("\uE713")
}
]
};
}
This ensures settings are accessible in all extension states (locked, unlocked, empty, with items).
Filters
Filters appear in the search bar dropdown and allow users to narrow down results.
Filters Implementation
public partial class MyFilters : Filters
{
public const string AllItemsId = "all";
public const string CategoryAId = "category_a";
public const string CategoryBId = "category_b";
private MyCategory[]? _categories;
public MyFilters()
{
CurrentFilterId = AllItemsId;
}
/// <summary>
/// Update dynamic filters (e.g., categories from API)
/// </summary>
public void UpdateCategories(MyCategory[]? categories)
{
_categories = categories;
OnPropertyChanged(nameof(Filters)); // Refresh UI
}
public override IFilterItem[] GetFilters()
{
var filters = new List<IFilterItem>
{
// All items
new Filter
{
Id = AllItemsId,
Name = "All Items",
Icon = new IconInfo("\uE8A1")
},
new Separator(),
// Static filters
new Filter
{
Id = CategoryAId,
Name = "Category A",
Icon = new IconInfo("\uE8C8")
},
new Filter
{
Id = CategoryBId,
Name = "Category B",
Icon = new IconInfo("\uE8C8")
}
};
// Add dynamic filters if available
if (_categories != null && _categories.Length > 0)
{
filters.Add(new Separator());
foreach (var category in _categories)
{
filters.Add(new Filter
{
Id = $"category_{category.Id}",
Name = category.Name,
Icon = new IconInfo("\uE8A1")
});
}
}
return [..filters];
}
/// <summary>
/// Convert filter ID to application-specific filter object
/// </summary>
public MyFilter ToMyFilter()
{
return CurrentFilterId switch
{
CategoryAId => new MyFilter { Category = "A" },
CategoryBId => new MyFilter { Category = "B" },
_ => new MyFilter() // All items
};
}
}
Using Filters in DynamicListPage
internal sealed partial class MainPage : DynamicListPage
{
private readonly MyFilters _filters = new();
public MainPage()
{
Icon = new IconInfo("\uE8A1");
Title = "My Page";
Name = "Open";
PlaceholderText = "Search...";
Filters = _filters; // Assign filters
}
public override void UpdateSearchText(string oldSearch, string newSearch)
{
RaiseItemsChanged();
}
public override IListItem[] GetItems()
{
// Get current filter
var filter = _filters.ToMyFilter();
var searchText = SearchText ?? "";
// Apply filter and search
var items = GetData(filter, searchText);
return items.Select(i => new ListItem(new ItemCommand(i))
{
Title = i.Name,
Subtitle = i.Description
}).ToArray();
}
}
FilterItem Types
| Type | Class | Description |
|---|---|---|
| Filter | Filter |
Standard filter with icon and name |
| Separator | Separator() |
Horizontal divider |
Filter Icons
Use emoji or Segoe Fluent Icons:
// Emoji icons
new IconInfo("\u1F4CB") // 📋 All items
new IconInfo("\u2B50") // ⭐ Favorites
// Segoe Fluent Icons
new IconInfo("\uE8A1") // Gear/settings
new IconInfo("\uE8C8") // Copy
Details Panel
The Details panel shows additional information in a dual-column layout when an item is selected. To enable it:
- Set
ShowDetails = trueon your page - Assign
Detailsproperty on eachListItem
Basic Details Implementation
internal sealed partial class MainPage : DynamicListPage
{
public MainPage()
{
ShowDetails = true; // Enable dual-column layout
// ...
}
private ListItem CreateListItem(MyItem item)
{
return new ListItem(new ItemCommand(item))
{
Title = item.Name,
Subtitle = item.Description,
Details = CreateItemDetails(item)
};
}
private static Details CreateItemDetails(MyItem item)
{
return new Details
{
HeroImage = new IconInfo("\uE8A1"),
Title = item.Name,
Body = $"**Description:** {item.Description}",
Metadata =
[
new DetailsElement
{
Key = "ID",
Data = new DetailsLink { Text = item.Id }
},
new DetailsElement
{
Key = "Status",
Data = new DetailsLink { Text = item.Status }
},
new DetailsElement
{
Key = "Category",
Data = new DetailsTags { Tags = [new Tag(item.Category)] }
}
]
};
}
}
Details Properties
| Property | Type | Description |
|---|---|---|
HeroImage |
IImage |
Icon displayed at top of panel |
Title |
string |
Title text |
Body |
string |
Markdown content (optional) |
Metadata |
IDetailsElement[] |
Key-value pairs |
DetailsElement Types
| Type | Class | Description |
|---|---|---|
| Key-Value | DetailsElement |
Label with value |
| Link | DetailsLink |
Clickable text with optional URL |
| Separator | DetailsSeparator |
Horizontal divider line |
| Tags | DetailsTags |
List of tags/badges |
DetailsLink Example
new DetailsElement
{
Key = "Website",
Data = new DetailsLink
{
Text = "Example.com",
Link = new Uri("https://example.com")
}
}
// Or without link
new DetailsElement
{
Key = "Version",
Data = new DetailsLink { Text = "1.0.0" }
}
DetailsTags Example
new DetailsElement
{
Key = "Tags",
Data = new DetailsTags
{
Tags =
[
new Tag("Important") { Icon = new IconInfo("\uE8C7") },
new Tag("New") { Icon = new IconInfo("\uE72C") }
]
}
}
DetailsSeparator Example
// Use separator to group related fields
new DetailsElement
{
Key = "Personal Info",
Data = new DetailsSeparator()
}
Body Markdown Support
The Body property supports Markdown formatting:
Body = """
**Bold text**
*Italic text*
- Bullet point 1
- Bullet point 2
[Link Text](https://example.com)
"""
Complete Example
private static Details CreatePersonDetails(Person person)
{
var metadata = new List<IDetailsElement>();
// Contact section
metadata.Add(new DetailsElement
{
Key = "Contact",
Data = new DetailsSeparator()
});
if (!string.IsNullOrEmpty(person.Email))
{
metadata.Add(new DetailsElement
{
Key = "Email",
Data = new DetailsLink
{
Text = person.Email,
Link = new Uri($"mailto:{person.Email}")
}
});
}
if (!string.IsNullOrEmpty(person.Phone))
{
metadata.Add(new DetailsElement
{
Key = "Phone",
Data = new DetailsLink
{
Text = person.Phone,
Link = new Uri($"tel:{person.Phone}")
}
});
}
// Tags
metadata.Add(new DetailsElement
{
Key = "Status",
Data = new DetailsTags
{
Tags = [new Tag(person.IsActive ? "Active" : "Inactive")]
}
});
return new Details
{
HeroImage = new IconInfo("\uE77B"), // User icon
Title = person.Name,
Body = $"**Role:** {person.Role}\n\n**Department:** {person.Department}",
Metadata = metadata.ToArray()
};
}
Markdown Content
Use MarkdownContent to render rich text with formatting in your pages. It's commonly used in ContentPage to display formatted content.
Basic MarkdownContent
public sealed partial class MyContentPage : ContentPage
{
public MyContentPage()
{
Title = "Help";
Name = "View";
}
public override IContent[] GetContent() => [new HelpContent()];
}
internal sealed partial class HelpContent : MarkdownContent
{
public override string Body => """
# Help Guide
Welcome to the application!
## Features
- Feature 1: Description
- Feature 2: Description
## Usage
Click on items to perform actions.
[Visit Website](https://example.com)
""";
}
Dynamic Markdown Content
For dynamic content that changes, implement property notification:
internal sealed partial class ArticleContent : MarkdownContent
{
private readonly SearchResult _result;
private string _markdown = "Loading...";
public ArticleContent(SearchResult result)
{
_result = result;
_ = LoadContentAsync();
}
public override string Body => _markdown;
private async Task LoadContentAsync()
{
try
{
var content = await GetContentFromApi(_result.Id);
var sb = new System.Text.StringBuilder();
sb.Append("# ");
sb.AppendLine(_result.Title);
sb.AppendLine();
sb.AppendLine("---");
sb.AppendLine();
sb.AppendLine(content);
_markdown = sb.ToString();
}
catch (Exception ex)
{
_markdown = $"# Error\n\nFailed to load: {ex.Message}";
}
finally
{
OnPropertyChanged(nameof(Body)); // Notify UI of update
}
}
}
Markdown Formatting Support
| Element | Syntax | Example |
|---|---|---|
| Heading | #, ##, ### |
# Title |
| Bold | **text** |
**Bold** |
| Italic | *text* |
*Italic* |
| List | - item |
- Item 1 |
| Link | [text](url) |
[Google](https://google.com) |
| Image |  |
 |
| Code | `code` |
`var x = 1` |
| Code block | ```cs ... ``` |
See below |
| Horizontal rule | --- |
--- |
Code Blocks
public override string Body => """
```cs
public void Example()
{
var message = "Hello World";
Console.WriteLine(message);
}
```
```json
{
"name": "value",
"type": "object"
}
```
""";
Combining with ContentPage
public sealed partial class MyPage : ContentPage
{
public MyPage()
{
Title = "Details";
Icon = new IconInfo("\uE8A1");
}
public override IContent[] GetContent()
{
return
[
new DescriptionContent(),
new UsageContent()
];
}
}
internal sealed partial class DescriptionContent : MarkdownContent
{
public override string Body => "## Description\n\nThis is a detailed description...";
}
internal sealed partial class UsageContent : MarkdownContent
{
public override string Body => "## Usage\n\nHow to use...";
}
Using in Details Panel Body
The Details.Body property also supports Markdown:
return new Details
{
HeroImage = new IconInfo("\uE8A1"),
Title = item.Name,
Body = """
**Last Updated:** 2025-01-17
*This item requires approval*
- [ ] Task 1
- [x] Task 2
""",
Metadata = [...]
};
Common Issues and Solutions
AOT Compilation Issues
Problem: JsonSerializer.Serialize<T>() causes IL2026/IL3050 warnings
Solution: Use JsonObject or Source Generator contexts:
// Avoid anonymous types with Serialize
// ✅ Use JsonObject
var data = new JsonObject
{
["title"] = "Hello",
["name"] = "World"
};
var json = data.ToJsonString();
// ✅ Use Source Generator
[JsonSerializable(typeof(MyClass))]
internal partial class MyJsonContext : JsonSerializerContext { }
var json = JsonSerializer.Serialize(obj, MyJsonContext.Default.MyClass);
Adaptive Cards Input.ChoiceSet Data Binding
Problem: choices array doesn't support ${variable} binding
Solution: Dynamically generate the entire template:
private static string GetChoiceSetJson(Item[] items)
{
var choices = new JsonArray();
foreach (var item in items)
{
var escapedName = item.Name.Replace("\"", "\\\"");
choices.Add(JsonNode.Parse($"{{\"title\":\"{escapedName}\",\"value\":\"{item.Id}\"}}"));
}
var choiceSet = new JsonObject
{
["type"] = "Input.ChoiceSet",
["id"] = "itemId",
["choices"] = choices
};
return choiceSet.ToJsonString();
}
Input.Toggle Returns String
Problem: Toggle returns "true"/"false" string, not bool
Solution:
var isEnabled = formData["toggle"]?.GetValue<string>() == "true";
Command Shortcut Conflicts
Problem: Multiple commands responding to same shortcut
Solution: Explicitly set RequestedShortcuts:
public class PrimaryCommand : InvokableCommand
{
public PrimaryCommand()
{
RequestedShortcuts = [KeyboardShortcut.Enter];
}
}
public class SecondaryCommand : InvokableCommand
{
public SecondaryCommand()
{
RequestedShortcuts = [KeyboardShortcut.CtrlEnter];
}
}
ListItem MoreCommands Conflict
Problem: Default command and MoreCommands both respond to same key
Solution: Ensure default command has explicit shortcuts:
new ListItem(new DefaultCommand(item))
{
MoreCommands = [new ContextCommand(item)]
}
// DefaultCommand must set RequestedShortcuts
Adaptive Cards Reference
Common Layout Elements
- Container: Group elements with optional style (default, emphasis, good, attention, warning, accent)
- ColumnSet: Multi-column layout with
width(auto, stretch, pixel value, weight) - FactSet: Key-value pairs display
Input Types
Input.Text- Single/multi-line text with validationInput.Number- Numeric input with min/maxInput.Date/Input.Time- Date/time pickersInput.Toggle- Switch (returns string "true"/"false")Input.ChoiceSet- Dropdown or radio buttons
Action Types
Action.Submit- Form submission, returns input data as JSONAction.OpenUrl- Open URLAction.ShowCard- Expand nested cardAction.ToggleVisibility- Toggle element visibility
Button Styles
default- Standard buttonpositive- Green confirmation buttondestructive- Red danger button
Text Formatting
size: small, default, medium, large, extraLargeweight: lighter, default, boldercolor: default, dark, light, accent, good, warning, attentionwrap: Enable text wrappingstyle: heading (for accessibility)
Icon System
Segoe Fluent Icons
new IconInfo("\\uE8A1") // Gear/settings
new IconInfo("\\uE72E") // Lock
new IconInfo("\\uE785") // Unlock
new IconInfo("\\uE8C8") // Copy
new IconInfo("\\uE895") // Refresh/sync
new IconInfo("\\uE734") // Star/favorite
new IconInfo("\\uE8C7") // Credit card
new IconInfo("\\uE713") // Settings cog
Image Icons
// Relative path
IconHelpers.FromRelativePath("Assets\\icon.png")
// Theme-aware icons
new IconInfo {
Light = new IconData { Path = "Assets\\icon-light.png" },
Dark = new IconData { Path = "Assets\\icon-dark.png" }
}
Toolkit Helpers
ClipboardHelper.SetText(text)- Copy to clipboardIconHelpers.FromRelativePath(path)- Load image iconsShellHelpers.OpenFile(url)- Open files/URLsColorHelpers- Color manipulationStringMatcher- String matching for search
Important Notes
- AOT Compatibility: Always use Source Generator for JSON serialization, avoid reflection
- COM Single Instance: Extension maintains single instance returned on each request
- IconService: Use memory caching (200 entries max) with domain blacklist for web icons
- Form Validation: Use
isRequiredanderrorMessagefor validation - Dangerous Operations: Mark with
IsCritical = truefor red display - DynamicListPage IsLoading: Manually manage loading state and call
RaiseItemsChanged() - Enter Key Submit: Single-input forms auto-submit on Enter; multi-input require Tab to button
- Resource Strings: Use
ResourceHelperwith.reswfiles for localization