how to disable T4 template auto-run in visual studio (2012)? - visual-studio

I have some T4 templates in my project. Whenever I make changes and save the tt file, it auto update the generated files. This is a template that loops all tables in a database and generates about 100+ files. So visual studio hangs for a few seconds every time I save my template and this is annoying. Is there a way to disable to "auto-refresh" function and I can manually run the template through the context menu.
Thanks!

You could delete TextTemplatingFileGenerator under "Custom Tool" in the file's Properties while you are editing it, and then put it back when you are finished.

I had a similiar issue. I found a quick work around by creating a ttinclude file (actually this was already a standard include file containing utility functions for my templates) and including it in all of my T4 templates. Then I simply created a compiler error in the include file. Thus when the generator attempted to run it would simply fail on the compile. Then when I'm ready to actually generate, I get rid of the offending code and then generate.
e.g. To cause a failure:
<#+
#
#>
To disable the failure:
<#+
//#
#>
You can also use this trick in the T4 template itself if you just want to disable the one you're working on.
Hopefully future VS versions will allow you to simply disable the auto-transform.

Since the TT is always executed (still), I found a different way to control the output when the TT is executed.
/********SET THIS TO REGENERATE THE FILE (OR NOT) ********/
var _RegenerateFile = true;
/********COS VS ALWAYS REGENERATES ON SAVE ***************/
// Also, T4VSHostProcess.exe may lock files.
// Kill it from task manager if you get "cannot copy file in use by another process"
var _CurrentFolder = new FileInfo(Host.ResolvePath(Host.TemplateFile)).DirectoryName;
var _AssemblyLoadFolder = Path.Combine(_CurrentFolder, "bin\\Debug");
Directory.SetCurrentDirectory(_CurrentFolder);
Debug.WriteLine($"Using working folder {_CurrentFolder}");
if (_RegenerateFile == false)
{
Debug.WriteLine($"Not Regenerating File");
var existingFileName = Path.ChangeExtension(Host.TemplateFile, "cs");
var fileContent = File.ReadAllText(existingFileName);
return fileContent;
}
Debug.WriteLine($"Regenerating File"); //put the rest of your usual template
Another way (what I eventually settled on) is based on reading a conditional compilation symbol that sets a property on one of the the classes that is providing the data for the T4. This gives the benefit of skipping all that preparation (and IDE lag) unless you add the REGEN_CODE_FILES conditional compilation symbol. (I guess this could also be made into a new solution configuration too. yes, this does work and removes the need for the class change below)
An example of the class i am calling in the same assembly..
public class MetadataProvider
{
public bool RegenCodeFile { get; set; }
public MetadataProvider()
{
#if REGEN_CODE_FILES
RegenCodeFile = true; //try to get this to set the property
#endif
if (RegenCodeFile == false)
{
return;
}
//code that does some degree of preparation and c...
}
}
In the TT file...
var _MetaProvider = new MetadataProvider();
var _RegenerateFile = _MetaProvider.RegenCodeFile;
// T4VSHostProcess.exe may lock files.
// Kill it from task manager if you get "cannot copy file in use by another process"
var _CurrentFolder = new FileInfo(Host.ResolvePath(Host.TemplateFile)).DirectoryName;
var _AssemblyLoadFolder = Path.Combine(_CurrentFolder, "bin\\Debug");
Directory.SetCurrentDirectory(_CurrentFolder);
Debug.WriteLine($"Using working folder {_CurrentFolder}");
if (_RegenerateFile == false)
{
Debug.WriteLine($"Not Regenerating File");
var existingFileName = Path.ChangeExtension(Host.TemplateFile, "cs");
var fileContent = File.ReadAllText(existingFileName);
return fileContent;
}
Debug.WriteLine($"Regenerating File");

Related

WiX project is never up-to-date

