Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions AsyncSingleFileGenerator/AsyncSingleFileGeneratorSample.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27620.3002
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SingleFileGeneratorSample", "src\SingleFileGeneratorSample.csproj", "{154F8BFA-6D1D-4FEF-95AC-95D4222B34DA}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D8854F3A-BEAA-44E0-A140-413225798789}"
ProjectSection(SolutionItems) = preProject
README.md = README.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{154F8BFA-6D1D-4FEF-95AC-95D4222B34DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{154F8BFA-6D1D-4FEF-95AC-95D4222B34DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{154F8BFA-6D1D-4FEF-95AC-95D4222B34DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{154F8BFA-6D1D-4FEF-95AC-95D4222B34DA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C1C1BBB7-1562-4820-8E03-795009DFE966}
EndGlobalSection
EndGlobal
21 changes: 21 additions & 0 deletions AsyncSingleFileGenerator/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2015 Microsoft

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
153 changes: 153 additions & 0 deletions AsyncSingleFileGenerator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Single File Generator sample

**Applies to Visual Studio 2017 and newer**

This example shows how to write an Asynchronous Single File Generator in a Visual Studio extension that will output a nested file when the parent file is modified.

> [!NOTE]
> Please note that there are two flavours of Single File Generators (aka Custom Tools) in Visual Studio:
> * Single File Generators that run synchronously on the UI thread
> * Asynchronous Single File Generators that run on a background thread (VS 2017 and newer)

This sample demonstrates how to create an **Asynchronous Single File Generator** that is compatible with synchrouns one and tries to use async APIs where possible otherwise will fallback to sync APIs.

To test the asynchronous behavior you will need to apply the code generator to a CPS-based project like a .NET Core project, or any SDK-style project.

Clone the repo to test out the sample in Visual Studio 2017 yourself.

## What is a Single File Generator
A Single File Generator is a mechanism that will auto-create and nest an output file when the source file changes. In this sample, the generator is applied to a **.js** file that will then output a **.min.js** file like so:

![Nested file](art/code-behind.png)

It is also known as a Custom Tool which can be manually set in the properties of supported files.

![Property Grid](art/property-grid.png)

The most well-known examples of existing generators are the ones creating a strongly typed C#/VB nested file for .resx files.

Every time the file with the Custom Tool property is modified, the Single File Generator will execute to update the nested file.

The nested file can be of any type - code, image, etc. - the sky is the limit.

## Let's get started
Before we begin, make sure you have created a VSIX project in Visual Studio. See how to [create a VSIX project](https://docs.microsoft.com/en-us/visualstudio/extensibility/extensibility-hello-world) if you don't already have one ready.

### Install NuGet package
The base classes for the Single File Generator are located in the [Microsoft.VisualStudio.TextTemplating.VSHost.15.0](https://www.nuget.org/packages/Microsoft.VisualStudio.TextTemplating.VSHost.15.0/) NuGet package, so go ahead and install that into your VSIX project.

We also need the [Nuglify](https://www.nuget.org/packages/NUglify/) NuGet package that can minify JavaScript.

### The generator
The generator is a simple class that inherits from the *BaseCodeGeneratorWithSite* and has 2 methods for us to implement.

```c#
using Microsoft.VisualStudio.TextTemplating.VSHost;

[Guid("82ca81c8-b507-4ba1-a33d-ff6cdad20e36")] // change this GUID
public sealed class MinifyCodeGenerator : BaseCodeGeneratorWithSite
{
public override string GetDefaultExtension()
{
return ".min.js";
}

protected override byte[] GenerateCode(string inputFileName, string inputFileContent)
{
UglifyResult minified = Uglify.Js(inputFileContent);
return Encoding.UTF8.GetBytes(minified.Code);
}
}
```

[See full generator class in the source](src/Generators/MinifyGenerator.cs).

That's it, you now have a Single File Generator that writes a .min.js file with the minified content of the source .js file. Now we must register the generator to make it work.

### Registering the generator
Decorate your *Package* class with the `ProvideCodeGenerator` attribute.

```c#
[ProvideCodeGenerator(typeof(MinifyCodeGenerator), nameof(MinifyCodeGenerator), "Minifies JavaScript", true)]
public class VSPackage : AsyncPackage
{
...
}
```

[See full Package class in the source](src/VSPackage.cs).

> Note: if you don't have a *Package* class, add one to your project using the Add New Item dialog. The template is called *Visual Studio AsyncPackage* in VS 2017.7

Now the generator is registered, and you can now manually give the Custom Tool property on .js files the *MinifyCodeGenerator* value.

That's it. We've now implemented a Single File Generator that minifies JavaScript files.

However, it would be much easier if we give our users a command in the context-menu of files in Solution Explorer to add the value for them so they don't have to type *MinifyCodeGenerator* in the Property Grid manually.

### Add the command button
In the .VSCT file you must specify a new button. It could look like this:

```c#
<Button guid="guidPackageCmdSet" id="ApplyCustomToolId" priority="0x0100" type="Button">
<Parent guid="guidSHLMainMenu" id="IDG_VS_CTXT_ITEM_INCLUDEEXCLUDE"/>
<Strings>
<ButtonText>Minify File</ButtonText>
</Strings>
</Button>
```

[See full .vsct file in the source](src/VSCommandTable.vsct).

That will place the button in the context-menu in Solution Explorer.

![Context Menu](art/context-menu.png)

Then we need to add the command handler C# file. It will look similar to this:

```c#
internal sealed class ApplyCustomTool
{
private const int _commandId = 0x0100;
private static readonly Guid _commandSet = new Guid("4aaf93c0-70ae-4a4b-9fb6-1ad3997a9adf");
private static DTE _dte;

public static async Task InitializeAsync(AsyncPackage package)
{
_dte = await package.GetServiceAsync(typeof(DTE)) as DTE;

var commandService = await package.GetServiceAsync((typeof(IMenuCommandService))) as IMenuCommandService;
var cmdId = new CommandID(_commandSet, _commandId);
var cmd = new MenuCommand(OnExecute, cmdId)
commandService.AddCommand(cmd);
}

private static void OnExecute(object sender, EventArgs e)
{
ProjectItem item = _dte.SelectedItems.Item(1).ProjectItem;
item.Properties.Item("CustomTool").Value = nameof(MinifyCodeGenerator);
}
}
```

[See full command handler in the source](src/Commands/ApplyCustomTool.cs).

And then finally initialize the command handler from the *Package* initialization method.

```c#
protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
{
await ApplyCustomTool.InitializeAsync(this);
}
```

[See full Package class in the source](src/VSPackage.cs).

### Single File Generators in the wild
Here are more samples of open source extensions implementing Single File Generators.

* [VSIX Synchronizer](https://github.com/madskristensen/VsixSynchronizer)
* [Extensibility Tools](https://github.com/madskristensen/extensibilitytools)

## License
[Apache 2.0](LICENSE)
Binary file added AsyncSingleFileGenerator/art/code-behind.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added AsyncSingleFileGenerator/art/context-menu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added AsyncSingleFileGenerator/art/property-grid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 44 additions & 0 deletions AsyncSingleFileGenerator/src/Commands/ApplyCustomTool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
namespace AsyncSingleFileGeneratorSample;

using System;
using System.ComponentModel.Design;
using System.IO;
using Microsoft.VisualStudio.Shell;
using Task = System.Threading.Tasks.Task;
using Microsoft.VisualStudio.Interop;
using EnvDTE;

internal sealed class ApplyCustomTool
{
private const int _commandId = 0x0100;
private static readonly Guid _commandSet = new Guid("4aaf93c0-70ae-4a4b-9fb6-1ad3997a9adf");
private static DTE _dte;

public static async Task InitializeAsync(AsyncPackage package)
{
ThreadHelper.ThrowIfNotOnUIThread();

_dte = await package.GetServiceAsync(typeof(DTE)) as DTE;

var commandService = await package.GetServiceAsync((typeof(IMenuCommandService))) as IMenuCommandService;
var cmdId = new CommandID(_commandSet, _commandId);

var cmd = new OleMenuCommand(OnExecute, cmdId)
{
// This will defer visibility control to the VisibilityConstraints section in the .vsct file
Supported = false
};

commandService.AddCommand(cmd);
}

private static void OnExecute(object sender, EventArgs e)
{
ProjectItem item = _dte.SelectedItems.Item(1).ProjectItem;

if (item != null)
{
item.Properties.Item("CustomTool").Value = MinifyCodeGenerator.Name;
}
}
}
Loading