ligershark / side-waffle

A collection of Item- and Project Templates for Visual Studio
Other
657 stars 205 forks source link

Fixing .xproj Artifacts Output File Path #326

Closed RehanSaeed closed 8 years ago

RehanSaeed commented 8 years ago

Some time ago I contributed the FixNuGetPackageHintPathsWizard to fix the hint path for references to DLL's in the NuGet packages folder.

I think we need to do something similar for the new .xproj project types to fix the BaseIntermediateOutputPath and OutputPath which is a relative path to the solution. The artifacts folder should be in the same folder as the solution but my template does not always generate it that way.

<PropertyGroup Label="Globals">
    <ProjectGuid>6e0ef33d-3c19-4ea2-8ca9-c7bf19bdd947</ProjectGuid>
    <RootNamespace>MvcBoilerplate</RootNamespace>
    <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
    <OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>

Unless there is something I am missing, I will take a look at creating a new wizard to fix this.

sayedihashimi commented 8 years ago

@billhiebert when creating a dnx project is there a better alternative to creating a custom wizard to get the artifacts path? I'm guessing a wizard is the way to go but wanted to double check.

sayedihashimi commented 8 years ago

@RehanSaeed I discussed with Bill and we will need to create our own custom wizard. The wizard used by the One ASP.NET is closed source and not suitable for us to directly use it. Hopefully we (my team at Microsoft) can work on a version of that wizard that can be consumed by us.

In the mean while we can create our own wizard. Below you'll find the code for the wizard used by the One ASP.NET dialog. We can't use the code as is because the One ASP.NET dialog uses two .vstemplate files where as we just use one, but we can probably implement a wizard that can handle what we need from the code. The code for that wizard is pasted below.

This wizard handles a bunch of stuff like creating the src directory, nuget.config, artifacts path, etc. I think we can use this to figure out the basic stuff like artifacts path, but for things like the src directory or global.json maybe it would be better to wait for my team to expose something that can handle it.

