Monorepo with NPM Workspaces and Typescript

I recently needed to set up a project with a monorepo and was searching about for a good guide or example. Nothing I found was quite right. This repo had a rather extensive example but brought in lerna, which I wanted to avoid. This one was a bit simpler but was using yarn in the examples.

I created ts-npm-monorepo as a bare-bones example for how to set up a monorepo with Typescript using only NPM Workspaces.

This is working well: I only have one copy of node_modules at the root and all the packages share it. The packages are set up with Typescript project references, so everything builds in the proper order and changes are immediately reflected. I can also add any shared external dependencies in the root package.json and they’ll be available in any package in the repo.

(Edit) After reading up a bit more on Yarn it’s pretty great and I’ve switched to using Yarn workspaces.

AWS Cognito Authentication in Electron

The AWS Cognito authentication service as of this writing does not officially support the Electron platform. But there is a Javascript SDK for Cognito, as part of AWS Amplify. Others have tried using it on Electron but have run into issues. I ran into several more than what are described in that thread, so I’ll go over the stumbling blocks and how I managed to get around each one.

In my case I’m using Federated authentication (via Google) through Cognito User Pools. I’m calling Auth.federatedSignIn() from the preload section, but I think this would still work if you ran it with the other page code.

Making the auth happen in a different window

AWS sends you off to another webpage to log in, via window.open(<url>, "_self"). Depending on your electron configuration, this might actually open a new window, or it might try to open the auth page in the same window.

Opening in the same window is problematic. You can be redirected back to your electron page, but then you might be running a bunch of page/app initialization again. To solve this I monkey-patched window.open:

// Strips off the "_self" that was added by the AWS Amplify SDK, to open auth in a new window.
const oldWindowOpen = window.open;
window.open = <any>((url: string) => {
    return oldWindowOpen(url);
});

You might need to refine this to only mess with auth URLs if you have other bits of code that you expect to call window.open().

But after this change, the auth page gets opened in a new window.

Allowing the auth window to open

At this point it’s trying to open a new window, but my boilerplate Electron setup had a setWindowHandler() call that was blocking it. To fix I updated the code to allow when the URL was for my auth service:

this.MainWindow.webContents.setWindowOpenHandler(({ url }) => {
    if (url.startsWith('https://my-service.auth.us-east-2.amazoncognito.com/')) {
        return { action: 'allow' };
    } else {
        return { action: 'deny' };
    }
});

Detecting when authentication is finished

Normally when using this authentication library on a website it will redirect back to your website, where you can handle the code it’s given you on the URL. In my case I have redirectSignIn and redirectSignOut set to http://localhost:49391/ . I don’t actually have anything running on port 49391, but in our case we don’t need to! We can instead add an onBeforeRequest listener to intercept it and handle it. In our setup in the main process:

this.MainWindow.webContents.on("did-create-window", (window, details) => {
    if (details.url.includes("amazoncognito.com")) {
        this.authWindow = window;
    }

});

session.defaultSession.webRequest.onBeforeRequest(
    { urls: ["http://localhost:49391/*"] },
    (details: Electron.OnBeforeRequestListenerDetails, callback: (response: Electron.Response) => void) => {

        this.MainWindow.webContents.send("handleAuthResponse", details.url);
        this.authWindow?.close();

        callback({ cancel: true });
    });

We capture the auth window when it’s created, then when we see a request come in for http://localhost:49391 we intercept it, dispatch a message to our renderer process, close the auth window, then cancel the request. Cancelling the request using the callback makes sure that nobody else can set up a server on port 49391 and listen in for our auth key.

You will also need to update your app client in the AWS Cognito console:

Passing the success URL back to the Amplify library

We need to set up a listener in our render preload script to get the IPC message:

ipcRenderer.on("handleAuthResponse", (event, url) => {

    (<any>Auth)._handleAuthResponse(url);
});

_handleAuthResponse is a private method on the Auth object that we need to invoke and provide the success URL with the returned key. Obviously, there are major caveats with using a private method as it could break at any point, but it works as of aws-amplify 4.3.10. Should work if I don’t upgrade the package until they’ve added proper support, right? 🤞

In my case I’m running all my auth code inside the preload context so I directly call from there, but you can also send it back to the browser context if you’ve been doing auth calls there.

Patching up history.replaceState

The aws-amplify library tries to call history.replaceState at one point in _handleAuthResponse, to attempt to scrub the URL with the key from the browser back history. But this failed in Electron for me. To fix, I replaced that API with a stub:

Object.defineProperty(history, "replaceState", {
    configurable: true,
    value: () => {}
});

We don’t need to do this history scrub anyway because the window with the URL in it is getting closed.

I don’t need to call history.replaceState() normally in my app, but I would restore it back to its old value after if it became important.

Now you’re logged in!

At this point the federated authentication is complete and you can proceed as normal as the AWS Amplify docs direct. It should fire the “signIn” event on the Hub under the “auth” channel as normal. In my case for the User Pool I needed to call Auth.currentUserInfo() or Auth.currentUserPoolUser().

Update – October 2022

I tried updating Electron from 16 to 21 and this method stopped working. The AWS SDK now thinks that it’s in a Node environment and starts trying to call Node APIs. Which fail because nodeIntegration is disabled on that page (which it should be for security reasons). If I want to continue the hack route I’ll need some way to fool the AWS SDK into thinking its actually in a web page. Or switch to Auth0 which actually has a promising tutorial for integration with Electron.

Update – December 2022

I also found out that the AWS Amplify SDK doesn’t tree shake well and hauled in 1.64 MB of minified Javascript to my preload script, which was horribly slowing down cold startup time. I switched over to Auth0 and the startup time recovered. It’s a lot more expensive than Cognito but at least they support Electron. The Auth0 electron tutorial is excellent and only requires tiny dependencies like jwt-decode, so you can keep your code lean and mean.

Efficient SVG icons in Web Components with Webpack and SVGO

So many ways to load them

There are a lot of different ways to show an SVG on a webpage: <img>, <embed>, <object>, <iframe>, <canvas> and <svg> among them. I think for any halfway modern browser there are really only two serious contenders here. Referencing an SVG file:

<img src="image.svg" />

And embedding the SVG directly into the DOM.

<svg><circle cx="50" cy="50" r="40" /></svg>

