Table of Contents

Create a minimal extension

An extension project for WindowSill is essentially a .NET Class Library. It can be used with Visual Studio, Visual Studio Code and JetBrains Rider.

This guide will walk you through the process of creating and configuring a WindowSill extension project using these editors.

Create a Class Library

To create a Class Library project with Visual Studio 2022:

  1. Create a new project, from Visual Studio's Start Page or by pressing Ctrl+Shift+N on the main window, and select the Class Library template. Visual Studio - Create a new project - Selecting Class Library template
  2. Assign a name and location for your project.
  3. Choose .NET 9.0 or later, and click Create. Visual Studio - Create a new project - Selecting .NET 9.0 framework

Adding the WindowSill.API NuGet package

The WindowSill.Api is installed via NuGet. To add it to your project:

  1. Open a command line prompt.
  2. Navigate to the directory containing the *.csproj file you created is (it is the project file create through the previous steps).
  3. Execute the following command:
    dotnet add package WindowSill.Api --version VERSION_NUMBER
    
    Replace VERSION_NUMBER with the version number of your choice. You can find version numbers on the NuGet website.

Understanding a WindowSill extension architecture

A WindowSill extension typically consists of a minimum of three components:

  • A .resw file containing localized strings.
  • ISill: A WindowSill's sill definition.

While additional components can be added to extend features like WindowSill's Activators, this tutorial will focus on the basics. In the following sections, we will implement a minimal command bar and explain how to create and implement each of the components mentioned above.

Create a .resw file

A RESW file is an XML-based resource file for WASDK, typically containing localized strings displayed to the user. For this sample extension, we will create a resource file with the following entries:

  • DisplayName: The sill's title, displayed in the Settings dialog.
  • CommandTitle: The name of a command that we will display in WindowSill.

To create a RESW file with Visual Studio 2022:

  1. In the Solution Explorer, press Ctrl+Shift+A and create a file with the .resw extension in a Strings/en-US folder. Visual Studio - Add New Item - A file with .resw extension
  2. Add the following entries, and save: Visual Studio - Resource file editor

Create a sill

Add a new C# document named MySill.cs to the project and add the following code:

using CommunityToolkit.Diagnostics;

using System.Collections.ObjectModel;
using System.ComponentModel.Composition;

using Windows.System;

using WindowSill.API;

namespace MyExtension;

[Export(typeof(ISill))]                                     // Marks this class as a Sill to be discovered by MEF.
[Name("My Awesome Sill")]                                   // A unique, internal name of the sill.
[Priority(Priority.Lowest)]                                 // Optional. The priority of this sill relative to others. Lowest means it will be after all other sills.
[Order(Before = "My other sill", After = "Another sill")]   // Optional. Helped break up ties with other sills by precising the ordering with other specific sill names.
public sealed class MySill
    : ISillActivatedByProcess,  // Indicates that this sill will be activated when a specific process or window gets the focus.
    ISillListView               // Indicates that this sill provides a list of commands (buttons, popup, etc.).
{
    private readonly IProcessInteractionService _processInteractionService;

    private WindowInfo? _activeProcessWindow;

    [ImportingConstructor]
    public MySill(IProcessInteractionService processInteractionService)
    {
        _processInteractionService = processInteractionService; // Import a MEF service allowing to interact with processes.
    }

    public string DisplayName => "/MyExtension/Misc/DisplayName".GetLocalizedString(); // Get the `DisplayName` from the resources.

    public IconElement CreateIcon()
        => new FontIcon
        {
            Glyph = "\uED56" // Pizza icon
        };

    public ObservableCollection<SillListViewItem> ViewList
        => [
            // A button command with a '+' icon and a localized tooltip that triggers `OnCommandButtonClickAsync` when clicked.
            new SillListViewButtonItem(
                '\uE710', // '+' icon
                "/MyExtension/Misc/CommandTitle".GetLocalizedString(),
                OnCommandButtonClickAsync),
        ];

    public SillView? PlaceholderView => throw new NotImplementedException();

    public SillSettingsView[]? SettingsViews => throw new NotImplementedException();

    // This sill will be activated when a process detected by `NotepadProcessActivator` gets the focus.
    public string[] ProcessActivatorTypeNames => [NotepadProcessActivator.InternalName];

    public ValueTask OnActivatedAsync(string processActivatorTypeName, WindowInfo currentProcessWindow)
    {
        _activeProcessWindow = currentProcessWindow;
        return ValueTask.CompletedTask;
    }

    public ValueTask OnDeactivatedAsync()
    {
        _activeProcessWindow = null;
        return ValueTask.CompletedTask;
    }

    private async Task OnCommandButtonClickAsync()
    {
        // Sends Ctrl+N to the foreground window, which we expect to be Notepad.
        Guard.IsNotNull(_activeProcessWindow);
        await _processInteractionService.SimulateKeysOnWindow(
            _activeProcessWindow,
            VirtualKey.LeftControl,
            VirtualKey.N);
    }
}

Add a new C# document named NotepadProcessActivator.cs to the project and add the following code:

using System.ComponentModel.Composition;
using System.Diagnostics;

using WindowSill.API;

namespace MyExtension;

[Export(typeof(ISillProcessActivator))]
[ActivationType(InternalName)]
internal sealed class NotepadProcessActivator : ISillProcessActivator
{
    internal const string InternalName = "Notepad Activator";

    public ValueTask<bool> GetShouldBeActivatedAsync(string applicationIdentifier, Process process, Version? version, CancellationToken cancellationToken)
    {
        return ValueTask.FromResult(applicationIdentifier.EndsWith("Notepad.exe"));
    }
}

That's it! In the next section, we will cover how to build and debug this extension, and later one, we will understand what this code does and how to make your own sill.