//--------------------------------------------------------------------------------------------
// FolderLayoutTemplateWizard
//
// Wizard which creates new projects with the following layout
//   sln
//      |- Src
//          |-Project
//
// Copyright(c) 2014 Microsoft Corporation
//--------------------------------------------------------------------------------------------
namespace Microsoft.VisualStudio.ProjectSystem.DotNet.Wizard
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Diagnostics.CodeAnalysis;
    using System.IO;
    using System.Text; // Consume files from project system as linked files
    using Microsoft.VisualStudio.Shell;
    using Microsoft.VisualStudio.Shell.Interop;
    using Microsoft.VisualStudio.TemplateWizard;
    using Microsoft.VisualStudio.ProjectSystem.DotNet;
    using Microsoft.VisualStudio.ProjectSystem.DotNet.Utilities;
    using Microsoft.VisualStudio.ProjectSystem.DotNet.VisualStudio;
    using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider;

    [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable",
        Justification = "This is called by wizard framework which does not call IDisposable at all.")]
    public class FolderLayoutTemplateWizard : IWizard
    {
        private IOleServiceProvider _oleServiceProvider;
        private ServiceProvider _serviceProvider;
        private Dictionary<string, string> _replacementsDictionary;
        private string _thisVstemplateFile;
        private string _customParams;
        private const string ProjectWizardName = "$projectwizardname$";
        private const string AspNetProjectTemplatesFolder = "AspNetProjectTemplates";

        private const string DefaultNugetFeedSourceKey = "api.nuget.org";
        private const string DefaultNugetFeedSourceValue = "https://api.nuget.org/v3/index.json";

        public EnvDTE.DTE DTE
        {
            get { return _serviceProvider.GetService<EnvDTE.DTE>(); }
        }

        public EnvDTE90.Solution3 Solution
        {
            get { return (EnvDTE90.Solution3)DTE.Solution; }
        }

        /// <summary>
        /// Is an exlusive solution if any of the two properties are set
        /// </summary>
        public bool IsExclusiveSolution
        {
            get
            {
                return GetBool("$oneaspnetexclusiveproject$") || GetBool("$exclusiveproject$");
            }
        }

        /// <summary>
        /// IWizard
        /// First function the wizard framework calls. Just remember some values like the replacementsDictionary
        /// </summary>
        public void RunStarted(object automationObject, Dictionary<string, string> replacementsDictionary, WizardRunKind runKind, object[] customParams)
        {
            _oleServiceProvider = automationObject as IOleServiceProvider;
            Debug.Assert(_oleServiceProvider != null);
            _serviceProvider = new ServiceProvider(_oleServiceProvider);
            _replacementsDictionary = replacementsDictionary;

            // Custom parameters should have two entries. The first is the full path to this vstemplate file, the second is our
            // custom parameter key=value, which identfies the project's vstemplate file to create
            Debug.Assert(customParams.Length >= 1);
            if(customParams.Length >= 1 && customParams[0] is string)
            {
                _thisVstemplateFile = (string)customParams[0];

                // One ASPNET dialog passes aditional replacement parameters, they get passed in 'customParams' paramerter.
                // Save customParams so that we can pass them on further
                StringBuilder customParamsBuilder = new StringBuilder();
                for (int i = 1; i < customParams.Length; i++)
                {
                    if (customParams[i] is string)
                    {
                        customParamsBuilder.Append("|");
                        customParamsBuilder.Append((string)customParams[i]);
                    }
                }

                _customParams = customParamsBuilder.ToString();

                return;
            }
            throw new Exception(Resources.InvalidWizard);
        }

        /// <summary>
        /// IWizard
        /// This is where we do all the work of creating the intermediate folders and creating
        /// the actual project
        /// </summary>
        public void RunFinished()
        {
            try
            {
                // Get the path the user said to create the project
                string originalProjectDirectory = GetString("$destinationdirectory$");
                string projectName = GetString("$projectname$");
                string solutionDirectory = GetString("$solutiondirectory$");
                string targetFrameworkVersion = GetString("$targetframeworkversion$");

                // We need to get the path to the project to create
                string projectTemplateWithCustomParams = GetProjectTemplatePath();
                // Append saved custom parameters
                projectTemplateWithCustomParams = projectTemplateWithCustomParams + _customParams;
                if (String.IsNullOrEmpty(GetString("$dnxtargetframeworkversion$")))
                {
                    projectTemplateWithCustomParams = AppendDnxTargetFrameworkVersionToken(projectTemplateWithCustomParams, targetFrameworkVersion);
                }

                /// We only create the layout if we are creating a new solution AND the solution has the 
                /// create directory for solution checked. There isn't a property for the checkbox but We 
                /// can infer it by comparing the solutions directory path with the projects path. If checked
                /// the solution path will be c:\...\projects\ProjectName and the projct path will be c:\...\projects\projectName\Projectname.
                /// If unchecked the two paths will be identical (case where the project is being created in the default location), or
                /// completely unrelated.

                bool createSolutionDirWasChecked = GetBool("$oneaspnetcreatesolutiondirwaschecked$") || CreateSolutionDirWasChecked(solutionDirectory, originalProjectDirectory, projectName);
                if(IsExclusiveSolution)
                {
                    if(createSolutionDirWasChecked)
                    {
                        // Since exclusive, this will create a src (or test) folder
                        EnvDTE.Project solutionFolder = GetPreferredSolutionFolder(createIfNotExist: true);
                        Debug.Assert(solutionFolder != null);

                        // Add the globl.json file, this should be done
                        // before the project is created, otherwise, the project load
                        // starts before the global.json file is created and ProjectMetadataProvider
                        // could try to read the sdk version from global.json before it was
                        // created and thus could miss out the actual entry from there.
                        var globalJsonWriter = new GlobalJsonWriter(_serviceProvider);
                        globalJsonWriter.AddGlobalJsonFile(solutionDirectory);

                        // Also need to ensure the Nuget.config file is in place before the project is created
                        // nugetfeedsource parameter specifies the nuget feed to use for this template.
                        string nugetFeedSource = GetString("$nugetfeedsource$");

                        // multiple feed sources are delimited via |
                        string[] nugetFeedSources = string.IsNullOrEmpty(nugetFeedSource) ? null : nugetFeedSource.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
                        if (nugetFeedSources != null)
                        {
                            // if feedsources where specified, create the file, otherwise don't create a nuget.config
                            Dictionary<string, string> feedSources = new Dictionary<string, string>();

                            foreach (var feedSource in nugetFeedSources)
                            {
                                string[] nugetFeedSourceParts = string.IsNullOrEmpty(feedSource) ? null : feedSource.Split(new char[] { '=' }, 2, StringSplitOptions.RemoveEmptyEntries);
                                if (nugetFeedSourceParts != null && nugetFeedSourceParts.Length == 2)
                                {
                                    feedSources.Add(nugetFeedSourceParts[0], nugetFeedSourceParts[1]);
                                }
                            }

                            AddNugetConfigFile(solutionDirectory, feedSources);
                        }

                        string projectDirectory = Path.Combine(new string[] { solutionDirectory, solutionFolder.Name, projectName });

                        if (!Directory.Exists(projectDirectory))
                        {
                            Directory.CreateDirectory(projectDirectory);
                        }

                        // Now the solution will have precreated the original destination directory. Try to delete it. It
                        // Will fail if not empty - which is what we want. We don't want to delete it if the directory is the 
                        // solution folder - user has unchecked the create directory for solution option
                        try
                        {
                            if (!originalProjectDirectory.EnsureTrailingBackslash().Equals(solutionDirectory.EnsureTrailingBackslash(), StringComparison.OrdinalIgnoreCase) &&
                                !originalProjectDirectory.EnsureTrailingBackslash().Equals(projectDirectory.EnsureTrailingBackslash(), StringComparison.OrdinalIgnoreCase))
                            {
                                Directory.Delete(originalProjectDirectory);
                            }
                        }
                        catch
                        {
                            // As above, if this fails we march on.
                        }

                        // Need to pass the output directory location
                        string relOutdir = PathUtilities.MakeRelativePath(projectDirectory, Path.Combine(solutionDirectory, "artifacts")).RemoveTrailingBackslash();
                        projectTemplateWithCustomParams = AppendBaseOutputLocationToken(projectTemplateWithCustomParams, relOutdir);

                        // Now create the real project in the solution folder
                        ((EnvDTE80.SolutionFolder)solutionFolder.Object).AddFromTemplate(projectTemplateWithCustomParams, projectDirectory, projectName);
                    }
                    else
                    {
                        // Just set the output directory and create the project. In this case we know the solution will be created one level above our folder. So
                        // the output directory is just ..\artifacts
                        projectTemplateWithCustomParams = AppendBaseOutputLocationToken(projectTemplateWithCustomParams, "..\\artifacts");

                        // Now create the real project using the solution
                        Solution.AddFromTemplate(projectTemplateWithCustomParams, originalProjectDirectory, projectName);
                    }
                }
                else
                {
                    // We are adding a new project to an existing solution. Try to find a good folder for it
                    EnvDTE.Project preferredSolutionFolder= GetPreferredSolutionFolder(createIfNotExist: false);

                    if (preferredSolutionFolder != null)
                    {
                        string preferredProjectDirectory = originalProjectDirectory;
                        // We want to create the project in the solution folders backing disk path  - but only if it exists. Otherwise, its just
                        // some random virtual solution folder. We are only going to do this if the user took the default path for creating the project.
                        // If they did anything else, we just create it where they asked
                        if(originalProjectDirectory.TrimEnd('\\').Equals(Path.Combine(solutionDirectory, projectName)))
                        {
                            string solutionFolderPath = GetSolutionFolderPath(solutionDirectory, preferredSolutionFolder, failIfNotExists: true);
                            if(solutionFolderPath != null)
                            {
                                preferredProjectDirectory = Path.Combine(solutionFolderPath, projectName);
                            }
                        }
                        if (!Directory.Exists(preferredProjectDirectory))
                        {
                            Directory.CreateDirectory(preferredProjectDirectory);
                        }
                        else
                        {
                            // We have to be careful. There could be an existing project in the folder. See bug 1092943. If that is the 
                            // case, we just use the original folder.
                            if(!DirectoryIsEmpty(preferredProjectDirectory))
                            {
                                preferredProjectDirectory = originalProjectDirectory;
                            }

                        }

                        // Now the solution will have precreated the original destination directory. Try to delete it. It
                        // Will fail if not empty - which is what we want. We don't want to delete it if the directory is the 
                        // solution folder - user has unchecked the create directory for solution option
                        try
                        {
                            if (!originalProjectDirectory.EnsureTrailingBackslash().Equals(solutionDirectory.EnsureTrailingBackslash(), StringComparison.OrdinalIgnoreCase) &&
                                !originalProjectDirectory.EnsureTrailingBackslash().Equals(preferredProjectDirectory.EnsureTrailingBackslash(), StringComparison.OrdinalIgnoreCase))
                            {
                                Directory.Delete(originalProjectDirectory);
                            }
                        }
                        catch
                        {
                            // As above, if this fails we march on.
                        }
                        string relOutdir = PathUtilities.MakeRelativePath(preferredProjectDirectory, Path.Combine(solutionDirectory, "artifacts")).RemoveTrailingBackslash();
                        projectTemplateWithCustomParams = AppendBaseOutputLocationToken(projectTemplateWithCustomParams, relOutdir);

                        // Now create the real project in the solution folder
                        ((EnvDTE80.SolutionFolder)preferredSolutionFolder.Object).AddFromTemplate(projectTemplateWithCustomParams, preferredProjectDirectory, projectName);
                    }
                    else
                    {
                        // Just the simple create project case
                        string relOutdir = PathUtilities.MakeRelativePath(originalProjectDirectory, Path.Combine(solutionDirectory, "artifacts")).RemoveTrailingBackslash();
                        projectTemplateWithCustomParams = AppendBaseOutputLocationToken(projectTemplateWithCustomParams, relOutdir);
                        Solution.AddFromTemplate(projectTemplateWithCustomParams, originalProjectDirectory, projectName, Exclusive: false);
                    }
                }
            }
            finally
            {
                _oleServiceProvider = null;
            }
        }

        /// <summary>
        /// Adds a Nuget.Config file to the solution as a solution item
        /// </summary>
        private void AddNugetConfigFile(string solutionDirectory, Dictionary<string, string> feedSources)
        {
            StringBuilder builder = new StringBuilder();

            // form the string for adding the feeds
            foreach (var feedSource in feedSources)
            {
                if (!string.IsNullOrWhiteSpace(feedSource.Key) && !string.IsNullOrWhiteSpace(feedSource.Value))
                {
                    builder.AppendLine();
                    builder.AppendFormat("    <add key=\"{0}\" value=\"{1}\" />", feedSource.Key, feedSource.Value);
                }
            }

            // Get our template from the embedded resources.
            string configFile = ResourceUtilities.GetEmbeddedResourceFile("Microsoft.VisualStudio.Web.ProjectSystem.Resources.new.NuGet.Config.xml");

            // Copy it to the target filename and add it to the solution items folder
            string nugetFile = Path.Combine(solutionDirectory, ProjectConstants.NugetConfigFilename);
            File.WriteAllText(nugetFile, string.Format(configFile, builder.ToString()), Encoding.UTF8);

            new EnvDTEHelpers(_serviceProvider).AddFileToSolutionItems(nugetFile);
        }

        /// <summary>
        /// Infers from the paths whether the CreateDirectoryForSolution was checked or not
        /// </summary>
        private bool CreateSolutionDirWasChecked(string solutionDirectory, string originalProjectDirectory, string projectName)
        {
            // The paths are based on the solution name
            string slnName = GetString("$specifiedsolutionname$");
            if(string.IsNullOrEmpty(slnName))
            {
                slnName = projectName;
            }
            if(originalProjectDirectory.StartsWith(solutionDirectory.EnsureTrailingBackslash()) && originalProjectDirectory.TrimEnd('\\').EndsWith(slnName + "\\" + projectName, StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }
            return false;
        }

        /// <summary>
        /// Returns the path of the solution folder if the folder exists on disk.NOTE that this code also
        /// handles nested solutionFolders. ie if the solution folder hierarhcy is MySolutionfolder\SolutionFolder the
        /// expected path will be solutionDirectory\MySolutionfolder\SolutionFolder. 
        /// </summary>
        private string GetSolutionFolderPath(string solutionDirectory, EnvDTE.Project solutionFolder, bool failIfNotExists)
        {
            Debug.Assert(solutionFolder.Object is EnvDTE80.SolutionFolder);
            string relPath = BuildRelativePathToSolutionFolder(solutionFolder);
            Debug.Assert(relPath != null);
            if(relPath != null)
            {
                string solutionFolderPath = Path.Combine(solutionDirectory, relPath);

                if(failIfNotExists && !Directory.Exists( solutionFolderPath))
                {
                    return null;
                }
                return solutionFolderPath;
            }
            return null;
        }

        /// <summary>
        /// Return the preferred solution folder as a EnvDTE.Project for the project type being created.
        /// </summary>
        private EnvDTE.Project GetPreferredSolutionFolder(bool createIfNotExist)
        {
            if (GetBool("$oneaspnettestproject$"))
            {
                // For test projects added from One ASPNET dialog with (unit test checkbox), we prefer solution folders in following order
                // 1. "test" directory,  if one exists or exclusive
                // 2. selected solution folder node
                // 3. default folder (typically solution root)
                return GetSolutionFolder(SolutionItems.SolutionFolderNameTest, createIfNotExist) ?? GetSelectedSolutionFolderIfAny();
            }
            else if (GetBool("$ktestproject$"))
            {
                // For stand alone K test projects, we prefer solution folders in following order
                // 1. selected solution folder node
                // 2. "test" directory,  if one exists or exclusive
                // 3. default folder (typically solution root)
                return GetSelectedSolutionFolderIfAny() ?? GetSolutionFolder(SolutionItems.SolutionFolderNameTest, createIfNotExist);
            }
            else
            {
                // For code projects
                // 1. selected solution folder node
                // 2. "src" directory,  if one exists or exclusive
                // 3. default folder (typically solution root)
                return GetSelectedSolutionFolderIfAny() ?? GetSolutionFolder(SolutionItems.SolutionFolderNameSrc, createIfNotExist);
            }
        }

        /// <summary>
        /// Returns the Project object that represents the solution folder. Use Project.Object
        /// to get the actual solution folder. Note that the following code assumes the solution
        /// folder names are unique. Ideally it would be passed a relpath to it
        /// </summary>
        private EnvDTE.Project GetSolutionFolder(string solutionFolderName, bool createIfNotExist)
        {
            foreach (EnvDTE.Project solutionItem in Solution.Projects)
            {
                if (solutionItem.Object is EnvDTE80.SolutionFolder)
                {
                    if(solutionItem.Name.Equals(solutionFolderName, StringComparison.OrdinalIgnoreCase))
                    {
                        return solutionItem;
                    }
                    // Walk into this subfolder
                    EnvDTE.Project possibleSubFolder = FindSubSolutionFolder(solutionItem, solutionFolderName);
                    if (possibleSubFolder != null)
                    {
                        return possibleSubFolder;
                    }
                }
            }

            // If exclusive (new solution) or it is the oneaspnet project being added as part of a new solution and preferred solution folder we want doesnt exist,
            // then we create the folder we want
            if (createIfNotExist)
            {
                return Solution.AddSolutionFolder(solutionFolderName);
            }

            return null;
        }

        private EnvDTE.Project FindSubSolutionFolder(EnvDTE.Project solnFolder, string solutionFolderName)
        {
            Debug.Assert(solnFolder.Object is EnvDTE80.SolutionFolder);
            EnvDTE.ProjectItems items =solnFolder.ProjectItems;
            if (items != null)
            {
                foreach (EnvDTE.ProjectItem item in items)
                {
                    // Is this a project (real or solution folder)
                    if (item.SubProject != null)
                    {
                        if(item.SubProject.Object is EnvDTE80.SolutionFolder)
                        {
                            if(item.SubProject.Name.Equals(solutionFolderName, StringComparison.OrdinalIgnoreCase))
                            {
                                return item.SubProject;
                            }
                            EnvDTE.Project possibleSubFolder = FindSubSolutionFolder(item.SubProject, solutionFolderName);
                            if (possibleSubFolder != null)
                            {
                                return possibleSubFolder;
                            }
                        }
                    }
                }
            }
            return null;
        }    

        /// <summary>
        /// If the current selection is a solution folder it returns that object
        /// </summary>
        /// <returns></returns>
        private EnvDTE.Project GetSelectedSolutionFolderIfAny()
        {
            // One ASPNET dialog passes selection to us because it allows creating multiple projects (project & unit test project)
            // And in that case, the selection needs to be atomic (i.e. same selection for both projects)
            if (!string.IsNullOrEmpty(GetString("$oneaspnetselectedsolutionfoldernone$")))
            {
                // If *no* selected solution folder is passed by one aspnet, return null
                return null;
            }

            if (!string.IsNullOrEmpty(GetString("$oneaspnetselectedsolutionfolder$")))
            {
                // If selected solution folder is passed to us, use that value
                return GetSolutionFolder(GetString("$oneaspnetselectedsolutionfolder$"), false);
            }

            // Check if any solution folder is selected, if yes, we use that
            if (DTE.SelectedItems != null && DTE.SelectedItems.Count == 1)
            {
                EnvDTE.SelectedItem selectedItem = DTE.SelectedItems.Item(1);

                if (selectedItem != null && selectedItem.Project != null && selectedItem.Project.Object is EnvDTE80.SolutionFolder)
                {
                    return selectedItem.Project;
                }
            }

            return null;
        }

        /// <summary>
        /// Helper to get a string value from the replacements dictionary
        /// </summary>
        private string GetString(string key)
        {
            string value;
            _replacementsDictionary.TryGetValue(key, out value);
            return value;
        }

        private bool GetBool(string key)
        {
            bool result;
            if (!bool.TryParse(GetString(key), out result))
            {
                result = false;
            }

            return result;
        }

        /// <summary>
        /// Helper to append a string value to the custom project string
        /// </summary>
        private string AppendToken(string projectString, string name, string value)
        {
            return string.Format("{0}|${1}$={2}", projectString, name, value);
        }

        /// <summary>
        /// Append the $baseoutputlocation$
        /// </summary>
        private string AppendBaseOutputLocationToken(string projectString, string location)
        {
            return AppendToken(projectString, "baseoutputlocation", location);
        }

        /// <summary>
        /// Append the $dnxtargetframeworkversion$. This takes a version string in the form of 4.5.2 and turns it to dnx452
        /// </summary>
        private string AppendDnxTargetFrameworkVersionToken(string projectString, string version)
        {
            // 4.5.1 is the min dnx version.
            if (string.Equals(version, "4.5"))
            {
                version = "4.5.1";
            }
            return AppendToken(projectString, "dnxtargetframeworkversion", String.Format("dnx{0}", version.Replace(".", "")));
        }

        /// <summary>
        /// Helper returns true if directory is empty
        /// </summary>
        private bool DirectoryIsEmpty(string folderPath)
        {
            var dirEnum  = Directory.EnumerateFileSystemEntries(folderPath, "*.*", SearchOption.TopDirectoryOnly);
            foreach(var item in dirEnum)
            {
                return false;
            }
            return true;
        }

        /// <summary>
        /// Helper gets the path to the project template to creatd
        /// </summary>
        /// <returns></returns>
        private string GetProjectTemplatePath()
        {
            string wizardName = GetString(ProjectWizardName);
            Debug.Assert(!string.IsNullOrEmpty(wizardName), string.Format("Invalid wizard. Need to set the custom parameter {0}", ProjectWizardName));

            var currentDirectory = Path.GetDirectoryName(_thisVstemplateFile);

            string projectTemplate = Path.Combine(currentDirectory, wizardName);

            if (!File.Exists(projectTemplate))
            {
                projectTemplate = Path.Combine(GetAspNetTemplateInstallPath(true), wizardName);
            }

            return projectTemplate;
        }

        /// <summary>
        /// Helper to get the path to the asp.net templates. The assumption is that
        /// the lcid (path) _thisVsTtemplateFile and that we are in a folder immediately under
        /// the lcid folder. Like ...\csharp\web\1033\EmptyWeb\Emptyweb.vstemplate
        /// 
        /// </summary>
        private string GetAspNetTemplateInstallPath(bool includeLCID)
        {
            string templatesPath;
            if(includeLCID)
            {
                string lcid = GetGrandparentFolderName(_thisVstemplateFile);
                templatesPath = Path.Combine(new string[] {GetInstallDirectory(), AspNetProjectTemplatesFolder, lcid});
            }
            else
            {
                templatesPath = Path.Combine(GetInstallDirectory(), AspNetProjectTemplatesFolder);
            }
            return templatesPath;
        }

        /// <summary>
        /// REturns the items grantparent folder
        /// c:\Test\bar\junk\file.exe
        /// 
        /// returns bar
        /// </summary>
        private string GetGrandparentFolderName(string filePath)
        {
            return Path.GetFileName(Path.GetDirectoryName(Path.GetDirectoryName(_thisVstemplateFile).RemoveTrailingBackslash()).RemoveTrailingBackslash());
        }

        /// <summary>
        /// Helper to get the install directory
        /// </summary>
        private string GetInstallDirectory()
        {
            string installDirectory = null;

            IVsShell shell = _serviceProvider.GetService<IVsShell>();
            if (shell != null)
            {
                object installDirectoryObj = null;
                shell.GetProperty((int)__VSSPROPID.VSSPROPID_InstallDirectory, out installDirectoryObj);
                if (installDirectoryObj != null)
                {
                    installDirectory = installDirectoryObj as string;
                }
            }

            return installDirectory;
        }

        /// <summary>
        /// FWalks forward starting at the solution until it finds the specified solutionFolder. As it
        /// does the walk it builds up the relative path to the solution folder.
        /// </summary>
        public string BuildRelativePathToSolutionFolder(EnvDTE.Project solutionFolder)
        {
            foreach(EnvDTE.Project  p in Solution.Projects)
            {
                if(p.Equals(solutionFolder))
                {
                    return solutionFolder.Name;
                }
                if(p.Object is EnvDTE80.SolutionFolder)
                {
                    string newRelPath;
                    if(WalkIntoSolutionFolder(p, solutionFolder, p.Name, out newRelPath))
                    {
                        return newRelPath;
                    }
                }
            }
            return null;
        }
        /// <summary>
        /// Called by the above to recurse into solution folders
        /// </summary>
        private bool WalkIntoSolutionFolder(EnvDTE.Project p, EnvDTE.Project solutionFolder, string relPath, out string newRelPath)
        {
            // Not a real project, but a solution folder. What a hack
            newRelPath = null;
            EnvDTE.ProjectItems items = p.ProjectItems;
            if(items != null)
            {
                foreach(EnvDTE.ProjectItem item in items)
                {
                    if(item.SubProject != null && item.SubProject.Equals(solutionFolder))
                    {
                        newRelPath = relPath + "\\" + item.SubProject.Name;
                        return true;
                    }
                    if(item.SubProject != null && item.SubProject.Object is EnvDTE80.SolutionFolder)
                    {
                        if(WalkIntoSolutionFolder(item.SubProject, solutionFolder, relPath + "\\" + item.SubProject.Name, out newRelPath))
                        {
                            return true;
                        }
                    }
                }
            }

            return false;
        }

        /// <summary>
        /// IWizard
        /// A number of interfaces we don't care about
        /// </summary>
        public void BeforeOpeningFile(EnvDTE.ProjectItem projectItem)
        {
        }

        public void ProjectFinishedGenerating(EnvDTE.Project project)
        {
        }

        public void ProjectItemFinishedGenerating(EnvDTE.ProjectItem projectItem)
        {
        }

        public bool ShouldAddProjectItem(string filePath)
        {
            return true;
        }
    }
}
RehanSaeed commented 8 years ago