There are some advantages and drawbacks to each. <img> tags will keep your layouts small and only download the image as it’s needed. It can be cached by the browser and doesn’t need to be re-downloaded when your JS bundles update. But you can’t change their fill with CSS rules, which makes them not ideal for icons, which often need to switch color in dark vs light mode or for high contrast mode.

Conversely, <svg> tags are malleable to CSS rules, but they are harder to bring in, cache separately and lazy-load.

In our case we support dark and light modes, so we went with <svg> tags for our icons.

Should we delay-load?

Now the question is, how do we include them from our Web Component templates? In React we have SVGR, to turn SVGs into React components that can do fancy stuff like tell Webpack to make a separate bundle with the SVG data and go load that in on-demand.

There is not yet a tool that can do exactly that for Web Components. I was preparing to sit down and write one, but after thinking about it for a bit and consulting with some members of the FAST Element team; I don’t think it’s necessary to load them separately.

  • Properly optimized icon SVGs are tiny, since they are just vector data. Not a lot is being gained by pushing that off to a separate web request and running it through a bunch of code to bring it in.
  • The icon data is slim enough that it will represent a small fraction of the template size.
  • Bundled icons never have late pop-in and are never missing. They are treated like just another part of your view template.

How to include them in your template

Putting them straight into the template is one way:

<button><svg><circle cx="50" cy="50" r="40" /></svg></button>

That’s not great because you can’t re-use them, and if you need to update an icon you have to go find everywhere you used it.

(fun fact: you only need the xmlns attribute on the <svg> tag when it’s in a file and you can omit it when it’s embedded in the DOM)

The next impulse is to just export them from some icons module:

export const circleIcon = `<svg><circle cx="50" cy="50" r="40" /></svg>`;

Then import the string and include in your template:

<button>${circleIcon}</button>

That was what we tried at first. It worked, but it had some drawbacks. Our central icons file soon got quite large, up to 220kb of icons from various things that might eventually be shown at some point in interacting with the app. But we only need ~10kb of that for the first page load. The initial assumption was that Webpack would save us, but it just ended up putting the whole giant module in a critical JS bundle. That means we’d need to front load every single icon before we could display anything at all. Oops.

The solution

We ended up using a combination of raw-loader and svgo-loader webpack modules. In our webpack config:

