5

I've create a project template which contains a csproj which contains an Import pointing to a project file where I list all the third party project locations. I'm always using this project template to create projects in the same relative directory.

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="3.5" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="../../../3rdParty/ThirdParty.targets" />
  ...
  <ItemGroup>
    <Reference Include="Library, Version=$(LibraryVersion), Culture=neutral, PublicKeyToken=$(LibraryPublicKeyToken), processorArchitecture=MSIL">
      <SpecificVersion>False</SpecificVersion>
      <HintPath>$(LibraryDir)LibraryDll.dll</HintPath>
    </Reference>
  </ItemGroup>
  ...
</Project>

The csproj file works properly in Visual Studio and when running msbuild from the command prompt. When I try to create a project using the project template I get the following error:

C:\Users...\AppData\Local\Temp\534cylld.o0p\Temp\MyModule.csproj(3,3): The imported project "C:\Users...\AppData\Local\3rdParty\ThirdParty.targets" was not found. Confirm that the path in the declaration is correct, and that the file exists on disk.

It seems that Visual Studio is trying to open the project in a temporary location first. I've tried adding $(MSBuildProjectDirectory) to the import location hoping that it might force it to use the location I intended, but that also didn't work.

Any suggestions?

Anthony Mastrean
  • 21,850
  • 21
  • 110
  • 188
Christo
  • 1,802
  • 4
  • 20
  • 31
  • I'm having the exact same problem, except that my imported file is used to execute a custom build task. I need this path to be relative because it is relative to my source control folder hierarchy. – madd0 Aug 12 '11 at 08:14
  • @Christo: Could you provide the full pah to a directory where from you are running build from command line? – sll Aug 15 '11 at 23:03

4 Answers4

10

You should set the CreateInPlace property to true in the vstemplate. The documentation says

Specifies whether to create the project and perform parameter replacement in the specified location, or perform parameter replacement in a temporary location and then save the project to the specified location.

If you want relative paths to work, you need the parameter replacement to occur in the place where you're creating the project, not in a temporary location.

Anthony Mastrean
  • 21,850
  • 21
  • 110
  • 188
Wilhelm Medetz
  • 601
  • 6
  • 15
1

I have opted for a solution using Wizards with Project Templates mainly because some of my templates already required wizards.

I created a base class that all my other wizards are supposed to extend or that can be used by itself for just basic functionality:

public class AddTargetsWizard : IWizard
{
    private const string RELATIVE_PATH_TO_TARGETS = @"..\..\..\..\PATH\TO\Custom.Tasks.Targets";

    private const string TASK_NOT_FOUND_MESSAGE = @"A project of this type should be created under a specific path in order for the custom build task to be properly executed.

The build task could not be found at the following location:
    {0}

Including the build task would result in unexpected behavior in Visual Studio.

The project was created, but the build task WILL NOT BE INCLUDED.
This project's builds WILL NOT benefit from the custom build task.";

    private string _newProjectFileName;

    private bool _addTaskToProject;

    private Window _mainWindow;

    public AddTargetsWizard()
    {
        this._addTaskToProject = true;
    }

    protected Window MainWindow
    {
        get
        {
            return this._mainWindow;
        }
    }

    public virtual void BeforeOpeningFile(EnvDTE.ProjectItem projectItem)
    {
    }

    public virtual void ProjectFinishedGenerating(EnvDTE.Project project)
    {
        this._newProjectFileName = project.FullName;

        var projectDirectory = Path.GetDirectoryName(this._newProjectFileName);

        var taskPath = Path.GetFullPath(Path.Combine(projectDirectory, RELATIVE_PATH_TO_TARGETS));

        if (!File.Exists(taskPath))
        {
            MessageBox.Show(
                this.MainWindow,
                string.Format(TASK_NOT_FOUND_MESSAGE, taskPath),
                "Project Creation Error",
                MessageBoxButton.OK,
                MessageBoxImage.Error,
                MessageBoxResult.OK,
                MessageBoxOptions.None);

                this._addTaskToProject = false;
        }
    }

    public virtual void ProjectItemFinishedGenerating(EnvDTE.ProjectItem projectItem)
    {
    }

    public virtual void RunFinished()
    {
        if (this._addTaskToProject)
        {
            var project = new Microsoft.Build.Evaluation.Project(this._newProjectFileName);

            project.Xml.AddImport(RELATIVE_PATH_TO_TARGETS);

            project.Save();
        }
    }

    public void RunStarted(object automationObject, Dictionary<string, string> replacementsDictionary, WizardRunKind runKind, object[] customParams)
    {
        var dte = (EnvDTE80.DTE2)automationObject;

        var mainWindow = dte.MainWindow;

        foreach (var proc in System.Diagnostics.Process.GetProcesses())
        {
            if (proc.MainWindowTitle.Equals(mainWindow.Caption))
            {
                var source = HwndSource.FromHwnd(proc.MainWindowHandle);
                this._mainWindow = source.RootVisual as System.Windows.Window;
                break;
            }
        }

        this.OnRunStarted(automationObject, replacementsDictionary, runKind, customParams);
    }

    protected virtual void OnRunStarted(object automationObject, Dictionary<string, string> replacementsDictionary, WizardRunKind runKind, object[] customParams)
    {
    }

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

This walkthrough provides a pretty good explanation on how to associate the wizard to a project (or item) template.

You'll notice that I am providing a virtual OnRunStarted method for child wizards that would need to provide additional functionality, such as displaying wizard windows, populating the replacements dictionary, etc.

The things I don't like about this approach and/or my implementation:

  • It is way more complicated than a plain project template.
  • In order for my wizard windows—all WPF—to be real modal windows with Visual Studio as their owner, I found no better way than to use the current instance's title to determine the HWND and associated Window.
  • All's well when the projects are created in the expected folder hierarchy, but Visual Studio behaves strangely (i.e. unhelpful dialogs pop up) otherwise. That's why I chose to display an error message and avoid inserting the Import if the current project's location does not work with my relative path.

If anyone has other ideas, I'm still all ears.

madd0
  • 9,053
  • 3
  • 35
  • 62
0

The default behavior is for the template to be unpacked in a temporary folder. Then, the parameter replacements are performed there. Somehow, the relative paths are tested and, in the temp location, the files do not exist.

Did you try to add the following line before your import?

<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
Anthony Mastrean
  • 21,850
  • 21
  • 110
  • 188
Vince
  • 1,036
  • 1
  • 10
  • 17
0

I think I might use an environment variable instead... Would that work in your situation? And if you had to share the project templates between developers, you could do something fancy in a powershell script where it would set the environment variable either automatically or by asking the developer where his template directory is.

[Environment]::SetEnvironmentVariable("3rdPartyTargets", "%ProgramFiles%/3rdParty/ThirdParty.targets", "User")

And then in csproj macro:

<Project ToolsVersion="3.5" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(3rdPartyTargets)" />
  ...

Oh wait. That works for C++, but you might have to use msbuild for a c#/vb.net proj.

Community
  • 1
  • 1
Jason
  • 4,897
  • 2
  • 33
  • 40