Cool, I'll look into creating a wizard when I get a chance.

sayedihashimi commented 8 years ago

@RehanSaeed ok great thanks!

RehanSaeed commented 8 years ago

@sayedihashimi I have a working solution but I need a little help:

namespace TemplateBuilder
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.IO;
    using Microsoft.Build.Construction;
    using Microsoft.Build.Evaluation;
    using Microsoft.VisualStudio.TemplateWizard;

    public class FixArtifactsPathWizard : IWizard
    {
        private const string BaseIntermediateOutputPathPropertyName = "BaseIntermediateOutputPath";
        private const string OutputPathPropertyName = "OutputPath";

        #region Public Methods

        public void BeforeOpeningFile(global::EnvDTE.ProjectItem projectItem)
        {
        }

        public void ProjectFinishedGenerating(global::EnvDTE.Project project)
        {
            var projectFilePath = project.FileName;
            var solutionFilePath = project.DTE.Solution.FileName;

            if (!string.Equals(Path.GetExtension(projectFilePath), ".xproj"))
            {
                return;
            }

            var projectDirectoryPath = Path.GetDirectoryName(projectFilePath);
            var solutionDirectoryPath = string.IsNullOrEmpty(solutionFilePath) ? projectDirectoryPath : Path.GetDirectoryName(solutionFilePath);

            var artifactsObjDirectoryPath = Path.Combine(solutionDirectoryPath, @"artifacts\obj\$(MSBuildProjectName)");
            var artifactsBinDirectoryPath = Path.Combine(solutionDirectoryPath, @"artifacts\bin\$(MSBuildProjectName)");

            var relativeObjPackagesDirectoryPath = GetRelativePath(
                projectDirectoryPath,
                artifactsObjDirectoryPath);
            var relativeBinPackagesDirectoryPath = GetRelativePath(
                projectDirectoryPath,
                artifactsBinDirectoryPath);

            //<PropertyGroup Label="Globals">
            //  <ProjectGuid>6e0ef33d-3c19-4ea2-8ca9-c7bf19bdd947</ProjectGuid>
            //  <RootNamespace>MvcBoilerplate</RootNamespace>
            //  <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
            //  <OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
            //</PropertyGroup>

            bool hasChanged = false;
            Project buildProject = new Project(projectFilePath);

            var configuration = project.ConfigurationManager.ActiveConfiguration;

            var baseIntermediateOutputPathProperty = buildProject.GetProperty(BaseIntermediateOutputPathPropertyName);
            var baseIntermediateOutputPathElement = buildProject
                .Xml
                .PropertyGroups
                .FirstOrDefault(x => string.Equals(x.Label, "Globals"))?
                .Children
                .OfType<ProjectPropertyElement>()
                .FirstOrDefault(x => string.Equals(x.Name, BaseIntermediateOutputPathPropertyName));
            if (baseIntermediateOutputPathElement != null &&
                !string.Equals(baseIntermediateOutputPathElement.Value, relativeObjPackagesDirectoryPath.TrimEnd('\\'), StringComparison.Ordinal))
            {
                baseIntermediateOutputPathElement.Value = relativeObjPackagesDirectoryPath.TrimEnd('\\');
                //buildProject.SetProperty(BaseIntermediateOutputPathPropertyName, relativeObjPackagesDirectoryPath.TrimEnd('\\'));
                //buildProject.SetGlobalProperty(BaseIntermediateOutputPathPropertyName, relativeObjPackagesDirectoryPath.TrimEnd('\\'));
                //configuration.Properties.Item(BaseIntermediateOutputPathPropertyName).Value = relativeObjPackagesDirectoryPath.TrimEnd('\\');
                hasChanged = true;
            }

            var outputPathProperty = buildProject.GetProperty(OutputPathPropertyName);
            var outputPathElement = buildProject
                .Xml
                .PropertyGroups
                .FirstOrDefault(x => string.Equals(x.Label, "Globals"))?
                .Children
                .OfType<ProjectPropertyElement>()
                .FirstOrDefault(x => string.Equals(x.Name, OutputPathPropertyName));
            if (outputPathElement != null &&
                !string.Equals(outputPathElement.Value, relativeBinPackagesDirectoryPath, StringComparison.Ordinal))
            {
                outputPathElement.Value = relativeBinPackagesDirectoryPath;
                //buildProject.SetProperty(OutputPathPropertyName, relativeBinPackagesDirectoryPath);
                //buildProject.SetGlobalProperty(OutputPathPropertyName, relativeBinPackagesDirectoryPath);
                //configuration.Properties.Item(OutputPathPropertyName).Value = relativeBinPackagesDirectoryPath;
                hasChanged = true;
            }

            if (hasChanged)
            {
                buildProject.Save();
                //project.Save();
            }
        }

        public void ProjectItemFinishedGenerating(global::EnvDTE.ProjectItem projectItem)
        {
        }

        public void RunFinished()
        {
        }

        public void RunStarted(
            object automationObject,
            Dictionary<string, string> replacementsDictionary,
            WizardRunKind runKind,
            object[] customParams)
        {
        }

        public bool ShouldAddProjectItem(string filePath)
        {
            return true;
        }

        #endregion

        #region Private Static Methods

        /// <summary>
        /// Creates a relative path from one file or folder to another.
        /// </summary>
        /// <param name="fromPath">Contains the directory that defines the start of the relative path.</param>
        /// <param name="toPath">Contains the path that defines the endpoint of the relative path.</param>
        /// <returns>The relative path from the start directory to the end path.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="fromPath"/> or <paramref name="toPath"/> is <c>null</c>.</exception>
        /// <exception cref="UriFormatException"></exception>
        /// <exception cref="InvalidOperationException"></exception>
        private static string GetRelativePath(string fromPath, string toPath)
        {
            if (string.IsNullOrEmpty(fromPath))
            {
                throw new ArgumentNullException(nameof(fromPath));
            }

            if (string.IsNullOrEmpty(toPath))
            {
                throw new ArgumentNullException(nameof(toPath));
            }

            var fromUri = new Uri(AppendDirectorySeparatorChar(fromPath));
            var toUri = new Uri(AppendDirectorySeparatorChar(toPath));

            if (fromUri.Scheme != toUri.Scheme)
            {
                return toPath;
            }

            var relativeUri = fromUri.MakeRelativeUri(toUri);
            var relativePath = Uri.UnescapeDataString(relativeUri.ToString());

            if (string.Equals(toUri.Scheme, Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase))
            {
                relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
            }

            return relativePath;
        }

        private static string AppendDirectorySeparatorChar(string directoryPath)
        {
            if (!Path.HasExtension(directoryPath) &&
                !directoryPath.EndsWith(Path.DirectorySeparatorChar.ToString()))
            {
                return directoryPath + Path.DirectorySeparatorChar;
            }

            return directoryPath;
        }

        #endregion
    }
}