module: {
    rules: [
        { test: /\.svg$/, use: [
            { loader: "raw-loader" },
            {
                loader: "svgo-loader",
                options: {
                    configFile: false,
                    floatPrecision: 2,
                    plugins: extendDefaultPlugins([
                        "removeXMLNS", // We can safely remove the XMLNS attribute because we are inlining our SVGs
                        {
                            name: "removeViewBox",
                            active: false
                        }
                    ])
                }
            }
        ]},
        ...

Raw-loader means it’s going to bring the string directly into the module instead of reference it somewhere else.

The SVGO-loader performs a lot of optimizations on the SVGs to slim them down: for example removing redundant paths and combining elements. So you end up with:

import circleIcon from "./Circle.svg";

...

<button>${circleIcon}</button>

The built JS file looks like this:

<button>${'<svg><circle cx="50" cy="50" r="40" /></svg>'}</button>

Except a bit smaller and more efficient than what was in Circle.svg.

De-duplication

A concern here is the size penalty from including the same icon multiple times. One thing to note is that Webpack will, even with the raw-loader re-use a variable if you’ve used the same SVG more than one time in a single module.

Another technique you can use is that if you have icons that are re-used and that show up together, is to re-export them from a shared icons module, and import them where needed:

import circleIcon from "./Circle.svg";
import squareIcon from "./Square.svg";

export { circleIcon, squareIcon };

Then you can import and use them as normal strings. Just remember that if you put everything in the same file it won’t scale very well as Webpack can’t break the module up.

TypeScript ANGRY

If you’re using TypeScript (which of course you should be) it’s going to be cross with you. Cannot find module './Circle.svg' or its corresponding type declarations. In other words “What are you doing importing an .svg file? That doesn’t look like an ES6 module.”

We need to tell it “Shhh, shhh, it’s OK. My buddy Webpack is going to come by and fix everything. When he’s done, it will look like you’re importing a string, so don’t worry about it.” Translated into TypeScript:

declare module "*.svg" {
    const content: string;
    export default content;
}

Then it says “OHHHH got it, When I see a module ending with .svg, I can assume it’s going to have a default export with a type of string.”

Put this type declaration in a package that all the icon users will import from.

Icon package

I also made an icon package with all of the .svg files in it:

In package.json you can tell it to include the icons folder:

Then you can import from “package-name/icons/Circle.svg”

The result

After we implemented this icon system, we dropped our critical bundle size by ~210KB, which improved our PLT1 by 100ms.

It’s also easy to add SVGs as you just check them into the repository and import them as strings, which have been automatically streamlined in the Webpack step.

How to Use Individual Code Signing Certificates to get rid of SmartScreen warnings

The problem

Windows SmartScreen has for a while been trying to keep malware at bay. One of the ways of doing that is putting up a big scary warning when you try to run anything they haven’t validated as safe:

You have to click “More info” then “Run anyway” to actually run it. Eventually an executable will gain enough installs for it to be deemed “safe”, but the first users to run your program will hit this hurdle. It’s not a huge problem if you have a lot of overall downloads but it can be troublesome for beta builds where releases are more frequent and the audience is smaller. Every time you release, you start all over again from zero trust.

The solution: Code signing

To solve this, you can digitally sign your executables, which allows you to benefit from the trust you earned from your other releases. That means if your certificate is trusted, any releases done with it are trusted. The downside is that code signing certificates are pricey. For a business, it’s not a huge issue to pay $500 a year to get a code signing certificate in the company’s name. But it’s a significant burden to open source developers who are just doing it in their spare time, giving software away for free.

But what about cheaper code signing?

I looked for alternatives. SignPath looked somewhat promising, but as far as I can tell it’s just some alternate software validation system and doesn’t inform SmartScreen decisions. Certum has a fairly cheap open source code signing cert, but requires special hardware, and I had heard horror stories about Certum support never getting back to you.

I eventually settled on the Comodo Individual Code Signing Certificate. $71 per year is something I can afford to just pay out of pocket.

How does it work?

Basically the system is: you buy the cert, you send them a selfie with your driver’s license to prove you are who you say you are, then they issue a certificate not to a company, but to you, personally. That means your real name must show up on the certificate! They will put your street address on the cert by default, but you can (and probably should) open a ticket on Sectigo support to leave off that part while the validation phase is happening. Your zip code has to be on the cert though.

After validation, they send you en email with instructions on how to pick up the cert from Firefox or IE 11. In my case I installed via IE11 and the certificate ended up in certmgr.msc under Personal/Certificates. The certificate name is your full name. You can right click -> export -> include private key -> .pfx format -> choose a password, and choose AES256-SHA256.

Now you’ve got your .pfx file. (Don’t check it into source control.) You can use signtool.exe (comes with Visual Studio or the Windows 10 SDK) to sign executables. I used a command like this:

signtool.exe sign /f d:\certs\YourCert.pfx /p myCoolPassword /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 MyRadApp-1.0.exe

The /p argument is the password you exported the .pfx with. /fd SHA256 tells it to use the recommended, more secure file digest algorithm. /tr gives it a server that can timestamp when the file was signed. That would come in handy if your certificate expires, then everything signed by it when it was valid will still be valid, since it knows when the signing took place. /td SHA256 tells it to use the recommended, more secure timestamp digest algorithm.

After running the command, your .exe should be updated with the signed version.

Success!

After a while, my new releases stopped triggering the “this software may kill you” warnings. The cert is now trusted by SmartScreen.

I was initially unsure if it would work and was worried that maybe only the more expensive or super-expensive “Enhanced Validation” certs would be necessary. But thankfully not!

Microsoft.VisualStudio.ComponentModelHost.dll issues in Visual Studio extensions

I maintain a Visual Studio extension called Unit Test Boilerplate Generator. Recently I went to release a small fix for it but found that I could no longer build it since reformatting my PC. AppVeyor was also unable to build it after I upgraded it to a VS 2019 build agent.

It was complaining about not being able to find Microsoft.VisualStudio.ComponentModelHost.dll v 14.0.0.0 . I had it referenced like this:

<Reference Include="Microsoft.VisualStudio.ComponentModelHost, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />

Many times the answer to these old-style direct references is to change them into NuGet <PackageReference> . When I did that it defaulted to 16.9.227, but that made it incompatible with VS 2017. I downgraded it to 14.0.25424 , and then it built on VS 2019 but still ran on VS 2017. Success!

There is apparently magic binding redirect voodoo built into VS to point all of these references to old versions to the proper modern versions on later VS versions.

Installing .NET 5 Runtime Automatically with Inno Setup

In this guide I will walk through how to get the .NET 5 runtime to download and install on-the-fly in an Inno Setup installer.

It works in 3 steps:

  1. Detect if the desired .NET runtime is installed
  2. Download the .NET Runtime bootstrap installer with Inno Download Plugin
  3. Run the bootstrap installer in quiet mode, which will download and install the .NET Framework. This is better than downloading the full installer since it only downloads the files it needs for your platform.

You can download the regular .NET Runtime or the .NET Desktop Runtime, which supports Windows desktop applications. If you install the Desktop Runtime you do not need to also install the normal .NET Runtime.

Here’s the full code I’m using, with the x64 .NET 5 Desktop Runtime:

#include <idp.iss>

// Other parts of installer file go here

[CustomMessages]
IDP_DownloadFailed=Download of .NET 5 failed. .NET 5 Desktop runtime is required to run VidCoder.
IDP_RetryCancel=Click 'Retry' to try downloading the files again, or click 'Cancel' to terminate setup.
InstallingDotNetRuntime=Installing .NET 5 Desktop Runtime. This might take a few minutes...
DotNetRuntimeFailedToLaunch=Failed to launch .NET Runtime Installer with error "%1". Please fix the error then run this installer again.
DotNetRuntimeFailed1602=.NET Runtime installation was cancelled. This installation can continue, but be aware that this application may not run unless the .NET Runtime installation is completed successfully.
DotNetRuntimeFailed1603=A fatal error occurred while installing the .NET Runtime. Please fix the error, then run the installer again.
DotNetRuntimeFailed5100=Your computer does not meet the requirements of the .NET Runtime. Please consult the documentation.
DotNetRuntimeFailedOther=The .NET Runtime installer exited with an unexpected status code "%1". Please review any other messages shown by the installer to determine whether the installation completed successfully, and abort this installation and fix the problem if it did not.

[Code]

var
  requiresRestart: boolean;

function CompareVersion(V1, V2: string): Integer;
var
  P, N1, N2: Integer;
begin
  Result := 0;
  while (Result = 0) and ((V1 <> '') or (V2 <> '')) do
  begin
    P := Pos('.', V1);
    if P > 0 then
    begin
      N1 := StrToInt(Copy(V1, 1, P - 1));
      Delete(V1, 1, P);
    end
      else
    if V1 <> '' then
    begin
      N1 := StrToInt(V1);
      V1 := '';
    end
      else
    begin
      N1 := 0;
    end;
    P := Pos('.', V2);
    if P > 0 then
    begin
      N2 := StrToInt(Copy(V2, 1, P - 1));
      Delete(V2, 1, P);
    end
      else
    if V2 <> '' then
    begin
      N2 := StrToInt(V2);
      V2 := '';
    end
      else
    begin
      N2 := 0;
    end;
    if N1 < N2 then Result := -1
      else
    if N1 > N2 then Result := 1;
  end;
end;

function NetRuntimeIsMissing(): Boolean;
var
  runtimes: TArrayOfString;
  registryKey: string;
  I: Integer;
  meetsMinimumVersion: Boolean;
  meetsMaximumVersion: Boolean;
  minimumVersion: string;
  maximumExclusiveVersion: string;
begin
  Result := True;

  minimumVersion := '5.0.0';
  maximumExclusiveVersion := '5.1.0';
  registryKey := 'SOFTWARE\WOW6432Node\dotnet\Setup\InstalledVersions\x64\sharedfx\Microsoft.WindowsDesktop.App';
  if RegGetValueNames(HKLM, registryKey, runtimes) then
  begin
    for I := 0 to GetArrayLength(runtimes)-1 do
    begin
      meetsMinimumVersion := not (CompareVersion(runtimes[I], minimumVersion) = -1);
      meetsMaximumVersion := CompareVersion(runtimes[I], maximumExclusiveVersion) = -1;
      if meetsMinimumVersion and meetsMaximumVersion then
      begin
        Log(Format('[.NET] Selecting %s', [runtimes[I]]));
        Result := False;
          Exit;
      end;
    end;
  end;
end;

procedure InitializeWizard;
begin
  if NetRuntimeIsMissing() then
  begin
    idpAddFile('http://go.microsoft.com/fwlink/?linkid=2155258', ExpandConstant('{tmp}\NetRuntimeInstaller.exe'));
    idpDownloadAfter(wpReady);
  end;
end;

function InstallDotNetRuntime(): String;
var
  StatusText: string;
  ResultCode: Integer;
begin
  StatusText := WizardForm.StatusLabel.Caption;
  WizardForm.StatusLabel.Caption := CustomMessage('InstallingDotNetRuntime');
  WizardForm.ProgressGauge.Style := npbstMarquee;
  try
    if not Exec(ExpandConstant('{tmp}\NetRuntimeInstaller.exe'), '/passive /norestart /showrmui /showfinalerror', '', SW_SHOW, ewWaitUntilTerminated, ResultCode) then
    begin
      Result := FmtMessage(CustomMessage('DotNetRuntimeFailedToLaunch'), [SysErrorMessage(resultCode)]);
    end
    else
    begin
      // See https://msdn.microsoft.com/en-us/library/ee942965(v=vs.110).aspx#return_codes
      case resultCode of
        0: begin
          // Successful
        end;
        1602 : begin
          MsgBox(CustomMessage('DotNetRuntimeFailed1602'), mbInformation, MB_OK);
        end;
        1603: begin
          Result := CustomMessage('DotNetRuntimeFailed1603');
        end;
        1641: begin
          requiresRestart := True;
        end;
        3010: begin
          requiresRestart := True;
        end;
        5100: begin
          Result := CustomMessage('DotNetRuntimeFailed5100');
        end;
        else begin
          MsgBox(FmtMessage(CustomMessage('DotNetRuntimeFailedOther'), [IntToStr(resultCode)]), mbError, MB_OK);
        end;
      end;
    end;
  finally
    WizardForm.StatusLabel.Caption := StatusText;
    WizardForm.ProgressGauge.Style := npbstNormal;
    
    DeleteFile(ExpandConstant('{tmp}\NetRuntimeInstaller.exe'));
  end;
end;

function PrepareToInstall(var NeedsRestart: Boolean): String;
begin
  // 'NeedsRestart' only has an effect if we return a non-empty string, thus aborting the installation.
  // If the installers indicate that they want a restart, this should be done at the end of installation.
  // Therefore we set the global 'restartRequired' if a restart is needed, and return this from NeedRestart()

  if NetRuntimeIsMissing() then
  begin
    Result := InstallDotNetRuntime();
  end;
end;

function NeedRestart(): Boolean;
begin
  Result := requiresRestart;
end;

Detecting if the desired .NET Runtime is installed

Antoine Aflalo has found the registry key location to check to see what versions of the .NET runtime are installed.

Check SOFTWARE\dotnet\Setup\InstalledVersions\x86\sharedfx on x86 or SOFTWARE\WOW6432Node\dotnet\Setup\InstalledVersions\x64\sharedfx on x64. Use Microsoft.WindowsDesktop.App for the Desktop Runtime and Microsoft.NETCore.App for the regular runtime.

I am checking SOFTWARE\WOW6432Node\dotnet\Setup\InstalledVersions\x64\sharedfx\Microsoft.WindowsDesktop.App .

We are checking for a version installed >= 5.0.0 and < 5.1.0. Having .NET 6 installed would not run apps built for the .NET 5 Runtime.

Downloading the bootstrapper

First you need to locate the correct download link for the installer. This will depend on if you want the x86 or x64 version, what version you’re installing, and whether you want the normal or Desktop runtime. The Inno Dependency Installer source code has a good listing of stable links; I haven’t been able to find an official list yet.

You could also use the Inno Dependency Installer instead, though I like having slimmer installer code for just the framework I want to install, and I think checking the registry is more efficient than including another executable to run and check the version.

Now install Inno Download Plugin. This will make the idpAddFile and idpDownloadAfter calls inside InitializeWizard work. Remember that you need #include <idp.iss> at the top of your file to bring this in.

You will also need to add a line to the end of C:\Program Files (x86)\Inno Setup 6\ISPPBuiltins.iss . The Inno Download Plugin installer tries to do it but hasn’t been updated in a while and is still trying to update Inno Setup 5.

#pragma include __INCLUDE__ + ";" + "C:\Program Files (x86)\Inno Download Plugin"

This “InitializeWizard” method is a special one that InnoSetup calls when first setting up its wizard pages. We call our helper function to detect if the framework is installed, then schedule a download step after the “Ready to install” page. We include our direct download link determined earlier, and save it in a temp folder, with a file name of “NetRuntimeInstaller.exe”. This name I picked arbitrarily; we just need to refer to it later when we’re installing and cleaning up.

Installing the bootstrapper

Our code is activated on PrepareToInstall. When this function returns a string, that string is shown as an error message that stops the install from happening. We call into InstallDotNetRuntime and return its result.

Inside InstallDotNetRuntime we’re running the bootstrapper we downloaded earlier, with a flag to make the install passive (non interactive). The /showrmui option prompts the user to close applications to avoid a system restart. /showfinalerror tells the installer to show an error message if the install fails.

The .NET installer UI will show on top of the first window:

After running the installer, the bootstrapper is deleted (whether or not the install succeeded).

And now your installer is done!

Testing the installer

I would recommend setting up a Windows 10 VM in Hyper-V to test it out. You can go to “Apps and Features” and sort by install date to uninstall your app and the .NET Runtime to repeat the testing.

Thanks to Antony Male for suggesting updates to the error handling code.

AWS AppSync Resolvers – Finding what you’ve got in context

When you write your own Resolvers for AWS AppSync, you are given a $context object that contains a lot of helpful things. In my case I needed to grab a unique ID associated with the user from the Cognito User Pool I set up. But the $ctx.identity.cognitoIdentityId just wasn’t there. I needed to find out what I could use there.

Normally my response template looks like this:

#if($ctx.error)
    $utils.error($ctx.error.message, $ctx.error.type)
#end

$utils.toJson($utils.rds.toJsonObject($ctx.result)[1][0])

In my case it was telling me that a value was required. So I changed the template to spit out the whole context object instead of just the message:

#if($ctx.error)
    $utils.error($utils.toJson($ctx), $ctx.error.type)
#end

Then I went to Queries and ran it again, this time getting the whole context spat out as the “error”. A trip through a JSON unescape tool and formatting with the JSON Viewer Notepad++ plugin gave me a complete picture of what’s available.

Turns out I need to use sub or username, and the cognitoIdentityId field is nowhere to be found.

Implementing a Custom Window Title Bar in WPF

There are several good reasons for wanting custom window chrome in WPF, such as fitting in additional UI or implementing a Dark theme. However the actual implementation is kind of tricky, since it is now your job to provide a bunch of features that you used to get for free. I’ll walk you through my implementation.

Appearance

I chose to emulate the Windows 10 style.

Windows 10 standard title bar UI

This will keep my UI consistent with the rest of Windows. I am choosing not to attempt to match the style for old versions of Windows, as I don’t think that would be a great time investment. It’s one little thing you lose when going with a full custom title bar, but it should be worth it by allowing full cohesive theming. Buttons are 46 wide, 32 tall.

Building the UI

First, set WindowStyle=”None” on your Window. That will remove the built-in title bar and allow you to do everything on your own.

Next, allocate space for your title bar in your UI. I like to allocate a Grid row for it.

Add this under the <Window> node in your XAML:

<WindowChrome.WindowChrome>
    <WindowChrome CaptionHeight="32" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
</WindowChrome.WindowChrome>

The CaptionHeight tells the OS to treat the top 32px of your window as if it was a title bar. This means that click to drag works, along with double clicking to maximize/restore, shaking to minimize other windows, etc. The ResizeBorderThickness allows the standard window resize logic to work, so we don’t need to reimplement that either.

Now we need to make the actual UI. This is mine:

<Grid>
	<Grid.ColumnDefinitions>
		<ColumnDefinition Width="Auto" />
		<ColumnDefinition Width="*" />
		<ColumnDefinition Width="Auto" />
		<ColumnDefinition Width="Auto" />
		<ColumnDefinition Width="Auto" />
	</Grid.ColumnDefinitions>
	<Image
		Grid.Column="0"
		Width="22"
		Height="22"
		Margin="4"
		Source="/Icons/VidCoder32.png" />
	<TextBlock
		Grid.Column="1"
		Margin="4 0 0 0"
		VerticalAlignment="Center"
		FontSize="14"
		Text="{Binding WindowTitle}">
		<TextBlock.Style>
			<Style TargetType="TextBlock">
				<Style.Triggers>
					<DataTrigger Binding="{Binding IsActive, RelativeSource={RelativeSource AncestorType=Window}}" Value="False">
						<Setter Property="Foreground" Value="{DynamicResource WindowTitleBarInactiveText}" />
					</DataTrigger>
				</Style.Triggers>
			</Style>
		</TextBlock.Style>
	</TextBlock>

	<Button
		Grid.Column="2"
		Click="OnMinimizeButtonClick"
		RenderOptions.EdgeMode="Aliased"
		Style="{StaticResource TitleBarButtonStyle}">
		<Path
			Width="46"
			Height="32"
			Data="M 18,15 H 28"
			Stroke="{Binding Path=Foreground,
							 RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
			StrokeThickness="1" />
	</Button>
	<Button
		Name="maximizeButton"
		Grid.Column="3"
		Click="OnMaximizeRestoreButtonClick"
		Style="{StaticResource TitleBarButtonStyle}">
		<Path
			Width="46"
			Height="32"
			Data="M 18.5,10.5 H 27.5 V 19.5 H 18.5 Z"
			Stroke="{Binding Path=Foreground,
							 RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
			StrokeThickness="1" />
	</Button>
	<Button
		Name="restoreButton"
		Grid.Column="3"
		Click="OnMaximizeRestoreButtonClick"
		Style="{StaticResource TitleBarButtonStyle}">
		<Path
			Width="46"
			Height="32"
			Data="M 18.5,12.5 H 25.5 V 19.5 H 18.5 Z M 20.5,12.5 V 10.5 H 27.5 V 17.5 H 25.5"
			Stroke="{Binding Path=Foreground,
							 RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
			StrokeThickness="1" />
	</Button>
	<Button
		Grid.Column="4"
		Click="OnCloseButtonClick"
		Style="{StaticResource TitleBarCloseButtonStyle}">
		<Path
			Width="46"
			Height="32"
			Data="M 18,11 27,20 M 18,20 27,11"
			Stroke="{Binding Path=Foreground,
							 RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
			StrokeThickness="1" />
	</Button>
</Grid>

I’ve got the app icon. I chose not to implement the special drop-down menu that comes with the standard title bar since it’s not often used and other major apps like Visual Studio Code don’t bother with it. But it’s certainly something you could add.

The title text has a trigger to change its color based on the “Active” state of the window. That allows the user to better tell if the window has focus or not.

The actual buttons use TitleBarButtonStyle and TitleBarCloseButtonStyle:

<Style x:Key="TitleBarButtonStyle" TargetType="Button">
	<Setter Property="Foreground" Value="{DynamicResource WindowTextBrush}" />
	<Setter Property="Padding" Value="0" />
	<Setter Property="WindowChrome.IsHitTestVisibleInChrome" Value="True" />
	<Setter Property="IsTabStop" Value="False" />
	<Setter Property="Template">
		<Setter.Value>
			<ControlTemplate TargetType="{x:Type Button}">
				<Border
					x:Name="border"
					Background="Transparent"
					BorderThickness="0"
					SnapsToDevicePixels="true">
					<ContentPresenter
						x:Name="contentPresenter"
						Margin="0"
						HorizontalAlignment="Center"
						VerticalAlignment="Center"
						Focusable="False"
						RecognizesAccessKey="True" />
				</Border>
				<ControlTemplate.Triggers>
					<Trigger Property="IsMouseOver" Value="true">
						<Setter TargetName="border" Property="Background" Value="{DynamicResource MouseOverOverlayBackgroundBrush}" />
					</Trigger>
					<Trigger Property="IsPressed" Value="true">
						<Setter TargetName="border" Property="Background" Value="{DynamicResource PressedOverlayBackgroundBrush}" />
					</Trigger>
				</ControlTemplate.Triggers>
			</ControlTemplate>
		</Setter.Value>
	</Setter>
</Style>

<Style x:Key="TitleBarCloseButtonStyle" TargetType="Button">
	<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
	<Setter Property="Padding" Value="0" />
	<Setter Property="WindowChrome.IsHitTestVisibleInChrome" Value="True" />
	<Setter Property="IsTabStop" Value="False" />
	<Setter Property="Template">
		<Setter.Value>
			<ControlTemplate TargetType="{x:Type Button}">
				<Border
					x:Name="border"
					Background="Transparent"
					BorderThickness="0"
					SnapsToDevicePixels="true">
					<ContentPresenter
						x:Name="contentPresenter"
						Margin="0"
						HorizontalAlignment="Center"
						VerticalAlignment="Center"
						Focusable="False"
						RecognizesAccessKey="True" />
				</Border>
				<ControlTemplate.Triggers>
					<Trigger Property="IsMouseOver" Value="true">
						<Setter TargetName="border" Property="Background" Value="{DynamicResource MouseOverWindowCloseButtonBackgroundBrush}" />
						<Setter Property="Foreground" Value="{DynamicResource MouseOverWindowCloseButtonForegroundBrush}" />
					</Trigger>
					<Trigger Property="IsPressed" Value="true">
						<Setter TargetName="border" Property="Background" Value="{DynamicResource PressedWindowCloseButtonBackgroundBrush}" />
						<Setter Property="Foreground" Value="{DynamicResource MouseOverWindowCloseButtonForegroundBrush}" />
					</Trigger>
				</ControlTemplate.Triggers>
			</ControlTemplate>
		</Setter.Value>
	</Setter>
</Style>

These are buttons with stripped-down control templates to remove a lot of the extra gunk. They have triggers to change the background color on mouse over (and the foreground color in the case of the Close button). Also they set WindowChrome.IsHitTestVisibleInChrome to True, which allows them to pick up clicks even though they are in the 32px caption area we set up earlier.

The button content itself uses <Path> to draw the icons. The minimize button uses RenderOptions.EdgeMode=”Aliased” to disable anti-aliasing and make sure it renders crisply without blurring over into other pixels. I set the Stroke to pick up the Foreground color from the parent button. The Path data for the maximize/restore buttons are all set on .5 to make sure it renders cleanly at the standard 96 DPI. With whole numbers it ends up drawing on the edge of the pixel and blurring the lines. We can’t use the same “Aliased” trick here as that might cause the pixel count for different lines to change and look off at different zoom levels like 125%/150%.

Looking good!

Responding to button clicks

Now that we have the UI in place, we need to respond to those button clicks. I normally use databinding/MVVM, but in this case I decided to bypass the viewmodel since these are really talking directly to the window.

Event handler methods:

private void OnMinimizeButtonClick(object sender, RoutedEventArgs e)
{
	this.WindowState = WindowState.Minimized;
}

private void OnMaximizeRestoreButtonClick(object sender, RoutedEventArgs e)
{
	if (this.WindowState == WindowState.Maximized)
	{
		this.WindowState = WindowState.Normal;
	}
	else
	{
		this.WindowState = WindowState.Maximized;
	}
}

private void OnCloseButtonClick(object sender, RoutedEventArgs e)
{
	this.Close();
}

Helper method to refresh the maximize/restore button:

private void RefreshMaximizeRestoreButton()
{
	if (this.WindowState == WindowState.Maximized)
	{
		this.maximizeButton.Visibility = Visibility.Collapsed;
		this.restoreButton.Visibility = Visibility.Visible;
	}
	else
	{
		this.maximizeButton.Visibility = Visibility.Visible;
		this.restoreButton.Visibility = Visibility.Collapsed;
	}
}

This, we call in the constructor and in an event handler for Window.StateChanged:

private void Window_StateChanged(object sender, EventArgs e)
{
	this.RefreshMaximizeRestoreButton();
}

This will make sure the button displays correctly no matter how the window state change is invoked.

Maximized placement

You thought we were done? Hah. Windows has other plans. You might notice that when you maximize your window, some content is getting cut off and it’s hiding your task bar. The default values it picks for maximized window placement are really weird, where it cuts off 7px of your window content and doesn’t account for task bar placement.

To fix this, we need to listen for the WM_GETMINMAXINFO WndProc message to tell our window it needs to go somewhere different when maximize. Put this in your window codebehind:

protected override void OnSourceInitialized(EventArgs e)
{
	base.OnSourceInitialized(e);
	((HwndSource)PresentationSource.FromVisual(this)).AddHook(HookProc);
}

public static IntPtr HookProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
	if (msg == WM_GETMINMAXINFO)
	{
		// We need to tell the system what our size should be when maximized. Otherwise it will cover the whole screen,
		// including the task bar.
		MINMAXINFO mmi = (MINMAXINFO)Marshal.PtrToStructure(lParam, typeof(MINMAXINFO));

		// Adjust the maximized size and position to fit the work area of the correct monitor
		IntPtr monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);

		if (monitor != IntPtr.Zero)
		{
			MONITORINFO monitorInfo = new MONITORINFO();
			monitorInfo.cbSize = Marshal.SizeOf(typeof(MONITORINFO));
			GetMonitorInfo(monitor, ref monitorInfo);
			RECT rcWorkArea = monitorInfo.rcWork;
			RECT rcMonitorArea = monitorInfo.rcMonitor;
			mmi.ptMaxPosition.X = Math.Abs(rcWorkArea.Left - rcMonitorArea.Left);
			mmi.ptMaxPosition.Y = Math.Abs(rcWorkArea.Top - rcMonitorArea.Top);
			mmi.ptMaxSize.X = Math.Abs(rcWorkArea.Right - rcWorkArea.Left);
			mmi.ptMaxSize.Y = Math.Abs(rcWorkArea.Bottom - rcWorkArea.Top);
		}

		Marshal.StructureToPtr(mmi, lParam, true);
	}

	return IntPtr.Zero;
}

