Select-Play-Select-3-0-Select

Last time, I created an installer with a shortcut in the user's start menu to a very simple application. Once that application is installed, it should be possible to right-click the shortcut and select "Run As Administrator" if you wanted to run the application as an administrator. Unfortunately, that option is not available for the shortcut installed with my Windows Installer package. What gives?

Context menu for advertised shortcut

It turns out that advertised shortcuts do not have this option, while non-advertised shortcuts do. How can someone tell which shortcuts are advertised and which are non-advertised? Well, if you're using a setup project, they are all advertised. To make them non-advertised, we'll need a little post-build magic.

The Shortcut Table entry in MSDN describes what it means to have a shortcut be non-advertised:

The field (Target) should contains a property identifier enclosed by square brackets ([ ]), that is expanded into the file or a folder pointed to by the shortcut.

That should clear things up. If not, look at the example in the Specifying Shortcuts walkthrough: the non-advertised shortcuts are formatted strings of the form [ #filekey]. So the good news is it won't be necessary to write any new tasks to accomplish this. The bad news is, we'll have to do some funky stuff to get MSBuild to do what we need it to do.

Start by creating the setup project for installing: SetupNonAdvertisedShortcuts. I had this project match a lot of the behavior of SetupNoElevation from last time. I also added another executable, CommandLineArgs.exe from SetupLaunchapplication because I wanted to make this example a little more tricky. I added a shortcut to the SimpleApplication to both the Desktop and the start menu, and added a shortcut to CommandLineArgs.exe to the start menu.

Next, add a postbuild step. Let's just add the project file from last time around, with the usual command-line parameters. This project file will do 2 different tasks: the SummaryInfo update from last time, as well as the non-advertised shortcuts for this time. So, the project file will start off like this:

 <Project xmlns="https://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="PostBuild"> 
    <Import Project="$(MSBuildExtensionsPath)\\SetupProjects\\SetupProjects.Targets" /> 
 
    <PropertyGroup> 
        <PostBuildDependsOn>NoElevation;NonAdvertisedShortcuts</PostBuildDependsOn> 
    </PropertyGroup> 
 
    <Target Name="PostBuild" DependsOnTargets="$(PostBuildDependsOn)" /> 
 
    <!--  
            No Elevation target 
    --> 
     
    <PropertyGroup> 
        <PID_WORDCOUNT>15</PID_WORDCOUNT> 
    </PropertyGroup> 
 
    <Target Name="NoElevation"> 
        <GetSummaryInfo MsiFileName="$(BuiltOutputPath)" 
                        PropertyIdentifiers="$(PID_WORDCOUNT)"> 
            <Output TaskParameter="SummaryInfo" ItemName="WordCount" /> 
        </GetSummaryInfo> 
 
        <AssignValue Items="@(WordCount)"  
                     Metadata="Value"  
                     Operator="Or"  
                     Value="0x8"> 
            <Output TaskParameter="ModifiedItems" ItemName="UpdatedWordCount" /> 
        </AssignValue> 
 
        <SetSummaryInfo MsiFileName="$(BuiltOutputPath)" 
                        SummaryInfo="@(UpdatedWordCount)" /> 
    </Target> 
 
</Project>

The PostBuild target is empty, but has 2 dependencies: NoElevation and NonAdvertisedShortcuts. NoElevation is exactly the same stuff as last time. So now its only necessary to define the NonAdvertisedShortcuts target.

This Target will convert all shortcut to non-advertised shortcuts. It starts with the assumption that all of the shortcuts within the MSI are pointing to files (as opposed to folders). Ideally, this target would grab all of the entries out of the shortcut table and join these with appropriate entries from the file table, based on Component_ column within each table. The Target column would end up being [#{File.File}]. For example, if we grabbed the following data out of the Shortcut table:

Shortcuts
Shortcut
Component_
_07FF8550467741DF83F0A745A8F8D3B4 C__4FE2FE1E91B9E7CBEA0CC779BA1485CC
_0AC4DC265C1E4F39A9AFCD2B592488AE C__10812A57967D596B3D0597E4309F0C8A
_0C140C82CCD14896AFC19C6B4F386499 C__4FE2FE1E91B9E7CBEA0CC779BA1485CC

and the following out of the File table:

Files
File
Component_
_10812A57967D596B3D0597E4309F0C8A C__10812A57967D596B3D0597E4309F0C8A
_4FE2FE1E91B9E7CBEA0CC779BA1485CC C__4FE2FE1E91B9E7CBEA0CC779BA1485CC

we would end up with a Shortcut data that looked like this:

ModifiedShortcuts
Shortcut
Component_
Target
_07FF8550467741DF83F0A745A8F8D3B4 C__4FE2FE1E91B9E7CBEA0CC779BA1485CC [#_4FE2FE1E91B9E7CBEA0CC779BA1485CC]
_0AC4DC265C1E4F39A9AFCD2B592488AE C__10812A57967D596B3D0597E4309F0C8A [#_10812A57967D596B3D0597E4309F0C8A]
_0C140C82CCD14896AFC19C6B4F386499 C__4FE2FE1E91B9E7CBEA0CC779BA1485CC [#_4FE2FE1E91B9E7CBEA0CC779BA1485CC]

I asked around and an operation like join isn't available in MSBuild: adding the correct File metadata based to the shortcut entry based on the Component_ is tricky. The filtering is possible, but confusing (see this sample question and explanation). However the merging is trickier. A suggestion was made to use the MSBuild task to run a target that does the merging for us, like this:

 <ItemGroup> 
    <_ShortcutColumns Include="Component_" /> 
</ItemGroup> 
 
<ItemGroup> 
    <_FileColumns Include="File" /> 
    <_FileColumns Include="Component_" /> 
</ItemGroup> 
 
<Target Name="NonAdvertisedShortcuts"> 
    <Select MsiFileName="$(BuiltOutputPath)" 
            TableName="File" 
            Columns="@(_FileColumns)"> 
        <Output TaskParameter="Records" ItemName="Files" /> 
    </Select> 
 
    <MSBuild Projects="$(MSBuildProjectFile)" Targets="SetShortcutTarget" Properties="Component_=%(Files.Component_);_Target=[#%(Files.File)]"> 
        <Output TaskParameter="TargetOutputs" ItemName="ModifiedShortcuts" /> 
    </MSBuild> 
 
    <Update MsiFileName="$(BuiltOutputPath)" 
            Records="@(ModifiedShortcuts)" />                 
</Target> 
 
<Target Name="SetShortcutTarget" 
        Outputs="@(ModifiedShortcuts)"> 
 
    <GetPrimaryKeys MsiFileName="$(BuiltOutputPath)"  
                    TableName="Shortcut"> 
        <Output TaskParameter="PrimaryKeys" ItemName="_ShortcutColumns" /> 
    </GetPrimaryKeys> 
 
    <Select MsiFileName="$(BuiltOutputPath)" 
            TableName="Shortcut" 
            Columns="@(_ShortcutColumns)" 
            Where="`Component_`='$(Component_)'"> 
        <Output TaskParameter="Records"  ItemName="Shortcuts" /> 
    </Select> 
 
    <CreateItem Include="@(Shortcuts)" AdditionalMetaData="Target=$(_Target)"> 
        <Output TaskParameter="Include" ItemName="ModifiedShortcuts" /> 
    </CreateItem> 
</Target>

In this case, the relevant File information (the Component_ and File key) value is passed through to the SetShortcutTarget target. The SetShortcutTarget gets the primary keys of the Shortcut table, appending the values onto the list of other columns to get (Component_). A call to Select is made, searching for the specific component that was passed as input to the target. The items found then have the Target property set to the input _Target property. Why the _ in front of _Target? MSBuild complained about setting a property called Target. So I made it different by throwing the _ in there. Finally, these Shortcuts are passed back out of the Target via the Outputs value. As the SetShortcutTarget target continues to be called because of the batching, the ModifiedShortcuts list keeps getting bigger and bigger, until all of the File entries have been used, and the msi can now be updated with the Update task.

Is this technique optimal? Not even close. There are 2 hits into the Windows Installer package with each call to SetShortcutTarget: one to get the primary keys of the Shortcut table, and another to select all shortcuts belonging to a specific component. The call to GetPrimaryKeys could be eliminated simply by including the primary key of the Shortcut table (which is Shortcut) in the ItemGroup list. But based on the MSBuild experts I spoke with, there is no avoiding the multiple calls to Select.

Is this technique functional? Yes, it gets the job done. Try installing the generated Windows Installer package to a Vista machine. When you're finished, you'll see the Run as Administrator option in your context menu.

Context menu of non-advertised shortcut

SetupProjects.Tasks-1.0.20822.0-src.zip