So in my above code I am using Microsoft.Build.Evaluation.Project to modify the file paths. When I call buildProject.Save(); you get a dialog box popping up saying that "the project has changed do you want to reload it". To avoid this, in my other wizard the trick was to call project.Save() (using EnvDTE.Project) instead. However, when I do this, the changes are not made to the project.

Somehow, in my other Wizard that fixes NuGet hint paths, even though I was changing this project using Microsoft.Build.Evaluation.Project, when I used project.Save() (using EnvDTE.Project) it actually made the change to the project. You can see from my comments that I've tried several methods, what am I missing?

sayedihashimi commented 8 years ago

In .xproj we interact with MSBuild via Common Project System (CPS). I saw project.save in your existing code and it does work as you describe for .csproj/.vbproj. If that doesn't work for you in .xproj then its likely because its a different project system and it accesses those projects in a different way. Lets see what @BillHiebert thinks and see what's the best idea here for .xproj.

RehanSaeed commented 8 years ago

@sayedihashimi Cool, I'm not going totally crazy then. Is there some way to modify the project using EnvDTE.Project instead? I tried a bunch of ways and all threw exceptions. If there is then we could use that on its own.

sayedihashimi commented 8 years ago

Chatted with @billhiebert quickly and he suggested asking @lifengl

@lifengl in csproj/vbproj you can modify projects in VS using MSBuild APIs. For example https://github.com/ligershark/template-builder/blob/master/src/TemplateBuilder/FixNuGetPackageHintPathsWizard.cs. To summarize you get a hold of the project using Project buildProject = new Project(projectFilePath);, modify it and then call buildProject.Save(). In cs/vb projects the project in VS is modified. Using the same code in xproj (and I assume other CPS based project types) the same code doesn't result in the project file getting updated.

What's the proper way to get a hold of the MSBuild project in the case that CPS is being used?

RehanSaeed commented 8 years ago

@sayedihashimi @lifengl To expand further:

CS/VB Projects

  1. Create MSBuild Project Project buildProject = new Project(envdteProject);
  2. Modify buildProject.
  3. When you
    • Call envdteProject.Save() then the project is actually saved correctly.
    • Call buildProject.Save() then the project is saved but you get a dialog asking you to reload the project.

.xproj Projects

  1. Create MSBuild Project Project buildProject = new Project(envdteProject);
  2. Modify buildProject.
  3. When you
    • Call envdteProject.Save() then the project is actually not saved.
    • Call buildProject.Save() then the project is saved but you get a dialog asking you to reload the project.

Calling buildProject.Save() is not a great user experience. Would really love for a way to get envdteProject.Save() working for xproj projects.

codewithtyler commented 8 years ago

@RehanSaeed not sure if you've looked at this yet or not but when we created the Template Reference experience we programmatically reloaded the project. Lately while I was working on the new Aurelia templates I've had to use the template reference and I know that this does not prompt the user. Depending on what would have to be done to get envdeteProject.Save(); to work this might be an alternative that you can use.

RehanSaeed commented 8 years ago

@RandomlyKnighted Cool, that worked.

Even better, as an added side effect it fixed the problem where the NPM and DNX packages stopped updating because I was editing project.json and package.json in another wizard. I had a comment asking users to reload the project if this happens. @sayedihashimi As I understand it this would have been fixed anyway in RC2 right?

FYI, PR created.

codewithtyler commented 8 years ago

@RehanSaeed I'm glad to hear that fixed your issue.

codewithtyler commented 8 years ago

@RehanSaeed with the merge of your latest PR and the recent release of the TemplateBuilder v1.1.4.7-beta is this issue resolved?

RehanSaeed commented 8 years ago

@RandomlyKnighted @sayedihashimi I've upgraded ASP.NET Boilerplate but have discovered a worrying bug. When I have ASP.NET Boilerplate and SideWaffle installed at the same time and try to create a new project using my template I get an error saying that the FixArtifactsPathWizard could not be found. When I uninstall SideWaffle, I can create projects just fine. Presumably VS is picking up SideWaffle's old copy of TemplateBuilder without the FixArtifactsPathWizard.

sayedihashimi commented 8 years ago

What version is it installing?

RehanSaeed commented 8 years ago

I'm trying out the latest version 1.1.4.7-beta. I never had this problem in the past with the FixNuGetPackageHintPathsWizard because I didn't use it straight after release.