private const int WM_GETMINMAXINFO = 0x0024;

private const uint MONITOR_DEFAULTTONEAREST = 0x00000002;

[DllImport("user32.dll")]
private static extern IntPtr MonitorFromWindow(IntPtr handle, uint flags);

[DllImport("user32.dll")]
private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi);

[Serializable]
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
	public int Left;
	public int Top;
	public int Right;
	public int Bottom;

	public RECT(int left, int top, int right, int bottom)
	{
		this.Left = left;
		this.Top = top;
		this.Right = right;
		this.Bottom = bottom;
	}
}

[StructLayout(LayoutKind.Sequential)]
public struct MONITORINFO
{
	public int cbSize;
	public RECT rcMonitor;
	public RECT rcWork;
	public uint dwFlags;
}

[Serializable]
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
	public int X;
	public int Y;

	public POINT(int x, int y)
	{
		this.X = x;
		this.Y = y;
	}
}

[StructLayout(LayoutKind.Sequential)]
public struct MINMAXINFO
{
	public POINT ptReserved;
	public POINT ptMaxSize;
	public POINT ptMaxPosition;
	public POINT ptMinTrackSize;
	public POINT ptMaxTrackSize;
}

When the system asks the window where it should be when it maximizes, this code will ask what monitor it’s on, then place itself in the work area of the monitor (not overlapping the task bar).

