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 "
PublishFile
" - The
Include
value 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
PublishFile
itemVisible
, 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 dialogPublishState
, which corresponds to how the file is distributed. There appear to be 5 different values(Auto)
: The file is in its default stateInclude
: The file will be published with the projectExclude
: The file will NOT be published with the projectDataFile
: The file will be published as a data filePrerequisite
: 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 isAssembly
File
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>
This ResolveManifestFiles
task takes all sorts of information (including PublishFile
items). It returns the OutputAssemblies
and OutputFiles
as _DeploymentManifestDependencies
and _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 _DeploymentManifestFiles
:
Group
TargetPath
IsDataFile
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 _DeploymentManifestFiles
: TargetPath
, Group
, and IsDataFile
. The 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 Touch
task.
Comments
Anonymous
June 30, 2008
Have you ever noticed that when your application has a web reference, and you go to publish that webAnonymous
August 27, 2009
At what point should we expect these to appear in the "Application Files" listings (if at all)? Or is safe to assume they'll end up in our deployed data files listing? In my case I'm hoping to use: <ItemGroup> <AdditionalPublishFile Include="$(OutputPath)Resources***.*"> <Visible>False</Visible> </AdditionalPublishFile> </ItemGroup> To include all content files from dependent assemblies that are within the "Resources" subfolder of the build directory. Andrew.Anonymous
March 11, 2010
I troubled a bit with this approach, because the file was published correctly but did not appear in the application.manifest file and heck was not downloaded during installation. The solution was to do the CreateItem stuff BeforeBuild and not BeforePublish.Anonymous
March 02, 2012
The comment has been removedAnonymous
August 27, 2012
Just tried this method and it seems to work, but somehow I'm not able to get sub-folders working. An Example: <CreateItem Include="@(AdditionalPublishFile)" AdditionalMetadata="TargetPath=MYADDITIONALPATH%(FileName)%(Extension);IsDataFile=false"> Also the above example with the **. did not seem to work for me. It didn't include/copy any of the files.Anonymous
November 03, 2015
The comment has been removedAnonymous
February 02, 2016
Can anyone explain why ClickOnce makes this so complicated? Maybe it's naive, but it seems like we would generally want to include all files from the output directory into the published project.