The problem with the EnvDTE ItemOperations.NewFile method

During my work on the latest version of the Snippet Designer I was struggling with solving an issue with the EnvDTE method ItemOperations.NewFile. The purpose of the NewFile method is to create a new file in the same manner as if you created it from the File->New->File  dialog in Visual Studio. I used this method as part of the Snippet Designer’s Export as Snippet feature. The NewFile method was working fine (I thought) until I started receiving some bug reports from French and German users. After looking into it deeper I learned that the way I was using the NewFile method would not work in non-English installations of Visual Studio. To understand why you need to know a little bit more about ItemOperations.NewFile.

The NewFile method is define as:

Window NewFile(
	string Item,
	string Name,
	string ViewKind
)

When I would launch a new snippet file I would call the method like:

 Dte.ItemOperations.NewFile( @"General\Text File", "SnippetFile1.snippet");

The MSDN documentation for the first argument of this method is

The virtual path to the item based on the tree nodes from the left pane of the dialog box and the item name from the right pane.

This is where the problem was, the virtual path is language dependent! So while in English the correct path is  “General\Text File” in Spanish it is something like “General\Archivo”.  At this point I looked into this further to see if anyone else has this issue with no luck.

This left me with two options:

  1. Figure out each language specific virtual path
  2. Find a different way to have Visual Studio launch the file

Option 1 is an annoying and fragile solution so I started looking for alternate methods. Eventually I got the idea to try to send the command directly to the Visual Studio shell’s IOleCommandTarget::Exec method. The IOleCommandTarget interface helps define the command design pattern which VS uses. Many components in VS implement this interface (explicitly or implicitly) so that they can handle commands from the user.

Getting the IOleCommandTarget for the Visual Studio shell is pretty simple once you know that there is a service for this called SUIHostCommandDispatcher

private IOleCommandTarget GetShellCommandDispatcher()
{
    return GetService(typeof (SUIHostCommandDispatcher)) as IOleCommandTarget;
}

Then looking through the VSConstants.VSStd97CmdID list I found the FileNew command id

FileNew     Command id:”File New” on File menu.

With the both the command target and the command id I was able to execute the command:

private bool LaunchNewFile(string fileName)
{
    IntPtr inArgPtr = Marshal.AllocCoTaskMem(200);
    Marshal.GetNativeVariantForObject(fileName, inArgPtr);

    Guid cmdGroup = VSConstants.GUID_VSStandardCommandSet97;
    IOleCommandTarget commandTarget = GetShellCommandDispatcher();
    int hr = commandTarget.Exec(ref cmdGroup,
                                (uint) VSConstants.VSStd97CmdID.FileNew,
                                (uint) OLECMDEXECOPT.OLECMDEXECOPT_DODEFAULT,
                                inArgPtr,
                                IntPtr.Zero);
    return ErrorHandler.Succeeded(hr);
}

One thing to note above is that in order to pass the file name for FileNew command into the Exec method you need to marshal the string into a native object since IOleCommandTarget is just a managed interface on top of native COM interfaces.

After all of that I was able to correctly launch a new file through Visual Studio that works regardless of locale.

  • Jared Parsons

    200 seems like an odd choice for the amount of memory to allocate for the fileName. Can you elaborate on this choice?

  • https://www.google.com/accounts/o8/id?id=AItOawlrLeowWytSCscAcNv3ky4tdtP7AcgDAC8 Matthew

    200 bytes might be too high. My logic was that a char is 2 bytes and that I would never have a filename more than 100 characters long.

    Do you think there is a problem with allocation this much?

  • Jared Parsons

    I was actually wondering why you didn’t just use 512 (MAX_PATH * 2).

  • https://www.google.com/accounts/o8/id?id=AItOawlrLeowWytSCscAcNv3ky4tdtP7AcgDAC8 Matthew

    Ah, that makes much more logical sense :)

  • Jared Parsons

    Good to hear. Mainly because I wanted to validate my own understanding of how these arguments are intended to be Marshalled :)

    This morning I sent an email asking almost the exact same question about how to properly marshal arguments to this API. I read the response, then went to my blog roll and happened to immediately see this article. It’s nice to see both samples agree on the appropriate way to marshal the values.

  • ING

    Great post!