Declare ourselves DPI-aware

In order to get the WM_GETMINMAXINFO handler to behave correctly, we need to declare our app as per-monitor DPI aware to the unmanaged system.

Right click on your project and select Add -> New Item -> Application Manifest File (Windows Only) .

Add these settings to declare us as properly DPI-aware:

  <application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings>
      <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitor</dpiAwareness>
      <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
    </windowsSettings>
  </application>

Otherwise with multi-monitor setups you can get maximized windows that stretch off the screen. This setting is a little confusing because WPF is by default DPI aware and normally works just fine out of the box on multiple monitors. But this is required to let the unmanaged APIs that we are hooking into know how WPF is handling things.

Window border

Finally, the window can be kind of hard to pick out when it doesn’t have a border, if it’s put against the wrong background. Let’s fix that now. Wrap your window UI in this:

<Border Style="{StaticResource WindowMainPanelStyle}">
    ...your UI
</Border>

This is the style:

<Style x:Key="WindowMainPanelStyle" TargetType="{x:Type Border}">
    <Setter Property="BorderBrush" Value="{DynamicResource WindowBorderBrush}" />
    <Setter Property="BorderThickness" Value="1" />
    <Style.Triggers>
        <DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=WindowState}" Value="Maximized">
            <Setter Property="BorderThickness" Value="0" />
        </DataTrigger>
    </Style.Triggers>
