Created
June 26, 2020 02:35
-
-
Save kevinoid/19f97146bb2a032d56cb04266f8d69fa to your computer and use it in GitHub Desktop.
Script to (re-)generate .designer files for .aspx, .ascx, .master, .resx, .settings, and other files which generate code at design time.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
##!/usr/bin/env pwsh | |
<# | |
.SYNOPSIS | |
Generate .designer files for a project or solution. | |
.NOTES | |
Copyright 2020 Kevin Locke <[email protected]> | |
Available under the MIT License: https://opensource.org/licenses/MIT | |
#> | |
[CmdletBinding( | |
DefaultParameterSetName='Path', | |
SupportsShouldProcess)] | |
Param( | |
[Parameter(HelpMessage='Literal path of solution or project for which to generate designer files', | |
Mandatory=$true, | |
ParameterSetName='LiteralPath')] | |
[ValidateNotNullOrEmpty()] | |
[string]$LiteralPath, | |
[Parameter(HelpMessage='Path of solution or project for which to generate designer files', | |
Mandatory=$true, | |
ParameterSetName='Path', | |
Position=0)] | |
[ValidateNotNullOrEmpty()] | |
[string]$Path, | |
[Parameter(HelpMessage='Overwrite existing .designer files')] | |
[Switch] | |
$Force | |
) | |
Set-StrictMode -Version Latest | |
if ([Threading.Thread]::CurrentThread.GetApartmentState() -ne [Threading.ApartmentState]::STA) { | |
throw [InvalidOperationException]'Must be run from single-threaded apartment. Use -STA PowerShell param.' | |
} | |
# Resolve and ensure path exists | |
if ($LiteralPath) { | |
$item = Get-Item -LiteralPath $LiteralPath -ErrorAction Stop | |
} else { | |
$item = Get-Item -Path $Path -ErrorAction Stop | |
} | |
if ($item -isNot [IO.FileInfo]) { | |
throw [ArgumentException]'Path must be a file.' | |
} | |
$itemPath = $item.FullName | |
if ($itemPath -notLike '*.sln' -and $itemPath -notLike '*.*proj') { | |
throw [ArgumentOutOfRangeException]'Path must be .sln or .*proj file.' | |
} | |
$envDteAssembly = [Reflection.Assembly]::LoadWithPartialName('EnvDTE') | |
$vsShellAssembly = [Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualStudio.Shell') | |
$vsShellInterop8Assembly = [Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualStudio.Shell.Interop.8.0') | |
$vsShellInteropAssembly = [Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualStudio.Shell.Interop') | |
$vsOleInteropAssembly = [Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualStudio.OLE.Interop') | |
[void][Reflection.Assembly]::LoadWithPartialName('VSLangProj') | |
# On my system, Add-Type with 'Microsoft.VisualStudio.Shell' causes | |
# FileNotFoundException by trying to load Version=2.0.0.0. I can't figure out | |
# why. Since LoadWithPartialName works fine, use the version it resolved. | |
# Do the same for all assemblies to ensure all code uses the same versions. | |
[string[]]$refAssemblies = @( | |
$envDteAssembly.FullName, | |
$vsShellInterop8Assembly.FullName, | |
$vsShellInteropAssembly.FullName, | |
$vsOleInteropAssembly.FullName, | |
$vsShellAssembly.FullName | |
) | |
# Note: May be defined from a previous run (e.g. running from PowerShell ISE) | |
if (-not ('DTEUtils' -as [Type])) { | |
Add-Type -ReferencedAssemblies $refAssemblies @' | |
using System; | |
using System.Runtime.InteropServices; | |
using EnvDTE; | |
using Microsoft.VisualStudio.Shell.Interop; | |
using OLEInterop = Microsoft.VisualStudio.OLE.Interop; | |
using Shell = Microsoft.VisualStudio.Shell; | |
public static class DTEUtils | |
{ | |
/// <summary> | |
/// Get a <see cref="Shell.ServiceProvider"/> for a given <see cref="DTE"/>. | |
/// </summary> | |
/// <remarks> | |
/// <c>New-Object ServiceProvider $dte</c> throws: | |
/// Cannot find an overload for "ServiceProvider" and the argument count: "1". | |
/// Presumably because it can't infer the type and ComObject can't be cast. | |
/// This method can be used as a workaround. | |
/// </remarks> | |
public static Shell.ServiceProvider GetServiceProvider(DTE dte) | |
{ | |
// Create Shell.ServiceProvider using OLE.Interop.IServiceProvider | |
// interface of DTE object, as documented in: | |
// https://docs.microsoft.com/en-us/visualstudio/extensibility/how-to-get-a-service#getting-a-service-from-the-dte-object | |
// https://www.mztools.com/articles/2007/MZ2007015.aspx | |
// https://weblogs.asp.net/cazzu/GetServiceFromDTE | |
return new Shell.ServiceProvider((OLEInterop.IServiceProvider)dte); | |
} | |
/// <remarks>Convenience method for PowerShell.</remarks> | |
public static Shell.ServiceProvider GetServiceProvider(object dte) | |
{ | |
return GetServiceProvider((DTE)dte); | |
} | |
} | |
/// <summary> | |
/// Wrapper for calling <see cref="IVsExtensibility3"/> methods from PowerShell. | |
/// </summary> | |
public class VsExtensibilityWrapper | |
{ | |
private readonly IVsExtensibility3 vsExtensibility; | |
public VsExtensibilityWrapper(DTE dte) | |
: this(DTEUtils.GetServiceProvider(dte)) | |
{ | |
} | |
public VsExtensibilityWrapper(System.IServiceProvider serviceProvider) | |
{ | |
// Get IVSExtensibility3 from IVsExtensibility as in | |
// https://github.com/dotnet/project-system/issues/1020 | |
this.vsExtensibility = | |
(IVsExtensibility3)serviceProvider.GetService(typeof(IVsExtensibility)); | |
if (this.vsExtensibility == null) | |
{ | |
throw new InvalidOperationException("ServiceProvider has no IVsExtensibility"); | |
} | |
} | |
/// <summary>Constructs a VsExtensibilityWrapper from a DTE.</summary> | |
/// <remarks>Convenience method for PowerShell.</remarks> | |
public static VsExtensibilityWrapper FromDTE(object dte) | |
{ | |
return new VsExtensibilityWrapper((DTE)dte); | |
} | |
/// <summary> | |
/// Set DTE state to indicate that an automation function is executing. | |
/// </summary> | |
/// <remarks> | |
/// <para> | |
/// <see cref="VsShellUtilities.IsInAutomationFunction"/> (or an equivalent) | |
/// is used to determine whether to prompt the user. Calling this function | |
/// indicates that an automation function is running and the uesr should not | |
/// be prompted. | |
/// </para> | |
/// <para> | |
/// <c>$vsExt.EnterAutomationFunction()</c> in PS throws MethodNotFound: | |
/// Method invocation failed because [System.__ComObject] does not contain a method named 'EnterAutomationFunction'. | |
/// even though <c>$vsExt -is [IVsExtensibility3]</c> is <c>$true</c>. | |
/// This method can be used as a workaround. | |
/// </para> | |
/// </remarks> | |
public void EnterAutomationFunction() | |
{ | |
int hresult = this.vsExtensibility.EnterAutomationFunction(); | |
Marshal.ThrowExceptionForHR(hresult); | |
} | |
/// <summary> | |
/// Set DTE state to indicate that an automation function is not executing. | |
/// </summary> | |
/// <seealso cref="EnterAutomationFunction"/> | |
public void ExitAutomationFunction() | |
{ | |
int hresult = this.vsExtensibility.ExitAutomationFunction(); | |
Marshal.ThrowExceptionForHR(hresult); | |
} | |
} | |
'@ | |
} | |
# Note: May be defined from a previous run (e.g. running from PowerShell ISE) | |
if (-not ('MessageFilter' -as [Type])) { | |
# From https://docs.microsoft.com/en-us/previous-versions/ms228772(v=vs.140) | |
# IOleMessageFilter => Microsoft.VisualStudio.OLE.Interop.IMessageFilter | |
Add-Type -ReferencedAssemblies $vsOleInteropAssembly.FullName @' | |
using System.Runtime.InteropServices; | |
using Microsoft.VisualStudio.OLE.Interop; | |
public class MessageFilter : IMessageFilter | |
{ | |
// | |
// Class containing the IMessageFilter | |
// thread error-handling functions. | |
// Start the filter. | |
public static void Register() | |
{ | |
IMessageFilter newFilter = new MessageFilter(); | |
IMessageFilter oldFilter = null; | |
CoRegisterMessageFilter(newFilter, out oldFilter); | |
} | |
// Done with the filter, close it. | |
public static void Revoke() | |
{ | |
IMessageFilter oldFilter = null; | |
CoRegisterMessageFilter(null, out oldFilter); | |
} | |
// | |
// IMessageFilter functions. | |
// Handle incoming thread requests. | |
uint IMessageFilter.HandleInComingCall(uint dwCallType, | |
System.IntPtr hTaskCaller, uint dwTickCount, INTERFACEINFO[] | |
lpInterfaceInfo) | |
{ | |
//Return the flag SERVERCALL_ISHANDLED. | |
return 0; | |
} | |
// Thread call was rejected, so try again. | |
uint IMessageFilter.RetryRejectedCall(System.IntPtr | |
hTaskCallee, uint dwTickCount, uint dwRejectType) | |
{ | |
if (dwRejectType == 2) | |
// flag = SERVERCALL_RETRYLATER. | |
{ | |
// Retry the thread call immediately if return >=0 & | |
// <100. | |
return 99; | |
} | |
// Too busy; cancel call. | |
return 0xFFFFFFFF; | |
} | |
uint IMessageFilter.MessagePending(System.IntPtr hTaskCallee, | |
uint dwTickCount, uint dwPendingType) | |
{ | |
//Return the flag PENDINGMSG_WAITDEFPROCESS. | |
return 2; | |
} | |
// Implement the IMessageFilter interface. | |
[DllImport("Ole32.dll")] | |
private static extern int | |
CoRegisterMessageFilter(IMessageFilter newFilter, out | |
IMessageFilter oldFilter); | |
} | |
'@ | |
} | |
# Note: COM interface types are not used for parameters due to inability to | |
# cast __ComObject in PowerShell. | |
# See https://stackoverflow.com/q/9036551 | |
<# | |
.SYNOPSIS | |
Determines whether the path of a .designer file for a given project item exists. | |
#> | |
Function Test-DesignerItem() { | |
Param( | |
[Parameter(HelpMessage='Project item', Mandatory=$true, Position=0)] | |
# [EnvDTE.ProjectItem] | |
$ProjectItem | |
) | |
$fileCount = $ProjectItem.FileCount | |
for ($i = 0; $i -lt $fileCount; $i++) { | |
$fileName = $ProjectItem.FileNames($i) | |
$dotIndex = $fileName.LastIndexOf('.') | |
if ($dotIndex -gt 0) { | |
$designerName = $fileName.Insert($dotIndex, '.designer') | |
if (Test-Path -LiteralPath $designerName) { | |
return $true | |
} | |
} | |
} | |
return $false | |
} | |
<# | |
.SYNOPSIS | |
Determines whether the path of a given project item exists. | |
#> | |
Function Test-ProjectItem() { | |
Param( | |
[Parameter(HelpMessage='Project item', Mandatory=$true, Position=0)] | |
# [EnvDTE.ProjectItem] | |
$ProjectItem | |
) | |
$fileCount = $ProjectItem.FileCount | |
for ($i = 0; $i -lt $fileCount; $i++) { | |
$fileName = $ProjectItem.FileNames($i) | |
if (Test-Path -LiteralPath $fileName) { | |
return $true | |
} | |
} | |
return $false | |
} | |
<# | |
.SYNOPSIS | |
Removes .designer project items from in a given project item. | |
#> | |
Function Remove-Designer() { | |
[CmdletBinding(SupportsShouldProcess)] | |
Param( | |
[Parameter(HelpMessage='Project items', Mandatory=$true, Position=0)] | |
# [EnvDTE.ProjectItems] | |
$ProjectItems, | |
[Parameter(HelpMessage='Remove project items which exist in addition to those that don''t')] | |
[Switch] | |
$Force, | |
[Parameter(HelpMessage='Recurse into sub-projects')] | |
[Switch] | |
$Recurse | |
) | |
foreach ($projectItem in $ProjectItems) { | |
if ($Recurse) { | |
if ($projectItem.SubProject) { | |
Remove-Designer $projectItem.SubProject.ProjectItems ` | |
-Force:$Force -Recurse:$Recurse | |
} | |
} | |
# Note: Although this is recursion, it is not covered by the -Recurse | |
# param because dependent files usually appear as ProjectItems below | |
# the item from which they are generated. If this were disabled, the | |
# command would be mostly useless. | |
$projectItems = $projectItem.ProjectItems | |
if ($null -ne $projectItems -and $projectItems.Count -gt 0) { | |
Remove-Designer $projectItems -Force:$Force -Recurse:$Recurse | |
} | |
# TODO: Limit to files which are DependentUpon non-designer file? | |
# TODO: Skip AutoGen files (without -Force)? | |
# All examples I'm aware of are the result of CustomTool and | |
# would be overwritten when CustomTool is run. | |
if ($projectItem.Name -notLike '*.designer.*') { | |
Write-Debug "Not removing $($projectItem.Name): Not named *.designer.*" | |
} elseif ($projectItem.Kind -ne [EnvDTE.Constants]::vsProjectItemKindPhysicalFile) { | |
Write-Debug "Not removing $($projectItem.Name): Not a physical file." | |
} elseif (-not $Force -and (Test-ProjectItem $projectItem)) { | |
Write-Debug "Not removing $($projectItem.Name): File exists and -Force not given." | |
} elseif ($PSCmdlet.ShouldProcess($projectItem.Name)) { | |
Write-Verbose "Removing $($projectItem.Name)" | |
$projectItem.Delete() | |
} | |
} | |
} | |
<# | |
.SYNOPSIS | |
Converts the open solutions to a Web Application Project. | |
#> | |
Function ConvertTo-WebApplication() { | |
[CmdletBinding(SupportsShouldProcess)] | |
Param( | |
[Parameter(HelpMessage='Visual Studio automation object', Mandatory=$true, Position=0)] | |
# [EnvDTE80.DTE2] | |
$DTE, | |
[Parameter(HelpMessage='Visual Studio extensibility wrapper')] | |
[VsExtensibilityWrapper]$VsExtensibility | |
) | |
# Note: Project.ConverttoWebApplication operates on the selected item(s). | |
# Could select a single project or file with code like | |
# https://stackoverflow.com/a/18924454: | |
# $uiItem = $dte.ToolWindows.SolutionExplorer.GetItem($Solution.Name + '\' + $Project.Name) | |
# $uiItem.Select([EnvDTE.vsUISelectionType]::vsUISelectionTypeSelect) | |
# Note2: $dte.ToolWindows may need C# to avoid PropertyNotFoundStrict | |
$projectNames = ($DTE.Solution.Projects | Select-Object -Expand Name) -join ', ' | |
if (-not $PSCmdlet.ShouldProcess($projectNames -join ', ')) { | |
return | |
} | |
Write-Verbose "Converting $projectNames to Web Application" | |
# Project.ConverttoWebApplication shows confirm dialog to the user unless: | |
# - VS is running in command-line mode (VSSPROPID_IsInCommandLineMode) | |
# - VS is in automation function (VsShellUtilities.IsInAutomationFunction) | |
# Therefore, tell VS that we are in an automation function | |
if ($VsExtensibility) { $VsExtensibility.EnterAutomationFunction() } | |
try { | |
# Available commands: https://stackoverflow.com/q/13855401 | |
$DTE.ExecuteCommand('Project.ConverttoWebApplication') | |
} finally { | |
if ($VsExtensibility) { $VsExtensibility.ExitAutomationFunction() } | |
} | |
} | |
<# | |
.SYNOPSIS | |
Runs the Custom Tool for each project item where one is configured. | |
#> | |
Function Invoke-CustomTool() { | |
[CmdletBinding(SupportsShouldProcess)] | |
Param( | |
[Parameter(HelpMessage='Project items', Mandatory=$true, Position=0)] | |
# [EnvDTE.ProjectItems] | |
$ProjectItems, | |
[Parameter(HelpMessage='Run even if .designer exists')] | |
[Switch] | |
$Force, | |
[Parameter(HelpMessage='Recurse into sub-projects/items')] | |
[Switch] | |
$Recurse | |
) | |
foreach ($projectItem in $ProjectItems) { | |
if ($Recurse) { | |
if ($projectItem.SubProject) { | |
Invoke-CustomTool $projectItem.SubProject.ProjectItems ` | |
-Force:$Force -Recurse:$Recurse | |
} | |
$projectItems = $projectItem.ProjectItems | |
if ($null -ne $projectItems -and $projectItems.Count -gt 0) { | |
Invoke-CustomTool $projectItems -Force:$Force -Recurse:$Recurse | |
} | |
} | |
$properties = $projectItem.Properties | |
if ($null -ne $properties) { | |
try { | |
$customTool = $properties.Item('CustomTool').Value | |
} catch [ArgumentException] { | |
# ProjectItem doesn't have CustomTool property (e.g. folders) | |
$customTool = $null | |
} | |
} else { | |
$customTool = $null | |
} | |
$vsProjectItem = $projectItem.Object | |
if ($customTool ` | |
-and $vsProjectItem -is [VSLangProj.VSProjectItem] ` | |
-and ($Force -or -not (Test-DesignerItem $projectItem))) { | |
if ($PSCmdlet.ShouldProcess($projectItem.Name)) { | |
Write-Verbose "Running $customTool on $($projectItem.Name)" | |
$vsProjectItem.RunCustomTool() | |
} | |
} elseif ($customTool) { | |
Write-Debug "Not running $customTool on $($projectItem.Name)" | |
} else { | |
Write-Debug "No CustomTool defined for $($projectItem.Name)" | |
} | |
} | |
} | |
# TODO: Check if file is already open in VS? | |
# If assume at most one VS instance, can check Solution/Project of | |
# $dte = Marshal.GetActiveObject('VisualStudio.DTE') | |
# To handle multiple VS instances, search Running Object Table | |
# https://stackoverflow.com/q/13432057 | |
# https://stackoverflow.com/a/10998689 | |
# https://docs.microsoft.com/en-us/visualstudio/extensibility/launch-visual-studio-dte | |
# Can search for solution path moniker or check Solution/Project of each DTE | |
# https://docs.microsoft.com/en-us/previous-versions/ms228755(v=vs.140) | |
# https://stackoverflow.com/q/44342459 (example of ROT moniker enumeration) | |
$dteType = [Type]::GetTypeFromProgID('VisualStudio.DTE', $true) | |
$dte = [Activator]::CreateInstance($dteType) | |
try { | |
# Fix "Call was Rejected By Callee" errors by registering a message filter | |
# to retry calls which fail with SERVERCALL_RETRYLATER. See: | |
# https://docs.microsoft.com/en-us/previous-versions/ms228772(v=vs.140) | |
[MessageFilter]::Register() | |
# Check extensibility early, since it may fail due to 64/32-bit differences | |
try { | |
$vsExtensibility = [VsExtensibilityWrapper]::FromDTE($DTE) | |
} catch [InvalidCastException] { | |
# Some COM classes may not be 64-bit registered (e.g. IVsExtensibility3) | |
# If REGDB_E_CLASSNOTREG occurs on 64-bit PowerShell, inform the user. | |
# Note: Decided against immediate error for 64-bit PS in case future | |
# versions of Visual Studio register necessary classes for 64-bit use. | |
# Note: Decided against automatic re-launching in 32-bit PowerShell, | |
# which hides the problem and is unreliable on PowerShell Core | |
# (Start-Job will fail, and 32-bit version not likely to be installed). | |
# Note: Could add param to continue w/o $vsExtensibility, if use case. | |
# | |
# Expect "(Exception from HRESULT: 0x80040154 (REGDB_E_CLASSNOTREG)" | |
# in .Message, but .HResult is E_NOINTERFACE. Ugh. | |
$ex = $_.Exception | |
if ([IntPtr]::Size -eq 8 ` | |
-and $ex.HResult -in 0x80004002,0x80040154 ` | |
-and $ex.Message.Contains('0x80040154')) { | |
# Note: Include InnerException message since PS doesn't print it | |
$userEx = (New-Object InvalidOperationException @( | |
'Required COM class not available, likely due to 64-bit PowerShell. Consider using PowerShell (x86). ' + $ex, | |
$ex)) | |
# Note: Construct ErrorRecord so PS doesn't use long Message as | |
# ErrorID, causing it to be printed twice. | |
$PSCmdlet.ThrowTerminatingError((New-Object ` | |
System.Management.Automation.ErrorRecord @( | |
$userEx, | |
'InvalidOperationLikely6432', | |
[Management.Automation.ErrorCategory]::InvalidOperation, | |
$_.TargetObject))) | |
} else { | |
$PSCmdlet.ThrowTerminatingError($_) | |
} | |
} | |
if ($itemPath -like '*.sln') { | |
$dte.Solution.Open($itemPath) | |
} else { | |
$dte.Solution.AddFromFile($itemPath, $false) | |
} | |
foreach ($project in $dte.Solution.Projects) { | |
Remove-Designer $project.ProjectItems -Force:$Force -Recurse | |
Invoke-CustomTool $project.ProjectItems -Force:$Force -Recurse | |
} | |
ConvertTo-WebApplication $dte -VsExtensibility $vsExtensibility | |
foreach ($project in $dte.Solution.Projects) { | |
Write-Verbose "Saving $($project.Name)" | |
try { | |
$project.Save() | |
} catch [NotImplementedException] { | |
# Can't save projects without project file (e.g. "Solution Items") | |
Write-Debug "Unable to save $($project.Name): $_" | |
} | |
} | |
} finally { | |
$dte.Quit() | |
[MessageFilter]::Revoke() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment