25

Is it possible to modify a ItemGroup's metadata after it is declared.

For Example:

  <ItemGroup>
    <SolutionToBuild Include="$(BuildProjectFolderPath)\MySolution.sln">
      <Targets></Targets>
      <Properties></Properties>
    </SolutionToBuild>

  </ItemGroup>

  <Target Name="BuildNumberOverrideTarget">
     <!--Code to get the version number from a file (removed)-->

     <!--Begin Pseudo Code-->
     <CodeToChangeItemGroupMetaData 
           ItemToChange="%(SolutionToBuild.Properties)" 
           Condition ="'%(SolutionToBuild.Identity)' ==
                       '$(BuildProjectFolderPath)\MySolution.sln'"
           NewValue="Version=$(Version)" />
     <!--End Pseudo Code-->         

  </Target>

I am hoping there is a way that does not require me to remove the item then re-declare it.

Thanks for any answers. Vaccano

Vaccano
  • 78,325
  • 149
  • 468
  • 850

6 Answers6

42

Yes you can modify or add to an <ItemGroup>'s meta data after it is defined (MSBuild 3.5)

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <!-- Define ItemGroup -->
  <ItemGroup>
    <TestItemGroup Include="filename.txt">
      <MyMetaData>Test meta data</MyMetaData>
    </TestItemGroup>
    <TestItemGroup Include="filename2.txt">
      <MyMetaData>Untouched</MyMetaData>
    </TestItemGroup>
  </ItemGroup>

  <Target Name="ModifyTestItemGroup" BeforeTargets="Build">
    <!-- Show me-->
    <Message Text="PRE:  %(TestItemGroup.Identity)  MyMetaData:%(TestItemGroup.MyMetaData)  OtherMetaData:%(TestItemGroup.OtherMetaData)" Importance="high" />

    <!-- Now change it - can only do it inside a target -->
    <ItemGroup>
      <TestItemGroup Condition="'%(TestItemGroup.MyMetaData)'=='Test meta data' AND 'AnotherCondition'=='AnotherCondition'">
        <MyMetaData>Well adjusted</MyMetaData>
        <OtherMetaData>New meta data</OtherMetaData>
      </TestItemGroup>
    </ItemGroup>

    <!-- Show me the changes -->
    <Message Text="POST: %(TestItemGroup.Identity)  MyMetaData:%(TestItemGroup.MyMetaData)  OtherMetaData:%(TestItemGroup.OtherMetaData)" Importance="high" />
  </Target>

  <Target Name="Build" />
</Project>

Reference: MSDN Library: New Methods for Manipulating Items and Properties (MSBuild)

Jonathan
  • 6,939
  • 4
  • 44
  • 61
Alex
  • 2,815
  • 2
  • 24
  • 22
5

There is a new way to modify metadata by using the Update attribute E.g.

<ItemGroup>
    <Compile Update="somefile.cs">  // or Update="*.designer.cs"
        <MetadataKey>MetadataValue</MetadataKey>
    </Compile>
</ItemGroup>  

However:

Optional attribute. (Available only for .NET Core projects in Visual Studio 2017 or later.)

More in the MSBuild documentation

0xC0000022L
  • 20,597
  • 9
  • 86
  • 152
honzajscz
  • 2,850
  • 1
  • 27
  • 29
3

I had to write a custom task to do this:

Here is how it works

<ItemGroup>
  <ItemsToChange Include="@(SolutionToBuild)">
    <Properties>ChangedValue</Properties>
  </ItemsToChange>
  <MetaDataToChange Include="Properties"/>
</ItemGroup>

<UpdateMetadata SourceList="@(SolutionToBuild)" ItemsToModify="@(ItemsToChange)" MetadataToModify="@(MetaDataToChange)">
  <Output TaskParameter="NewList" ItemName="SolutionToBuildTemp" />
</UpdateMetadata>

<ItemGroup>
  <SolutionToBuild Remove="@(SolutionToBuild)"/>
  <SolutionToBuild Include ="@(SolutionToBuildTemp)"/>
</ItemGroup>

It fills a new item called SolutionToBuildTemp with the changed value. I then remove everything in the SolutionToBuild item and fill it iwith the SolutionToBuildTemp item.

Here is the code for the task if anyone is interested (I did submit it to the MSBuildExtenstionPack too).

// By Stephen Schaff (Vaccano).  
// Free to use for your code. Need my Permission to Sell it.
using System;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace UpdateMetadata
{
    ///<summary>
    /// Used to update the metadata in a ItemGroup (Note: Requires an MSBuild Call After using this task to complete the update.  See Usage.)
    /// Usage:
    /// &lt;?xml version="1.0" encoding="utf-8"?&gt;
    ///&lt;Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Testing" ToolsVersion="3.5"&gt;
    /// 
    ///  &lt;!-- Do not edit this --&gt;
    ///  &lt;Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\TeamBuild\Microsoft.TeamFoundation.Build.targets" /&gt;
    ///  &lt;UsingTask AssemblyFile="C:\Base\Junk\UpdateMetadata\UpdateMetadata\bin\Debug\UpdateMetadata.dll" TaskName="UpdateMetadata"/&gt;
    /// 
    /// 
    ///  &lt;!--Re-setup the solutions to build definition--&gt;
    ///  &lt;ItemGroup&gt;
    ///    &lt;SolutionToBuild Include="$(BuildProjectFolderPath)\ChangeThisOne.sln"&gt;
    ///      &lt;Properties&gt;Change&lt;/Properties&gt;
    ///    &lt;/SolutionToBuild&gt;
    ///    &lt;SolutionToBuild Include="$(BuildProjectFolderPath)\ChangeThisToo.sln"&gt;
    ///      &lt;Properties&gt;Change&lt;/Properties&gt;
    ///    &lt;/SolutionToBuild&gt;
    ///    &lt;SolutionToBuild Include="$(BuildProjectFolderPath)\DontChangeThisOne.sln"&gt;
    ///      &lt;Properties&gt;Don'tChange&lt;/Properties&gt;
    ///    &lt;/SolutionToBuild&gt;
    ///  &lt;/ItemGroup&gt;
    /// 
    ///  &lt;Target Name="Testing"&gt;
    ///    &lt;Message Text="Before = %(SolutionToBuild.Identity) %(SolutionToBuild.Properties)" /&gt;
    /// 
    ///    &lt;ItemGroup&gt;
    ///      &lt;ItemsToChange Include="@(SolutionToBuild)"&gt;
    ///        &lt;Properties&gt;ChangedValue&lt;/Properties&gt;
    ///      &lt;/ItemsToChange&gt;
    ///   
    ///      &lt;ItemsToChange Remove="%(ItemsToChange.rootdir)%(ItemsToChange.directory)DontChangeThisOne%(ItemsToChange.extension)"/&gt;      
    ///    &lt;/ItemGroup&gt;
    /// 
    ///    &lt;ItemGroup&gt;
    ///      &lt;MetaDataToChange Include="Properties"/&gt;
    ///    &lt;/ItemGroup&gt;
    /// 
    ///    &lt;UpdateMetadata SourceList="@(SolutionToBuild)" ItemsToModify="@(ItemsToChange)" MetadataToModify="@(MetaDataToChange)"&gt;
    ///      &lt;Output TaskParameter="NewList" ItemName="SolutionToBuildTemp" /&gt;
    ///    &lt;/UpdateMetadata&gt;
    /// 
    ///    &lt;ItemGroup&gt;
    ///      &lt;SolutionToBuild Remove="@(SolutionToBuild)"/&gt;
    ///      &lt;SolutionToBuild Include ="@(SolutionToBuildTemp)"/&gt;
    ///    &lt;/ItemGroup&gt;
    ///         
    ///    &lt;Message Text="After  = %(SolutionToBuild.Identity) %(SolutionToBuild.Properties)"/&gt;
    ///  &lt;/Target&gt;
    ///&lt;/Project&gt;
    ///</summary>
    public class UpdateMetadata : Task
    {
        ///<summary>
        /// The list to modify.
        ///</summary>
        [Required]
        public ITaskItem[] SourceList { get; set; }

        ///<summary>
        /// Items in <see cref="SourceList"/> to change the Metadata for.  
        /// It should have the valid metadata set.
        ///</summary>
        [Required]
        public ITaskItem[] ItemsToModify { get; set; }


        ///<summary>
        /// List of metadata to modify.  This is an item group, but any metadata in it is ignored.
        ///</summary>
        public ITaskItem[] MetadataToModify { get; set; }

        ///<summary>
        /// If true then info about the update is output
        ///</summary>
        public Boolean OutputMessages { get; set; }

        ///<summary>
        /// Changed List.  If you call the following it can replace the <see cref="SourceList"/>:
        ///</summary>
        [Output]
        public ITaskItem[] NewList { get; set; }

        ///<summary>
        /// Runs the task to output the updated version of the property
        ///</summary>
        ///<returns></returns>
        public override bool Execute()
        {
            // If we got empty params then we are done.
            if ((SourceList == null) || (ItemsToModify == null) || (MetadataToModify == null))
            {
                Log.LogMessage("One of the inputs to ModifyMetadata is Null!!!", null);
                return false;
            }
            if (OutputMessages)
                Log.LogMessage(MessageImportance.Low, "Beginning Metadata Changeover", null);
            int sourceIndex = 0;
            foreach (ITaskItem sourceItem in SourceList)
            {
                // Fill the new list with the source one
                NewList = SourceList;
                foreach (ITaskItem itemToModify in ItemsToModify)
                {
                    // See if this is a match.  If it is then change the metadat in the new list
                    if (sourceItem.ToString() == itemToModify.ToString())
                    {
                        foreach (ITaskItem metadataToModify in MetadataToModify)
                        {
                            try
                            {

                                if (OutputMessages)
                                    Log.LogMessage(MessageImportance.Low, "Changing {0}.{1}",
                                        NewList[sourceIndex].ToString(), metadataToModify.ToString());
                                // Try to change the metadata in the new list.
                                NewList[sourceIndex].SetMetadata(metadataToModify.ToString(),
                                                                 itemToModify.GetMetadata(metadataToModify.ToString()));

                            }
                            catch (System.ArgumentException exception)
                            {
                                // We got some bad metadata (like a ":" or something).
                                Log.LogErrorFromException(exception);
                                return false;
                            }
                        }
                    }
                }
                sourceIndex += 1;
            }

            return true;
        }
    }
}