sayedihashimi commented 8 years ago

@RehanSaeed it may be because of the -beta. You said that it's installing the wrong version, what version is it installing?

RehanSaeed commented 8 years ago

My boilerplate VSIX contains the latest version 1.1.4.7-beta of TemplateBuilder.dll. I even cheched the DLL using reflector for the new wizard. Its either a bug or I'm missing something very obvious. Take a look at the VSIX yourself here.

sayedihashimi commented 8 years ago

I'm seeing it, see the image below. artifacts

RehanSaeed commented 8 years ago

Perhaps I haven't explained myself, here are the full steps to recreate.

  1. Install ASP.NET Boilerplate from here (pre-release version) and Sidewaffle template pack extensions.
  2. Create a new ASP.NET Core project using ASP.NET MVC Boilerplate and you will see this error message:

untitled

  1. Now uninstall the Sidewaffle template pack.
  2. Create a new ASP.NET Core project using ASP.NET MVC Boilerplate and there will be no error, the project will be created successfully.

Sidewaffle template pack is using an older version of TemplateBuilder.dll without the FixArtifactsPathWizard. Somehow, my ASP.NET Boilerplate extension is picking up the Sidewaffle template pack version of TemplateBuilder.dll.

sayedihashimi commented 8 years ago

@RehanSaeed OK thanks for the info. I tried it and it did not repro on my box. I'm guessing it's some race condition and in some cases the SideWaffle version of the assembly is getting loaded first. I think the issue is that the version of the assembly is always 1.0.0.0. I just made an update to change the assembly version to match the nuget package version in 1.1.4.8.

The publish to nuget.org just finished so it should be available in a minute or two. Can you update to this latest version and let me know if it still repros?

You'll have to update the version number in your .vstemplate files. We will also need to do the same in SideWaffle whenever we update the TemplateBuilder nuget package.

RehanSaeed commented 8 years ago

Latest update works fine. I've released a new version of Boilerplate including this change. Will report back if I get people reporting issues.