I have an installer (.msi) project that uses Wix Toolset v3.14. For some reason it is never up-to-date -- i.e. building it again always produces some activity (C:\Program Files (x86)\WiX Toolset v3.14\bin\Light.exe gets called, but not candle.exe). Is there any way to track down and fix the cause?
Here is what I observe when detailed output is ON:
Target "ReadPreviousBindInputsAndBuiltOutputs" in file "C:\Program Files (x86)\MSBuild\Microsoft\WiX\v3.x\wix.targets" from project "<my-project>" (target "Link" depends on it):
Task "ReadLinesFromFile"
Task Parameter:File=obj\x64\Debug\<my-project>.wixproj.BindContentsFileListen-us.txt
Output Item(s):
_BindInputs=
C:\Users\<me>\AppData\Local\Temp\a5uljxg1\MergeId.418703\api_ms_win_core_console_l1_1_0.dll.AF4EABEE_4589_3789_BA0A_C83A71662E1D
...
Done building target "ReadPreviousBindInputsAndBuiltOutputs" in project "<my-project>.wixproj".
Target "Link" in file "C:\Program Files (x86)\MSBuild\Microsoft\WiX\v3.x\wix.targets" from project "<my-project>.wixproj" (target "CompileAndLink" depends on it):
Building target "Link" completely.
Input file "C:\Users\<me>\AppData\Local\Temp\a5uljxg1\MergeId.418703\api_ms_win_core_console_l1_1_0.dll.AF4EABEE_4589_3789_BA0A_C83A71662E1D" does not exist.
...
<and here it executes Light.exe>
So, it looks like it reads BindContentsFileListen-us.txt and expects it to contain files that were inputs during last build run. But, unfortunately some of these files were generated in temporary folder and got wiped out (presumably during last build) and since they don't exist anymore -- Link step is re-executed. I observe this pattern every time I press F7, only number in MergeId.418703 gets changed every time (looks like process id to me).
UPDATE: this is a known (and pretty old) issue. As of now it is planned to be fixed in WiX v4.0.
I have hit the same issue, and the only information I found apart from this question was a pretty unhelpful mail thread from 2013 (1, 2) and an issue from the same era.
Troubleshooting
Reading through the logs and Wix's source code shows that the bugs occurs as follows:
light.exe, the linker, receives all of the object (.wixobj) files it should combine, some of them referencing the .msm merge module's file path.
light.exe uses a combination of mergemod.dll's IMsmMerge::ExtractCAB and cabinet.dll's ::FDICopy (through their own winterop.dll) to extract the contents of the merge module to a temporary path:
// Binder.cs:5612, ProcessMergeModules
// extract the module cabinet, then explode all of the files to a temp directory
string moduleCabPath = String.Concat(this.TempFilesLocation, Path.DirectorySeparatorChar, safeMergeId, ".module.cab");
merge.ExtractCAB(moduleCabPath);
string mergeIdPath = String.Concat(this.TempFilesLocation, Path.DirectorySeparatorChar, "MergeId.", safeMergeId);
Directory.CreateDirectory(mergeIdPath);
using (WixExtractCab extractCab = new WixExtractCab())
{
try
{
extractCab.Extract(moduleCabPath, mergeIdPath);
}
// [...]
}
At the same time, the contents of the merge module are inserted among the other input files in the fileRows collection:
// Binder.cs:5517, ProcessMergeModules
// NOTE: this is very tricky - the merge module file rows are not added to the
// file table because they should not be created via idt import. Instead, these
// rows are created by merging in the actual modules
FileRow fileRow = new FileRow(null, this.core.TableDefinitions["File"]);
// [...]
fileRow.Source = String.Concat(this.TempFilesLocation, Path.DirectorySeparatorChar, "MergeId.", wixMergeRow.Number.ToString(CultureInfo.InvariantCulture.NumberFormat), Path.DirectorySeparatorChar, record[1]);
FileRow collidingFileRow = fileRows[fileRow.File];
FileRow collidingModuleFileRow = (FileRow)uniqueModuleFileIdentifiers[fileRow.File];
if (null == collidingFileRow && null == collidingModuleFileRow)
{
fileRows.Add(fileRow);
// keep track of file identifiers in this merge module
uniqueModuleFileIdentifiers.Add(fileRow.File, fileRow);
}
// [...]
fileRows ends up being written to the <project_name>BindContentsFileList<culture>.txt file inside the intermediate directory, including the temporary (and randomly named) files extracted from the merge module:
// Binder.cs:7346
private void CreateContentsFile(string path, FileRowCollection fileRows)
{
string directory = Path.GetDirectoryName(path);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
using (StreamWriter contents = new StreamWriter(path, false))
{
foreach (FileRow fileRow in fileRows)
{
contents.WriteLine(fileRow.Source);
}
}
}
During the next build, the ReadPreviousBindInputsAndBuiltOutputs target from wix2010.targets reads the file into the #(_BindInputs) item group. This item group is then listed as input to the Link target. Since the temporary files have since disappeared, the target is always considered out-of-date and re-run, generating an new set of temporary files that get listed in BindContentsFileList, and so on.
Workaround
An actual fix would be to patch Wix so that merge modules discoverd in .wixobj files are listed in BindContentsFileList, and files extracted from them during linking aren't. Unfortunately I wasn't able to make Wix's source code compile, and can't be bothered to go through its distribution process. Hence, here is the workaround I have implemented.
Removing temporary files from the input list
This is done using a custom target which slots in-between ReadPreviousBindInputsAndBuiltOutputs and Link and filters #(_BindInputs) to remove whatever is under %temp%.
<Target
Name="RemoveTempFilesFromBindInputs"
DependsOnTargets="ReadPreviousBindInputsAndBuiltOutputs"
BeforeTargets="Link"
>
<PropertyGroup>
<!-- This includes a final backslash, so we can use StartsWith. -->
<TemporaryDirectory>$([System.IO.Path]::GetTempPath())</TemporaryDirectory>
</PropertyGroup>
<ItemGroup>
<_BindInputs
Remove="#(_BindInputs)"
Condition="$([System.String]::new('%(FullPath)').StartsWith('$(TemporaryDirectory)'))"
/>
</ItemGroup>
</Target>
At that point, Link triggers only when actual input files change. Success! However, changes to the .msm files are not detected. This might be good enough a solution anyway, since merge modules are generally static. Otherwise...
Detecting changes to merge modules
The main hurdle is that the only reference to the .msm file is within a .wxs source file, so we need to bridge the gap between that and MSBuild. There are a couple ways that can be used, such as parsing the .wixobj to fish out the WixMerge tables. However, I already had code in place to generate Wix code, so I went that way, lifting the merge modules into an MSBuild item group and using a custom task to generate a .wxs file referencing them in a feature. Full code below:
<Target
Name="GenerateMsmFragment"
BeforeTargets="GenerateCompileWithObjectPath"
Inputs="#(MsmFiles)"
Outputs="$(IntermediateOutputPath)MsmFiles.wxs"
>
<GenerateMsmFragment
MsmFiles="#(MsmFiles)"
FeatureName="MsmFiles"
MediaId="2"
OutputFile="$(IntermediateOutputPath)MsmFiles.wxs"
>
<Output TaskParameter="OutputFile" ItemName="Compile" />
</GenerateMsmFragment>
</Target>
// GenerateMsmFragment.cs
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
namespace tasks
{
[ComVisible(false)]
public class GenerateMsmFragment : Task
{
[Required]
public ITaskItem[] MsmFiles { get; set; }
[Required]
public string FeatureName { get; set; }
[Required]
public string MediaId { get; set; }
[Output]
public ITaskItem OutputFile { get; set; }
public override bool Execute()
{
var xmlns = "http://schemas.microsoft.com/wix/2006/wi";
var outputXml = new XmlDocument();
outputXml.AppendChild(outputXml.CreateXmlDeclaration("1.0", "utf-8", null));
var fragmentElem = outputXml
.AppendElement("Wix", xmlns)
.AppendElement("Fragment", xmlns);
{
var mediaElem = fragmentElem.AppendElement("Media", xmlns);
mediaElem.SetAttribute("Id", MediaId);
mediaElem.SetAttribute("Cabinet", "MsmFiles.cab");
mediaElem.SetAttribute("EmbedCab", "yes");
}
{
var directoryRefElem = fragmentElem.AppendElement("DirectoryRef", xmlns);
directoryRefElem.SetAttribute("Id", "TARGETDIR");
var featureElem = fragmentElem.AppendElement("Feature", xmlns);
featureElem.SetAttribute("Id", FeatureName);
featureElem.SetAttribute("Title", "Imported MSM files");
featureElem.SetAttribute("AllowAdvertise", "no");
featureElem.SetAttribute("Display", "hidden");
featureElem.SetAttribute("Level", "1");
foreach (var msmFilePath in MsmFiles.Select(i => i.ItemSpec)) {
var mergeElem = directoryRefElem.AppendElement("Merge", xmlns);
mergeElem.SetAttribute("Id", msmFilePath);
mergeElem.SetAttribute("SourceFile", msmFilePath);
mergeElem.SetAttribute("DiskId", MediaId);
mergeElem.SetAttribute("Language", "0");
featureElem
.AppendElement("MergeRef", xmlns)
.SetAttribute("Id", msmFilePath);
}
}
Directory.CreateDirectory(Path.GetDirectoryName(OutputFile.GetMetadata("FullPath")));
outputXml.Save(OutputFile.GetMetadata("FullPath"));
return true;
}
}
}
// XmlExt.cs
using System.Xml;
namespace nrm
{
public static class XmlExt
{
public static XmlElement AppendElement(this XmlDocument element, string qualifiedName, string namespaceURI)
{
var newElement = element.CreateElement(qualifiedName, namespaceURI);
element.AppendChild(newElement);
return newElement;
}
public static XmlElement AppendElement(this XmlNode element, string qualifiedName, string namespaceURI)
{
var newElement = element.OwnerDocument.CreateElement(qualifiedName, namespaceURI);
element.AppendChild(newElement);
return newElement;
}
}
}
And voilĂ , working up-to-date detection for merge modules.