I hope this is useful to some one, but the code is obviously "Use at your own risk".

Vaccano

Vaccano
  • 78,325
  • 149
  • 468
  • 850
1

In response to Nomad, it appears that a target gets a copy of the current value of properties and items when the target is called. In your example you can fix the problem by calling the DefaultTarget after the PreProcess target has completed. One way to do this would be to specify that the DefaultTarget depends on the PreProcess target:

<Target Name="DefaultTarget" DependsOnTargets="PreProcess"> ... </Target>

Rich257
  • 11
  • 1
1

I tried using the UpdateMetaData TaskAction from the MSBuildHelper task in the 4.0 extensions pack but it didn't do what I expected so I went with the remove/replace method. In this example I'm trying to update the DestinationRelativePath metadata property in the FilesForPackagingFromProject item group.

    <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">

    <PropertyGroup>
        <CopyAllFilesToSingleFolderForPackageDependsOn>
            $(CopyAllFilesToSingleFolderForPackageDependsOn);
            SetFilePathsToRoot;
        </CopyAllFilesToSingleFolderForPackageDependsOn>
    </PropertyGroup>

    <Target Name="SetFilePathsToRoot">

        <Message Text="Stripping \bin directory from package file paths" />

        <!-- Tweak the package files' DestinationRelativePath property such that binaries don't go into a \bin directory -->
        <ItemGroup>
            <ModifiedFilesForPackagingFromProject Include="@(FilesForPackagingFromProject)">
                <DestinationRelativePath>%(FileName)%(Extension)</DestinationRelativePath>
            </ModifiedFilesForPackagingFromProject>
        </ItemGroup>

        <ItemGroup>
            <FilesForPackagingFromProject Remove="@(FilesForPackagingFromProject)" />
            <FilesForPackagingFromProject Include="@(ModifiedFilesForPackagingFromProject)" />
        </ItemGroup>

    </Target>

</Project>
David Peters
  • 1,938
  • 1
  • 20
  • 18
1

It isn't possible to modify an existing Item, but you can create a new list.

<CreateItem Include="@(SolutionToBuild)"  
            AdditionalMetadata="Version=$(Version)" >
      <Output ItemName="SolToBuildMods" TaskParameter="Include" />
</CreateItem>
<Message Text="%(SlnToBuildMods.Identity) %(SlnToBuildMods.Version)" />
Scott Weinstein
  • 18,890
  • 14
  • 78
  • 115