How To: Publish Files Which are not in the Project
Every one in a while on our ClickOnce forums, we are asked how to publish files that aren't part of the project. The short answer is: you can't. The ClickOnce project is only capable of publishing files (build output, data, or content files) which are part of the project (or referenced by the project). If you have a file in a different project that you want to add to your publishing project, you can't simply add that item to your ClickOnce project: the project system will copy the file to the ClickOnce project directory. This means that any change you make to the file in the original project will be lost in the ClickOnce project, unless you remember to make the corresponding change in your ClickOnce project. It's possible to better manage this mess by adding the item as a link; that helps a little, but you still have to add all of the files you want to publish into the project.
This wouldn't be an issue if the ClickOnce project were able to deal with Project Output Groups like a setup project can. Hopefully I will get to that in a later post, but for now let's keep it a little more simple: is there some way to include all of the files in a given directory in the ClickOnce publish? This is, after all, how mage operates. The answer is "yes", if you are willing to make some by-hand modifications to a project file. I won't be dealing with assemblies in this post; the need to do so isn't as pressing: I think it's more rare for an application to require a lot of assemblies as references that aren't adequately addressed with the current system for dealing with references. Indeed, you can think of project-to-project references as similar to project output groups, and other references match the "link" analogy, as references are not copied locally and then dealt with statically by the project system. Instead, I want to focus on how those assemblies which may require their own content files can make sure those are deployed with the ClickOnce application.
Let's get started by exploring how information from the application files dialog makes the journey to what gets written into the application manifest. For those of you who want to play along at home, I created a solution containing a Windows Forms Application and 2 class library projects. I also added 5 text files to a sub-folder of the WinForms project, one of which was added as a link. I changed a bunch of settings in the application files dialog, like this:
Viewing the contents of the project file, we see this:
<ItemGroup> <PublishFile Include="ClassLibrary1"> <Visible>False</Visible> <Group></Group> <PublishState>Include</PublishState> <TargetPath></TargetPath> <FileType>Assembly</FileType> </PublishFile> <PublishFile Include="ClassLibrary2"> <Visible>False</Visible> <Group></Group> <PublishState>Prerequisite</PublishState> <TargetPath></TargetPath> <FileType>Assembly</FileType> </PublishFile> <PublishFile Include="NewFolder1\\TextFile2.txt"> <Visible>False</Visible> <Group>Foo</Group> <PublishState>Auto</PublishState> <TargetPath></TargetPath> <FileType>File</FileType> </PublishFile> <PublishFile Include="NewFolder1\\TextFile3.txt"> <Visible>False</Visible> <Group></Group> <PublishState>Exclude</PublishState> <TargetPath></TargetPath> <FileType>File</FileType> </PublishFile> <PublishFile Include="NewFolder1\\TextFile4.txt"> <Visible>False</Visible> <Group>Foo</Group> <PublishState>DataFile</PublishState> <TargetPath></TargetPath> <FileType>File</FileType> </PublishFile> <PublishFile Include="NewFolder1\\TextFileLink.txt"> <Visible>False</Visible> <Group></Group> <PublishState>Include</PublishState> <TargetPath></TargetPath> <FileType>File</FileType> </PublishFile> </ItemGroup>
All sorts of observations can be made:
- Application File items are stored as Items with name "
Includevalue is the name of the file in the application files dialog
- If a metadata value is in the default state, it is generally left blank
- If the entire item is in a default state, there is no entry for it in the project file (see TextFile1.txt for example)
- There are 5 different metadata values for the
Visible, which determines whether or not the Item appears in the solution explorer (always "False" because all of these have different representations within the project file)
Group, which corresponds to the Download Group in the application files dialog
PublishState, which corresponds to how the file is distributed. There appear to be 5 different values
(Auto): The file is in its default state
Include: The file will be published with the project
Exclude: The file will NOT be published with the project
DataFile: The file will be published as a data file
Prerequisite: The assembly will not be published with the application, but will be written into the manifest as a prerequisite
TargetPath, which corresponds to the location relative to the application manifest the file will be published to (this is value is not settable through the application files dialog)
FileType, which describes what kind of file this is
Now that we have some idea of how files are included in output, let's see how they are applied by MSBuild. To do that, we start by opening Microsoft.Common.targets. Performing a search for
@(PublishFile), we see the following entry:
<!-- Create list of items for manifest generation --> <ResolveManifestFiles EntryPoint="@(_DeploymentManifestEntryPoint)" ExtraFiles="@(_DebugSymbolsIntermediatePath);$(IntermediateOutputPath)$(TargetName).xml;@(_ReferenceRelatedPaths)" Files="@(ContentWithTargetPath);@(_DeploymentManifestIconFile);@(AppConfigWithTargetPath)" ManagedAssemblies="@(_DeploymentReferencePaths);@(ReferenceDependencyPaths);@(_SGenDllsRelatedToCurrentDll)" NativeAssemblies="@(NativeReferenceFile);@(_DeploymentNativePrerequisite)" PublishFiles="@(PublishFile)" SatelliteAssemblies="@(IntermediateSatelliteAssembliesWithTargetPath);@(ReferenceSatellitePaths)" TargetCulture="$(TargetCulture)"> <Output TaskParameter="OutputAssemblies" ItemName="_DeploymentManifestDependencies"/> <Output TaskParameter="OutputFiles" ItemName="_DeploymentManifestFiles"/> </ResolveManifestFiles>
ResolveManifestFiles task takes all sorts of information (including
PublishFile items). It returns the
_DeploymentManifestFiles items, respectively. These items are then used by the
GenerateApplicationManifest and a pair of
Copy tasks within the
_CopyFilesToPublishFolder target. Before we latch onto these values to publish files not in the project, let's see what sort of metadata the items carry along.
You could write your own task to figure this out, or you could take my word for it: here is the relevant metadata names for
It's a similar group to what was written for
PublishFile items. It appears that (for files)
PublishState was used mapped to
IsDataFile by the
ResolveManifestFiles task. The
FileType metadata was used used to break all
PublishFile items into the assemblies and files group.
Let's check out the metadata values from these names. Let's modify the winform project file to display all of this metadata. Unload the project within Visual Studio and edit it with the following info:
<Target Name="AfterPublish"> <Message Text="_DeploymentManifestFiles (Identity=%(_DeploymentManifestFiles.Identity)) \tab Group=%(_DeploymentManifestFiles.Group) \tab TargetPath=%(_DeploymentManifestFiles.TargetPath) \tab IsDataFile=%(_DeploymentManifestFiles.IsDataFile)" /> </Target> </Project>
After publishing, the output window shows (amongst other things):
_DeploymentManifestFiles (Identity=..\..\..\..\TextFileLink.txt) Group= TargetPath=NewFolder1\TextFileLink.txt IsDataFile=false _DeploymentManifestFiles (Identity=NewFolder1\TextFile1.txt) Group= TargetPath=NewFolder1\TextFile1.txt IsDataFile=false _DeploymentManifestFiles (Identity=NewFolder1\TextFile2.txt) Group=Foo TargetPath=NewFolder1\TextFile2.txt IsDataFile=false _DeploymentManifestFiles (Identity=NewFolder1\TextFile4.txt) Group= TargetPath=NewFolder1\TextFile4.txt IsDataFile=true
So to add additional files to our output, we need only figure out how to generate new
_DeploymentManifestFiles items. It turns out this is easy enough. Consider the following piece of information written into the project file:
<ItemGroup> <AdditionalPublishFile Include="C:\\Documents and Settings\\mwade\\My Documents\\My Pictures\\*.jpg"> <Visible>False</Visible> </AdditionalPublishFile> </ItemGroup> <Target Name="BeforePublish"> <Touch Files="@(IntermediateAssembly)" /> <CreateItem Include="@(AdditionalPublishFile)" AdditionalMetadata="TargetPath=%(FileName)%(Extension);IsDataFile=false"> <Output TaskParameter="Include" ItemName="_DeploymentManifestFiles" /> </CreateItem> </Target>
These statements take all of the jpg files in my My Pictures directory, stores each item in a
AdditionalPublishFile. Then as part of
BeforePublish target, the
PublishDirectoryFile are used (via the
CreateItem task) to pre-populate the
_DeploymentManifestFiles items. The
CreateItem task sets the necessary metadata for
Touch task is used to make sure that
GenerateApplicationManifest task is re-run as part of the publish process.
Touch modifies the write time of the primary executable. This file is an input to the
GenerateApplicationManifest task, and with a write time later than the write time of the application manifest (the task output), MSBuild will not skip the task. Publishing the project shows that these files are indeed written into the application manifest. Furthermore, the publishing service makes sure these files are published to the remote server. You can use your own MSBuild magic to include which ever files you want to include.
One thing to note: I have only tested this on VS 2008, and I don't know how well things will work on Visual Studio 2005. My expectation is that the manifest generation should be basically the same; however, its possible the files will not be automatically published with the application.
One final note: strictly speaking, MSBuild guidelines indicate that things which begin with "_" are supposed to indicate that the things are not for public consumption. So, we could take the approach of re-defining the Publish tasks to enable this scenario (and we could get that to work), but that would involve re-defining targets which are also considered "private" because they start with "_" as well. So either way, we're doing something we shouldn't; taking this approach will definitely be the easiest route.
Update 7/2/2008: Added information regarding the