</Style>

This will remove the 1px border when the window is maximized.

Okay, now we’re actually done

At least until Windows decides they want to shake up the title bar style again.

Dark Theme in WPF

In a recent Windows 10 update a toggle switch was added to allow the user to specify that they wanted “Dark” themes in apps:

I decided to add support for this to VidCoder. But no updates to WPF were made to make this easy. WPF does having theming support where it can pull in different .xaml resource files from a Themes folder, but this is all Luna/Aero/Win10 styling that is automatically applied based on your OS. After digging in the WPF source code I determined that there’s no way to hook in your own theme to this or manually alter which theme is loaded.

Furthermore, the actual dark theme setting is not exposed in a friendly manner to WPF. But we can still do it. Let’s get started.

Detecting Windows Dark Mode setting + High Contrast

The first step is finding out what theme we should be applying. To do that we need to tell what dark mode choice the user has made and detect when it’s changed. We also need to tell if the user has turned on High Contrast and detect when it’s changed.

Dark mode detection

To get the windows theme we need to poke into the registry. I used a WMI query to watch the registry for changes so we can update the app theme when they change the setting.

private const string RegistryKeyPath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";

private const string RegistryValueName = "AppsUseLightTheme";

private enum WindowsTheme
{
	Light,
	Dark
}

public void WatchTheme()
{
	var currentUser = WindowsIdentity.GetCurrent();
	string query = string.Format(
		CultureInfo.InvariantCulture,
		@"SELECT * FROM RegistryValueChangeEvent WHERE Hive = 'HKEY_USERS' AND KeyPath = '{0}\\{1}' AND ValueName = '{2}'",
		currentUser.User.Value,
		RegistryKeyPath.Replace(@"\", @"\\"),
		RegistryValueName);

	try
	{
		var watcher = new ManagementEventWatcher(query);
		watcher.EventArrived += (sender, args) =>
		{
			WindowsTheme newWindowsTheme = GetWindowsTheme();
			// React to new theme
		};

		// Start listening for events
		watcher.Start();
	}
	catch (Exception)
	{
		// This can fail on Windows 7
	}

	WindowsTheme initialTheme = GetWindowsTheme();
}

private static WindowsTheme GetWindowsTheme()
{
	using (RegistryKey key = Registry.CurrentUser.OpenSubKey(RegistryKeyPath))
	{
		object registryValueObject = key?.GetValue(RegistryValueName);
		if (registryValueObject == null)
		{
			return WindowsTheme.Light;
		}

		int registryValue = (int)registryValueObject;

		return registryValue > 0 ? WindowsTheme.Light : WindowsTheme.Dark;
	}
}

High Contrast detection

Our app will have 3 “modes”: Light, Dark and High Contrast. In High Contrast we’ll reference a limited set of system colors which will come from the user’s settings:

We’ll need to find if High Contrast is enabled and when it changes:

bool isHighContrast = SystemParameters.HighContrast;
SystemParameters.StaticPropertyChanged += (sender, args) =>
{
	if (args.PropertyName == nameof(SystemParameters.HighContrast))
	{
		bool newIsHighContrast = SystemParameters.HighContrast;
	}
};

Combining the values

We want to end up with a value from this enum:

public enum AppTheme
{
	Light,
	Dark,
	HighContrast
}

You’ll need to use logic to combine light/dark with the High Contrast bool to get the theme and react to changes. I used ReactiveX Observables but you might have something else. Anyhow, if High Contrast is enabled, use that theme. Otherwise, use the Light/Dark as indicated by the registry key setting.

Setting up theme swapping

After we know what theme we want, we now need to apply it. Fortunately WPF has an excellent mechanism to swap out styles: ResourceDictionaries. Make your dictionaries like so:

Then inside each:

<ResourceDictionary
	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
	<SolidColorBrush x:Key="MyBackgroundBrush" Color="#8EC2FA" />
</ResourceDictionary>

We can specify the “default” style by including Light.xaml in App.xaml:

<Application.Resources>
	<ResourceDictionary>
		<ResourceDictionary.MergedDictionaries>
			<ResourceDictionary Source="/Themes/Light.xaml" />
		</ResourceDictionary.MergedDictionaries>

		<!-- Other App-level items -->

That will make the designer happy. Though obviously we don’t want to have that theme all the time. We can swap out the theme before opening any windows in App.xaml.cs:

this.Resources.MergedDictionaries[0].Source =
    new Uri($"/Themes/{appTheme}.xaml", UriKind.Relative);

That way the app loads up with the correct theme. You can also run this code when the user updates the theme.

Populating the theme dictionaries

The only thing you want to put into the theme dictionaries are Brush resources (and maybe Colors if you need them). Dark and Light should have whatever looks good to you. For High Contrast, we should pick from the official SystemColors. For example, if you had a special window background brush, you’d want to define it as this in HighContrast.xaml:

<SolidColorBrush x:Key="MyBackgroundBrush" Color="{x:Static SystemColors.WindowColor}" />

If you choose from SystemColors, it will work for any High Contrast variant the user chooses, even if they customize it.

Another trick you can use in Dark.xaml is overriding system colors with your own:

<SolidColorBrush x:Key="{x:Static SystemColors.WindowBrushKey}" Color="Black" />
<SolidColorBrush x:Key="{x:Static SystemColors.WindowTextBrushKey}" Color="White" />

This causes any standard control that uses the system colors to use the one you’ve supplied.

Referencing theme colors

Now to use a theme color you should always refer to it like so:

{DynamicResource MyBackgroundBrush}

A StaticResource never changes, but a DynamicResource reference can update, which comes in handy when the users switches on/off dark mode or High Contrast.

Theming built-in controls

While the SystemColor overrides I mentioned earlier can help a lot with re-theming built-in controls, unfortunately there are a lot of controls that don’t pay any attention to system colors.

Button, for example is one of them. Unfortunately, there doesn’t appear to be any way to override these colors. The only way to re-skin them is to make a copy of their ControlTemplate and insert references to our own themed colors. Unfortunately that means that the controls will now look the same no matter what version of Windows the user has, but I don’t know of any way around it. I based mine off the styling in Windows 10, just so it would look consistent in the latest version and stay “current” longer.

Anyway, there’s a couple ways to do it: One by copying the Styles and Templates documentation for the control you want to theme. Another is by right clicking on the control in the designer, then Edit Template -> Edit a Copy.

You’ll want to park the style with ControlTemplate override in App.xaml (Or another dictionary referenced by App.xaml):

<Style x:Key="ButtonBaseStyle" TargetType="Button">
	<Setter Property="Background" Value="{DynamicResource Button.Static.Background}" />
	<Setter Property="BorderBrush" Value="{DynamicResource Button.Static.Border}" />
	<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
	<Setter Property="BorderThickness" Value="1" />
	<Setter Property="HorizontalContentAlignment" Value="Center" />
	<Setter Property="VerticalContentAlignment" Value="Center" />
	<Setter Property="Padding" Value="1" />
	<Setter Property="Template">
		<Setter.Value>
			<ControlTemplate TargetType="{x:Type Button}">
				<Border
					x:Name="border"
					Background="{TemplateBinding Background}"
					BorderBrush="{TemplateBinding BorderBrush}"
					BorderThickness="{TemplateBinding BorderThickness}"
					SnapsToDevicePixels="true">
					<ContentPresenter
						x:Name="contentPresenter"
						Margin="{TemplateBinding Padding}"
						HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
						VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
						Focusable="False"
						RecognizesAccessKey="True"
						SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
				</Border>
				<ControlTemplate.Triggers>
					<Trigger Property="IsDefaulted" Value="true">
						<Setter TargetName="border" Property="BorderBrush" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}" />
					</Trigger>
					<Trigger Property="IsMouseOver" Value="true">
						<Setter TargetName="border" Property="Background" Value="{DynamicResource Button.MouseOver.Background}" />
						<Setter TargetName="border" Property="BorderBrush" Value="{DynamicResource Button.MouseOver.Border}" />
					</Trigger>
					<Trigger Property="IsPressed" Value="true">
						<Setter TargetName="border" Property="Background" Value="{DynamicResource Button.Pressed.Background}" />
						<Setter TargetName="border" Property="BorderBrush" Value="{DynamicResource Button.Pressed.Border}" />
					</Trigger>
					<Trigger Property="IsEnabled" Value="false">
						<Setter TargetName="border" Property="Background" Value="{DynamicResource Button.Disabled.Background}" />
						<Setter TargetName="border" Property="BorderBrush" Value="{DynamicResource Button.Disabled.Border}" />
						<Setter TargetName="contentPresenter" Property="TextElement.Foreground" Value="{DynamicResource Button.Disabled.Foreground}" />
					</Trigger>
				</ControlTemplate.Triggers>
			</ControlTemplate>
		</Setter.Value>
	</Setter>
