locked
Executing External Tool does not flush modified files RRS feed

  • Question

  • Executing External Tool does not write modified files.  Therefore, if External Tool is written to process a specifiec file, one has to manually save it (or all files) in order for External Tool to be able to process it.  This was not the behavior up to and including Visual Studio 2005.  What gives?
    Dave
    Monday, October 17, 2011 11:19 PM

Answers

  • Hi Dave,

    I think I got your point. Yes, you are correct, in VS2005 launch the external tool will help save the file.

    After perform a research, it is a change start from VS2008, I don't think we can change it back. This issue was already logged in connect: http://connect.microsoft.com/VisualStudio/feedback/details/425059/running-external-tools-does-not-save-open-documents

    In fact, I agree it is useful for the scenario as you are facing. But for some other user, they may not really need such auto save function. It is a double-edge.

    A workaround we can provide is to write a menu button in a addin/package to invoke your external tool. Before starting your tool, use DTE automation to save the file first.

    Regards,

    Yi


    Yi Feng Li [MSFT]
    MSDN Community Support | Feedback to us
    Get or Request Code Sample from Microsoft
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.

    • Marked as answer by Yi Feng Li Tuesday, November 1, 2011 3:36 AM
    Thursday, October 20, 2011 4:08 AM
  • Here is something quick and dirty I threw together.  To get it working on your box just do the following

    File -> New Project and select the Other Project Types -> Extensibility Node and select Visual Studio AddIn.

    You can keep all the defaults but make sure that you mark the AddIn to load on startup (it is a checkbox on the 4th screen, it says 'I would like my AddIn to load when the host application starts').

    When the project is set up open up the auto-generated Connect.cs file and replace the contents with the code below

    Add references to the project for Microsoft.VisualStudio.OLE.Interop.dll and Micrsoft.VisualStudio.Shell.Interop.dll

    This page talks about deploying the AddIn, basically copying the dll + the .AddIn file from the build directory to the <VS>\AddIns directory under <user name>\Documents\Visual Studio <version>\AddIns

    The AddIn doesn't make any visible UI, you can check if it is recognized by VS by looking in the AddIn manager on the Tools menu, it should show up as well as be checked as loaded and load on startup.  All it does is anytime a command from the External Tools range of commands is executed it invokes the File.SaveAll command.  This may be slightly more aggressive than previous behavior in that it may save things other than documents (like projects/solutions), it could probably be dialed back if that wasn't desired.

    As always no warranties are expressed or implied with this code, it may have bugs as I wrote it on lunch and tested it only at a rudimentary level :)

    using System;
    using Extensibility;
    using EnvDTE;
    using EnvDTE80;
    using Microsoft.VisualStudio.OLE.Interop;
    using Microsoft.VisualStudio.Shell.Interop;
    using System.Runtime.InteropServices;
    
    using IServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider;
    using OleConstants = Microsoft.VisualStudio.OLE.Interop.Constants;
    
    namespace AutoSaveDirtyFilesAddIn
    {
    	public class Connect : IDTExtensibility2, IOleCommandTarget
    	{
    		public Connect()
    		{
    		}
    
    		public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom)
    		{
    			_applicationObject = (DTE2)application;
    			_addInInstance = (AddIn)addInInst;
    
                IVsRegisterPriorityCommandTarget registerCmdTrgt = GetService<SVsRegisterPriorityCommandTarget, IVsRegisterPriorityCommandTarget>((IServiceProvider)application);
                if (registerCmdTrgt != null)
                {
                    registerCmdTrgt.RegisterPriorityCommandTarget(0, this, out this.priCmdTargetCookie);
                }
    		}
    
    		public void OnDisconnection(ext_DisconnectMode disconnectMode, ref Array custom)
    		{
                if (this.priCmdTargetCookie != 0)
                {
                    IVsRegisterPriorityCommandTarget registerCmdTrgt = GetService<SVsRegisterPriorityCommandTarget, IVsRegisterPriorityCommandTarget>((IServiceProvider)_applicationObject);
                    if (registerCmdTrgt != null)
                    {
                        registerCmdTrgt.UnregisterPriorityCommandTarget(this.priCmdTargetCookie);                    
                    }
    
                    this.priCmdTargetCookie = 0;
                }
    		}
    
    		public void OnAddInsUpdate(ref Array custom)
    		{
    		}
    
    		public void OnStartupComplete(ref Array custom)
    		{
    		}
    
            public void OnBeginShutdown(ref Array custom)
    		{
    		}
    
            private I GetService<S, I>(IServiceProvider serviceProvider) where S : class
                                                                         where I : class
            {
                if (serviceProvider == null)
                {
                    throw new ArgumentNullException("serviceProvider");
                }
    
                IntPtr rawServicePtr = IntPtr.Zero;
                try
                {
                    Guid serviceGuid = typeof(S).GUID;
                    Guid interfaceGuid = typeof(I).GUID;
    
                    if (serviceProvider.QueryService(ref serviceGuid, ref interfaceGuid, out rawServicePtr) == 0 /*S_OK*/)
                    {
                        return (I)Marshal.GetObjectForIUnknown(rawServicePtr);
                    }
                }
                finally
                {
                    if (rawServicePtr != IntPtr.Zero)
                    {
                        Marshal.Release(rawServicePtr);
                    }
                }
    
                return (I)null;
            }
    
            #region IOleCommandTarget Members
    
            public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut)
            {
                if (pguidCmdGroup == guidVsStd97)
                {
                    if (nCmdID >= firstExternalCommandId && nCmdID <= lastExternalCommandId)
                    {
                        //invoke SaveAll command
                        IOleCommandTarget commandHostDispatcher = GetService<SUIHostCommandDispatcher, IOleCommandTarget>((IServiceProvider)_applicationObject);
                        commandHostDispatcher.Exec(ref guidVsStd97, saveAllCommandId, (uint)OLECMDEXECOPT.OLECMDEXECOPT_DODEFAULT, IntPtr.Zero, IntPtr.Zero);
                    }
                }
    
                //Say we didn't handle the command, this will allow it to continue on to the real handler.
                return (int)OleConstants.OLECMDERR_E_NOTSUPPORTED;
            }
    
            public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText)
            {
                return (int)OleConstants.OLECMDERR_E_NOTSUPPORTED;
            }
    
            #endregion
    
            private Guid guidVsStd97 = new Guid("{5efc7975-14bc-11cf-9b2b-00aa00573819}");
            private uint saveAllCommandId = 224;
            private uint firstExternalCommandId = 630;
            private uint lastExternalCommandId = 653;
    
            private uint priCmdTargetCookie;
    		private DTE2 _applicationObject;
    		private AddIn _addInInstance;
        }
    }
    

    • Proposed as answer by Yi Feng Li Tuesday, October 25, 2011 6:29 AM
    • Marked as answer by Yi Feng Li Tuesday, November 1, 2011 3:36 AM
    Thursday, October 20, 2011 10:18 PM