Resolve actual Reference path using Microsoft.Build.Evaluation

I'm doing some introspection and analysis of csproj files using the Microsoft.Build.Evaluation tools in a small C# console app. I want to locate the actual location of Reference items, using the same heuristics as MSBuild itself ie the locations described here. I'm heading towards auto conversion of build artifacts into packages, similar to what's outlined on the JetBrains blog here
The only examples I can find expect the HintPath to be correct, for example this project, and I know there are some HintPaths that are not currently correct, I don't want to trust them. This project very close what I'm trying to do, with the added complication that I want to use real resolution behaviour to find dependencies.
I have an instance of a Microsoft.Build.Evaluation.Project object for my csproj, and I can't see any methods available on it that could exersize the resolution for me. I think what I'm hoping for is a magic Resolve() method for a Reference or a ProjectItem, a bit like this method.
I can probably find an alternative by constraining my own search to a set of limited output paths used by this build system, but I'd like to hook into MSBuild if I can.
The reference resolution is one of the trickiest parts of MSBuild. The logic of how assemblies are located is implemented inside the a standard set of tasks:
ResolveAssemblyReference, ResolveNativeReference, etc. The logic is how this works is very complicated, you can see that just by looking at number of possible parameters to those tasks.
However you don't need to know the exact logic to find the location of referenced files. There are standard targets called "ResolveAssemblyReferences", "ResolveProjectReferences" and some others more specialized for native references, COM references. Those targets are executed as part of the normal build. If you just execute those targets separately, you can find out the return values, which is exactly what you need. The same mechanism is used by IDE to get location of refereces, for Intellisense, introspection, etc.
Here is how you can do it in code:
using Microsoft.Build.BuildEngine;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using System;
using System.Collections.Generic;
class Program
{
static int Main(string[] args)
{
if (args.Length < 1)
{
Console.WriteLine("Usage: GetReferences.exe <projectFileName>");
return -1;
}
string projectFileName = args[0];
ConsoleLogger logger = new ConsoleLogger(LoggerVerbosity.Normal);
BuildManager manager = BuildManager.DefaultBuildManager;
ProjectInstance projectInstance = new ProjectInstance(projectFileName);
var result = manager.Build(
new BuildParameters()
{
DetailedSummary = true,
Loggers = new List<ILogger>() { logger }
},
new BuildRequestData(projectInstance, new string[]
{
"ResolveProjectReferences",
"ResolveAssemblyReferences"
}));
PrintResultItems(result, "ResolveProjectReferences");
PrintResultItems(result, "ResolveAssemblyReferences");
return 0;
}
private static void PrintResultItems(BuildResult result, string targetName)
{
var buildResult = result.ResultsByTarget[targetName];
var buildResultItems = buildResult.Items;
if (buildResultItems.Length == 0)
{
Console.WriteLine("No refereces detected in target {0}.", targetName);
return;
}
foreach (var item in buildResultItems)
{
Console.WriteLine("{0} reference: {1}", targetName, item.ItemSpec);
}
}
}
Notice, the engine is called to invoke specific targets in the project. Your project usually does not build, but some targets might be invoked by pre-requisite targets.
Just compile it and will print a sub-set of all dependencies. There might be more dependencies if you use COM references or native dependencies for your project. It should be easy to modify the sample to get those as well.