</Style>
<Style BasedOn="{StaticResource ButtonBaseStyle}" TargetType="Button" />

I ripped out the FocusVisualStyle override since the default one seems to work fine for all themes. I also include both a “Base” style and an implicit style (that has no key). Controls that don’t specify a style will pick up the implicit style, while control that do need to specify a style can base it on the Base style.

I also moved all the “Brush” resources out to the Theme dictionaries where they belong, and changed all the references to {DynamicResource}.

One thing to watch out for is that some overrides like ComboBox will reference a control from PresentationFramework.Aero2 in their ControlTemplate. If you see a namespace brought in that references Aero2, that means your program won’t work on Windows 7. To keep it compatible, delete the “2” to reference PresentationFramework.Aero.

There’s a lot more “grunt work” involved in picking all the colors, but that covers the overall strategy I used to convert all the app UI. Here’s our new, themed UI:

Window title bar

Now that we’ve converted all of our UI, we might see something like this:

All of the content is themed, but the title bar isn’t. It doesn’t look the best. We can fix it, but unfortunately the only way to do that is to implement the title bar from scratch all on our own. That is a whole separate journey that I go over in another post.

Here’s what we get with our custom title bar:

A pain to implement, but at least now it looks less like garbage.

Source reference

You can check out VidCoder’s source code for reference (beta branch).