All replies

  • Hi Dave,

    Yes, Visual Studio will not save the modified files into disk before you build the project or save the file manually. As an external tool, it load the file from the disk directly, the most modification made in VS IDE should not be applied to the file yet. 

     

    Once you modified the file outside the VS, VS will offer a notification and let you decide if you need to reload the file from the disk.

     

    I believe this is a by-design behavior.

     

    Regards,

    Yi


    Yi Feng Li [MSFT]
    MSDN Community Support | Feedback to us
    Get or Request Code Sample from Microsoft
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.

    Tuesday, October 18, 2011 6:59 AM
  • Thanks Yi.  However, note: This was not the behavior up to and including Visual Studio 2005.  Therefore, it looks like an oversight.
    Dave
    Tuesday, October 18, 2011 5:10 PM
  • Hi

    I tested it using VS2005 and it seems VS2005 has the same the behavior as VS2010 and VS2008. What I tested is to open a project using VS2005 with some modification, and then I opened the source code file using Notepad, the notepad opened the file without any unsaved modification.

    I still believe we have to save all the files before external tools process the file.

    If I misunderstand you, please let me know.

    Regards,

    Yi


    Yi Feng Li [MSFT]
    MSDN Community Support | Feedback to us
    Get or Request Code Sample from Microsoft
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.

    Wednesday, October 19, 2011 12:26 PM
  • Hi Yi,

    I tested VS2005 again today, and it works as expected.  I have a program which I wrote, which checks the uniqueness of each value in resource.h, and reports offending values to the output window. If I modify a value to conflict with another value, and run the External Tool "rescan", the asterisk on the IDE goes away, and the error is found.  I do not have to save all files first, so that the tool can find the modified file. I do not see this behavior in VS2010.  I have not checked VS2008.  I'm not sure what steps you took, but this is exactly what happens for me.

    Dave


    Dave
    Wednesday, October 19, 2011 5:26 PM
  • A complete, stand-alone repro we could debug would be best for getting to the root of the problem the fastest.

    Ryan

    Wednesday, October 19, 2011 8:38 PM
  • Ryan,

    I thought I was pretty clear.  But, I will break it down even further.

    Using VS2005:

    1. Modify a file in the IDE.  Note that an asterisk is shown next to the file name.  This indicates that it has been modified.

    2. Run the tool "Visual Studio 2005 Command Prompt".  Notice that the asterisk goes away.  This indicates that the modified file has been written to disk.

    Try the same sequence in Visual Studio 2010.  The asterisk does not clear.

    Clear?


    Dave
    Wednesday, October 19, 2011 9:25 PM
  • I don't have VS 2005 installed and it is outside of its support window afaik.  I tried a roughly similar repro on 2008 and 2010 and neither seem to flush dirty files before executing the tools as you said.  I did this

    1:  Modify an open cpp file leaving it dirty

    2:  Execute Tools -> Create Guid (external program, there is no 'VS <version> command prompt' external tool in 2008 or 2010).

    It must have regressed in 2008 and simply carried forward in 2010 (nothing was done in this area in 2010).

    You can file a Connect bug, first I had heard of this issue/behavior.

    Ryan

    Wednesday, October 19, 2011 9:36 PM
  • Thanks,

    I guess I might have added that tool also - sorry.  I'm glad to know that you can see what I do.  As you can see, it does no good to have External Tools which can (and should be able to) operate on files which can be modified in the editor, which don't get written to disk, as when they are when compiling, etc.

    I was hoping for someone to either direct me to somewhere in the IDE to re-enable this functionality, or pick this up as a bona fide bug, and get it fixed in the next Service Pack.  I'm aware that these forums are moderated by Microsoft employees, and had I had hoped that the next step in moving the report forward was for them to do so.  I don't know what a Connect bug is, so either give me more information on how to file it, or please do it for me.

    Dave

     


    Dave
    Wednesday, October 19, 2011 11:26 PM
  • Hi Dave,

    I think I got your point. Yes, you are correct, in VS2005 launch the external tool will help save the file.

    After perform a research, it is a change start from VS2008, I don't think we can change it back. This issue was already logged in connect: http://connect.microsoft.com/VisualStudio/feedback/details/425059/running-external-tools-does-not-save-open-documents

    In fact, I agree it is useful for the scenario as you are facing. But for some other user, they may not really need such auto save function. It is a double-edge.

    A workaround we can provide is to write a menu button in a addin/package to invoke your external tool. Before starting your tool, use DTE automation to save the file first.

    Regards,

    Yi


    Yi Feng Li [MSFT]
    MSDN Community Support | Feedback to us
    Get or Request Code Sample from Microsoft
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.

    • Marked as answer by Yi Feng Li Tuesday, November 1, 2011 3:36 AM
    Thursday, October 20, 2011 4:08 AM
  • >or pick this up as a bona fide bug, and get it fixed in the next Service Pack. 

    Since it seems like an intentional (though unfortunate in your case) change as Yi points out from the Connect bug he linked there is no chance of it being fixed in a service pack, as it isn't a bug simply a purposeful behavior change that helps some people but not you :(

    >I'm aware that these forums are moderated by Microsoft employees, and had I had hoped that the next step in moving the report forward was for them to do so.  I don't know what a Connect bug is, so either give me more information on how to file it, or please do it for me.

    I am a Microsoft employee, I suggested Connect simply because customer filed bugs (ala Connect) hold about 100 times more weight than any bug I file internally.  Yi linked to a Connect bug below, so it seems it already had been filed in 2009 and closed as By Design.

    I could imagine writing a package that simply auto-loaded on startup and intercepted any invocations of items from the External Tools list and would force save all open, dirty documents before executing the real command.  An AddIn could likely also do it using DTE command events, but I am less familiar with that route.

    Ryan

    Thursday, October 20, 2011 7:31 AM
  • Hi Yi,

    Now I understand what is meant by a Connect bug.  Perhaps one could refer to the service as Microsoft Connect.  That way, the verb becomes a noun.  I read the several posts to the closed issue which you sited, and saw that there were several other users who became disappointed by this change of functionality between versions 2005 and 2008.  It seems that the decision to remove saving before executing External Tools, should have been paired with a preference in the "options menu".  That is what we do in our company.  It is simply bad business to take away functionality which can become depended upon (particularly becuase of its long life) without offering a way to maintain it through some work-around.

    I've had some experience with DTE automation, and it is too laborious, and too terse to deal with.  A recommendation using a type-in example would be desireable, but note that in closing said Connect bug report, no one offered a work-around.

    Dave


    Dave
    Thursday, October 20, 2011 6:28 PM
  • Here is something quick and dirty I threw together.  To get it working on your box just do the following

    File -> New Project and select the Other Project Types -> Extensibility Node and select Visual Studio AddIn.

    You can keep all the defaults but make sure that you mark the AddIn to load on startup (it is a checkbox on the 4th screen, it says 'I would like my AddIn to load when the host application starts').

    When the project is set up open up the auto-generated Connect.cs file and replace the contents with the code below

    Add references to the project for Microsoft.VisualStudio.OLE.Interop.dll and Micrsoft.VisualStudio.Shell.Interop.dll

    This page talks about deploying the AddIn, basically copying the dll + the .AddIn file from the build directory to the <VS>\AddIns directory under <user name>\Documents\Visual Studio <version>\AddIns

    The AddIn doesn't make any visible UI, you can check if it is recognized by VS by looking in the AddIn manager on the Tools menu, it should show up as well as be checked as loaded and load on startup.  All it does is anytime a command from the External Tools range of commands is executed it invokes the File.SaveAll command.  This may be slightly more aggressive than previous behavior in that it may save things other than documents (like projects/solutions), it could probably be dialed back if that wasn't desired.

    As always no warranties are expressed or implied with this code, it may have bugs as I wrote it on lunch and tested it only at a rudimentary level :)

    using System;
    using Extensibility;
    using EnvDTE;
    using EnvDTE80;
    using Microsoft.VisualStudio.OLE.Interop;
    using Microsoft.VisualStudio.Shell.Interop;
    using System.Runtime.InteropServices;
    
    using IServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider;
    using OleConstants = Microsoft.VisualStudio.OLE.Interop.Constants;
    
    namespace AutoSaveDirtyFilesAddIn
    {
    	public class Connect : IDTExtensibility2, IOleCommandTarget
    	{
    		public Connect()
    		{
    		}
    
    		public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom)
    		{
    			_applicationObject = (DTE2)application;
    			_addInInstance = (AddIn)addInInst;
    
                IVsRegisterPriorityCommandTarget registerCmdTrgt = GetService<SVsRegisterPriorityCommandTarget, IVsRegisterPriorityCommandTarget>((IServiceProvider)application);
                if (registerCmdTrgt != null)
                {
                    registerCmdTrgt.RegisterPriorityCommandTarget(0, this, out this.priCmdTargetCookie);
                }
    		}
    
    		public void OnDisconnection(ext_DisconnectMode disconnectMode, ref Array custom)
    		{
                if (this.priCmdTargetCookie != 0)
                {
                    IVsRegisterPriorityCommandTarget registerCmdTrgt = GetService<SVsRegisterPriorityCommandTarget, IVsRegisterPriorityCommandTarget>((IServiceProvider)_applicationObject);
                    if (registerCmdTrgt != null)
                    {
                        registerCmdTrgt.UnregisterPriorityCommandTarget(this.priCmdTargetCookie);                    
                    }
    
                    this.priCmdTargetCookie = 0;
                }
    		}
    
    		public void OnAddInsUpdate(ref Array custom)
    		{
    		}
    
    		public void OnStartupComplete(ref Array custom)
    		{
    		}
    
            public void OnBeginShutdown(ref Array custom)
    		{
    		}
    
            private I GetService<S, I>(IServiceProvider serviceProvider) where S : class
                                                                         where I : class
            {
                if (serviceProvider == null)
                {
                    throw new ArgumentNullException("serviceProvider");
                }
    
                IntPtr rawServicePtr = IntPtr.Zero;
                try
                {
                    Guid serviceGuid = typeof(S).GUID;
                    Guid interfaceGuid = typeof(I).GUID;
    
                    if (serviceProvider.QueryService(ref serviceGuid, ref interfaceGuid, out rawServicePtr) == 0 /*S_OK*/)
                    {
                        return (I)Marshal.GetObjectForIUnknown(rawServicePtr);
                    }
                }
                finally
                {
                    if (rawServicePtr != IntPtr.Zero)
                    {
                        Marshal.Release(rawServicePtr);
                    }
                }
    
                return (I)null;
            }
    
            #region IOleCommandTarget Members
    
            public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut)
            {
                if (pguidCmdGroup == guidVsStd97)
                {
                    if (nCmdID >= firstExternalCommandId && nCmdID <= lastExternalCommandId)
                    {
                        //invoke SaveAll command
                        IOleCommandTarget commandHostDispatcher = GetService<SUIHostCommandDispatcher, IOleCommandTarget>((IServiceProvider)_applicationObject);
                        commandHostDispatcher.Exec(ref guidVsStd97, saveAllCommandId, (uint)OLECMDEXECOPT.OLECMDEXECOPT_DODEFAULT, IntPtr.Zero, IntPtr.Zero);
                    }
                }
    
                //Say we didn't handle the command, this will allow it to continue on to the real handler.
                return (int)OleConstants.OLECMDERR_E_NOTSUPPORTED;
            }
    
            public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText)
            {
                return (int)OleConstants.OLECMDERR_E_NOTSUPPORTED;
            }
    
            #endregion
    
            private Guid guidVsStd97 = new Guid("{5efc7975-14bc-11cf-9b2b-00aa00573819}");
            private uint saveAllCommandId = 224;
            private uint firstExternalCommandId = 630;
            private uint lastExternalCommandId = 653;
    
            private uint priCmdTargetCookie;
    		private DTE2 _applicationObject;
    		private AddIn _addInInstance;
        }
    }
    

    • Proposed as answer by Yi Feng Li Tuesday, October 25, 2011 6:29 AM
    • Marked as answer by Yi Feng Li Tuesday, November 1, 2011 3:36 AM
    Thursday, October 20, 2011 10:18 PM
  • Hello,

    I am writing to check the status of the issue on your side.  Would you mind letting us know the result of the suggestions? 

    Yi


    Yi Feng Li [MSFT]
    MSDN Community Support | Feedback to us
    Get or Request Code Sample from Microsoft
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.

    Tuesday, October 25, 2011 6:30 AM
  • Actually, just because I'm a programmer doesn't mean that I like to spend my time programming things like this.  I can accept this recommendation as a valid work-around (but the author seems to have admitted not having tested it).  However, my recommendation remains:  Don't take stuff out before considering allowing a mechanism to allow putting it back as an option. I feel sad for the other users of Visual Studio 2008 who initially complained about the functionality being removed, and the "Connect Bug" was silently debated and closed as "by design".  There possibly weren't enough users of this functionality to have noticed, but it really was nice to have been able to throw together a tool for the code currently being edited, without all of the (verbose lines of) scripting that the above work-around prescribes.

    Dave


    Dave
    Tuesday, October 25, 2011 5:31 PM
  • Hi Dave,

     

    Thank you for update and understand our support.

     

    We're always looking for suggestions and feedback from our customers. We have submitted this issue as a feedback internally. We still recommend you create a connect feedback from custom view via MSDN Product Feedback Center to submit this idea.

     

    Regards,

    Yi


    Yi Feng Li [MSFT]
    MSDN Community Support | Feedback to us
    Get or Request Code Sample from Microsoft
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.

    Wednesday, October 26, 2011 2:14 AM
  • Hi Dave,

    Since this is a by design issue, I close the thread by marking mine and Ryan's workaround. If you have any future questions, you are welcome to reopen the thread whenever you want.

    Thank you for your understanding.

    Yi


    Yi Feng Li [MSFT]
    MSDN Community Support | Feedback to us
    Get or Request Code Sample from Microsoft
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.

    Tuesday, November 1, 2011 3:36 AM
  • The much simpler workaround would be to record a macro which:

     - will save all files (Ctrl+Shift+S)

     - will run external tool

    Then name the macro, put it somewhere and instead of running external tool run the macro (you can assign some keyboard shortcut).

    My example:

    Sub CompileCppFileViaRsp()
        DTE.ExecuteCommand("File.SaveAll")
        DTE.ExecuteCommand("Tools.ExternalCommand7")
    End Sub

    Friday, August 8, 2014 9:37 AM