Visual Studio Extensibility: Intercepting file paste event

How do I intercept the file paste event in my Visual Studio extension?
File paste = pasting the whole file in Solution Explorer.
My goal is to apply certain replacements to the copied file.
It can be intercepted using DTE.Events.CommandEvents (MSDN) with typeof(VSConstants.VSStd97CmdID).GUID as command guid and VSConstants.VSStd97CmdID.Paste as command id.
Example:
protected override void Initialize() {
var dte = (DTE)GetService(typeof(DTE));
var pasteGuid = typeof(VSConstants.VSStd97CmdID).GUID.ToString("B");
var pasteID = (int)VSConstants.VSStd97CmdID.Paste;
_pasteEvent = dte.Events.CommandEvents[pasteGuid, pasteID];
_pasteEvent.BeforeExecute += delegate { Trace.WriteLine("Before paste."); };
_pasteEvent.AfterExecute += delegate { Trace.WriteLine("After paste."); };
}
This is not really perfect as it may intercept paste in other contexts as well, but it a good first step. It is also possible to watch ItemAdded event during the paste to get the pasted items.

Skip single file from Minifying?

I'm trying to use ASP.Nets BundleTable to optomize some javascript files, but have run into a problem where a specific addon (jQuery-Timepicker) fails to work when the code has been minified. See here.
Bundle code is currently similar to:
// Add our commonBundle
var commonBundle= new Bundle("~/CommonJS" + culture.ToString());
// JQuery and related entries.
commonBundle.Include("~/Scripts/jquery-1.7.2.js");
commonBundle.Include("~/Scripts/jquery-ui-1.8.22.js");
commonBundle.Include("~/Scripts/jquery.cookie.js");
commonBundle.Include("~/Scripts/jquery-ui/jquery-ui-timepicker-addon.js"); // This is the one that does not work when bundled
// JS Transformer
commonBundle.Transforms.Add(new JsMinify());
BundleTable.Bundles.Add(commonBundle);
If I remove the jquery-ui-timepicker-addon.js file, then include it separate in my webpage, then it works properly. (Otherwise I get the Uncaught TypeError: undefined is not a function error).
I'm wondering if I can somehow setup my bundling code to skip minifying this one file (but still have it included in the bundle)? I've been looking around but have not come up with any solutions for doing so.
So the issue is that all the files are bundled together, and then the entire bundle is minimized. As a result you aren't going to easily be able to skip minification of just one file. Probably the best way to do this would be to create a new Transform that appended the contents of this file you want unminified. Then you would append this Transform to your registered ScriptBundle:
commonBundle.Transforms.Add(new AppendFileTransform(""~/Scripts/jquery-ui/jquery-ui-timepicker-addon.js""));
AppendFileTransform would simply append the contents of the file to the bundled response. You would no longer include the timepicker in the bundle explicitly, but instead this transform would be including it, and this would effectively give you the behavior you are looking since the JsMinify transform would run first and minify the bundle, and then you would add the file you want at the end unminified.
This can be solved better from the other direction - instead of trying to not minify a single file, add transforms for individual items instead.
First - create a class that implements IItemTransform and uses the same code to minify the given input:
public class JsItemMinify : System.Web.Optimization.IItemTransform
{
public string Process(string includedVirtualPath, string input)
{
var min = new Microsoft.Ajax.Utilities.Minifier();
var result = min.MinifyJavaScript(input);
if (min.ErrorList.Count > 0)
return "/*minification failed*/" + input;
return result;
}
}
Second - add this item transform to the individual files and remove the bundle transform:
var commonBundle= new Bundle("~/CommonJS");
// the first two includes will be minified
commonBundle.Include("~/Scripts/jquery-1.7.2.js", new JsItemMinify());
commonBundle.Include("~/Scripts/jquery-ui-1.8.22.js", new JsItemMinify());
// this one will not
commonBundle.Include("~/Scripts/jquery.cookie.js");
// Remove the default JsMinify bundle transform
commonBundle.Transforms.Clear();
BundleTable.Bundles.Add(commonBundle);
You cannot setup Bundle to skip minifying certain files and to minify rest of the files.
You could implement your own Bundle or Transform by overriding Bundle.ApplyTransform or JsMinify.Process methods, but you would need to take care not to break change-tracking of files, key generation, cache invalidation, etc... (or doing some ugly hack). It's not worth the effort.
I would keep separate js file, as you already mentioned.
This is just complete example based on Hao Kung's answer
var myBundle = new ScriptBundle("~/bundles/myBundle").Include(
"~/Scripts/script1.js",
"~/Scripts/script2.js",
);
myBundle.Transforms.Add(new AppendFileTransform("~/Scripts/excludedFile.min.js"));
bundles.Add(myBundle);
And here is example implementation of the AppendFileTransform:
public class AppendFileTransform : IBundleTransform
{
private readonly string _filePath;
public AppendFileTransform(string filePath)
{
_filePath = filePath;
}
public void Process(BundleContext context, BundleResponse response)
{
response.Content += File.ReadAllText(context.HttpContext.Server.MapPath(_filePath));
}
}

Visual Studio 2010 plugin / code to clear "Error List" warnings before each build

VS2010 is driving me nuts: whenever I rebuild, the "Error List" warnings from the previous compilation are persisted and any new warnings are simply added to the end of the list. Over time, this list becomes ridiculously long and unwieldy.
I'm using the Chirpy 2.0 tools to run JSHint and JSLint on my JS files, and these tools generate a lot of false positives.
I've been looking for an easy way to clear the contents of this window, but the only manual mechanism that works 100% of the time is to close and re-open the solution. Not very elegant.
I'd like to write a small VS Plug-In or some code that gets called right before a compilation to clear out this list so I can focus only on new warnings for the currently loaded file(s).
I see a .Clear() method for the Output window but not for the Error List. Is this doable?
Once upon a time I was an Add-In/VSIX Package/MEF developer ...
The answer is shortly no, but I have to do it on the long way:
Add-Ins, packages (Managed or not) have access to the VS service level separatedly. Every error belongs to the reporter (If they are manage them as Chirpy do), so you can not handle the errors created by Chirpy 2.0
I take a several look to it's source code and it is persist it's erros gained by the tools in a Singleton collection called TaskList.
The deletion of the collection elements is happening in several point of code in the latest release through the RemoveAll method:
First: after the soulution is closed.
by this:
private static string[] buildCommands = new[] { "Build.BuildSelection", "Build.BuildSolution", "ClassViewContextMenus.ClassViewProject.Build" };
private void CommandEvents_BeforeExecute(string guid, int id, object customIn, object customOut, ref bool cancelDefault) {
EnvDTE.Command objCommand = default(EnvDTE.Command);
string commandName = null;
try {
objCommand = this.App.Commands.Item(guid, id);
} catch (System.ArgumentException) {
}
if (objCommand != null) {
commandName = objCommand.Name;
var settings = new Settings();
if (settings.T4RunAsBuild) {
if (buildCommands.Contains(commandName)) {
if (this.tasks != null) {
this.tasks.RemoveAll();
}
Engines.T4Engine.RunT4Template(this.App, settings.T4RunAsBuildTemplate);
}
}
}
}
As you may see, clear of results depends on many thigs.
First on a setting (which I don't know where to set on GUI or configs, but seems to get its value form a check box).
Second the array of names which are not contains every build commands name.
So I see a solution, but only on the way to modify and rebuild/redepeloy your own version from Chirpy (and make a Pull request):
The code souldn't depend on the commands, and their names. (rebuilds are missing for example)
You could change the method above something like this:
this.eventsOnBuild.OnBuildBegin += ( scope, action ) =>
{
if (action != vsBuildAction.vsBuildActionDeploy)
{
if (this.tasks != null)
{
this.tasks.RemoveAll();
}
if (settings.T4RunAsBuild && action != vsBuildAction.vsBuildActionClean)
{
Engines.T4Engine.RunT4Template(this.App, settings.T4RunAsBuildTemplate);
}
}
};
Or with something equivalent handler method instead of lambda expression.
You shold place it into the subscription OnStartupComplete method of Chirp class.
The unsubscription have to placed into OnDisconnection method in the same class. (As for all other subscribed handlers...)
Update:
When an Add-In disconneced, it isn't means the Studio will be closed immediately. The Add-In could be unloaded. So you should call the RemoveAll from OnDisconneconnection too. (Or Remove and Dispose the TaskList...)
Update2:
You can also make a custom command, and bind it to a hotkey.

Resources