Sail Away With Me To Another World
If I had any blog readership and a place to send questions to, I anticipate one would look something like this:
(From the mailbag): I've created some cool new games that I want people to be able to run. I want each game to appear as a separate entry in a user's Start menu. I've been able to do this no problem for Windows XP, but every time a Vista user tries to install, they get an elevation prompt. How can I get rid of that?
Rattled in Redmond
Dear Rattled,
Make sure that the application is getting installed into the user's profile. For example, all registry keys should be in HKCU, not HKLM or HKCR. Also, don't install any files to the Program Files folder either.
(From the mailbag): Yeah, I already did that, Slick. I'm still getting prompted.
Rattled in Redmond
Dear Rattled,
Wow! What an efficient mailbag this is! I should have had this fake idea a long time ago!
You need to set the LUAAware bit in your Windows Installer package. Its the third-order bit of the PID_WORDCOUNT part of the summary info stream.
(From the mailbag): I'm using Visual Studio 2005 to create my package. So how am I supposed to do that?
Rattled in Redmond
Dear Rattled,
Well, you could do it via a postbuild step. This sounds like the perfect opportunity to write a ridiculously long and convoluted blog post to show how to do this.
(From the mailbag): Fantastic. Wake me when its over.
Rattled in Redmond
Background
As we can see there is a lot of information available in the summary info stream. This info comes in 3 different flavors: ints, strings, and dates. To programmatically access the summary info stream, you can access the SummaryInfo property off of a Windows Installer Database object. To access individual properties within the summary info stream, use the Property property. The property takes a property ID to indicate which particular field is getting accessed. The value is passed in and out of this property as an object, as it can have different types.
The plan is to create two different tasks; one to get values and another to set values. Let's do the right thing this time and start with a unit test to tackle dealing with summary info values. The tests I have come up with thus far are:
- Getting/setting a single value
- Getting/setting a null value
- Getting/setting multiple values
This may break some sort of unwritten rule, but I'm going to do it anyways: all 6 tests are going to be defined in the same test file, since I anticipate that the get tests will be close enough to the set tests to warrant it.
Getting Summary Info Stream Values
Below you'll find the first test for getting a single value out of the summary info stream. I'll break it all down after you have absorbed it in all its craptastic glory.
[TestClass()]
public class SummaryInformationTest
{
private string databaseFileName;
#region Additional test attributes
[TestInitialize()]
public void TestInitialize()
{
databaseFileName = Path.GetTempFileName();
FileStream fout = new FileStream(databaseFileName, FileMode.Open);
fout.Write(SetupProjects.Tasks.UnitTests.Properties.Resources.schema, 0, SetupProjects.Tasks.UnitTests.Properties.Resources.schema.Length);
fout.Close();
}
[TestCleanup()]
public void MyTestCleanup()
{
if (File.Exists(databaseFileName))
{
File.Delete(databaseFileName);
}
}
#endregion
[TestMethod()]
public void GetOneValueTest()
{
Database msi = OpenDatabase(MsiOpenDatabaseMode.msiOpenDatabaseModeTransact);
SummaryInfoValue testValue = new SummaryInfoValue(1252, 1);
SummaryInfo info = msi.get_SummaryInformation(1);
info.set_Property(testValue.Identifier, testValue.Value);
info.Persist();
msi.Commit();
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(info);
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(msi);
GetSummaryInfo target = new GetSummaryInfo();
target.MsiFileName = databaseFileName;
target.PropertyIdentifier = testValue.Identifier.ToString();
Assert.IsTrue(target.Execute(), "GetSummaryInfo failed");
Assert.AreEqual(testValue, new SummaryInfoValue(target.SummaryInfo));
}
private Database OpenDatabase(MsiOpenDatabaseMode mode)
{
Type classType = Type.GetTypeFromProgID("WindowsInstaller.Installer");
Object installerClassObject = Activator.CreateInstance(classType);
Installer installer = (Installer)installerClassObject;
return installer.OpenDatabase(databaseFileName, mode);
}
}
class SummaryInfoValue
{
private int identifier;
private object value;
public SummaryInfoValue(object value, int index)
{
this.value = value;
this.identifier = index;
}
public SummaryInfoValue(ITaskItem item)
{
identifier = int.Parse(item.ItemSpec);
value = ParseValue(item);
}
private string GetMetadata(ITaskItem item, string name)
{
foreach (string metadataName in item.MetadataNames)
{
if (name.Equals(metadataName))
{
return item.GetMetadata(name);
}
}
return null;
}
public int Identifier
{
get { return identifier; }
}
public object Value
{
get { return value; }
}
public string Type
{
get
{
if (Value == null)
{
return string.Empty;
}
return value.GetType().ToString();
}
}
public override string ToString()
{
if (Value != null)
return string.Format("SummaryInfo[{0}]: {1} ({2})", Identifier, Value.ToString(), Type);
return string.Format("SummaryInfo[{0}]: null", Identifier);
}
public override bool Equals(object obj)
{
if (!(obj is SummaryInfoValue))
{
return false;
}
SummaryInfoValue other = (SummaryInfoValue)obj;
return Identifier == other.Identifier &&
string.Equals(Type, other.Type) &&
object.Equals(Value, other.Value);
}
public override int GetHashCode()
{
int hashCode = identifier;
if (Type != null)
{
hashCode ^= Type.GetHashCode();
}
if (Value != null)
{
hashCode ^= Value.GetHashCode();
}
return hashCode;
}
private object ParseValue(ITaskItem item)
{
string val = GetMetadata(item, "Value");
string type = GetMetadata(item, "Type");
if (type == null)
{
if (val != null)
{
throw new Exception("An empty Type value requires an empty Value value");
}
return null;
}
if (val == null)
{
throw new Exception("A non-empty Type value requires that Value be specified");
}
if (type.Equals(typeof(string).ToString()))
{
return val;
}
else if (type.Equals(typeof(int).ToString()))
{
return int.Parse(val);
}
else if (type.Equals(typeof(DateTime).ToString()))
{
return DateTime.Parse(val);
}
else
{
throw new Exception("Unrecognized type: " + type);
}
}
}
I must admit, a lot of this was based on the UpdateTest class from last time around. Most of the code is in the support class to handle the anticipated structure of the summary info value: SummaryInfoValue. There are 3 pieces of information in this class:
Identifier: the PID value for this summary info information
Value: the actual property value
Type: a string representation of the type of the value. This is necessary because when a value is passed out of an ITaskItem, the value will be in string format, and an application needs to know how to convert that value into its proper data type.
It is expected that the Type will end up being string, int, or DateTime.
The test itself is pretty straight-forward: create a database, add some summary info into the database, create a task to get the value back out of the summary info stream, and make sure the value matches. Of course, right now the test doesn't compile, as there is no GetSummaryInfo task defined. Let's correct that now.
public class GetSummaryInfo : SetupProjectTask
{
protected string propertyIdentifier;
protected ITaskItem summaryInfo;
[Required]
public string PropertyIdentifier
{
get { return propertyIdentifier; }
set { propertyIdentifier = value; }
}
[Output]
public ITaskItem SummaryInfo
{
get { return summaryInfo; }
set { }
}
protected MsiOpenDatabaseMode Mode
{
get { return MsiOpenDatabaseMode.msiOpenDatabaseModeReadOnly; }
}
protected override bool ExecuteTask()
{
summaryInfo = new TaskItem(propertyIdentifier);
return true;
}
}
There is our task: one input parameter (not counting the filename which is inherited from SetupProjectTask) and one output paramter. The output parameter has its ItemSpec set to the PropertyIdentifier input.
Running the test still isn't successful:
Failed GetOneValueTest Assert.AreEqual failed. Expected:<SummaryInfo[1]: 1252 (int)>, Actual:<SummaryInfo[1]: null>.
Looks like the task actually needs to fill in some TaskItem information. There are two properties to set: the "Type" and the "Value":
protected override bool ExecuteTask()
{
// Only reading out of the summary info, so there is no need to specify anything other than 0 here
SummaryInfo info = Msi.get_SummaryInformation(0);
object value = info.get_Property(Utilities.ParseInt(PropertyIdentifier));
summaryInfo = new TaskItem(propertyIdentifier);
summaryInfo.SetMetadata("Value", value.ToString());
summaryInfo.SetMetadata("Type", value.GetType().ToString());
return true;
}
This gets that first test passing, but I see a couple of problems (in addition to the potentially unfortunate name I gave to the output parameter). Like: what happens if the retrieved summary information is null? Fortunately, that was the next test:
[TestMethod()]
public void GetNullValueTest()
{
Database msi = OpenDatabase(MsiOpenDatabaseMode.msiOpenDatabaseModeTransact);
SummaryInfo info = msi.get_SummaryInformation(1);
info.set_Property(2, null);
info.Persist();
msi.Commit();
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(info);
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(msi);
SummaryInfoValue expectedValue = new SummaryInfoValue(null, 2);
GetSummaryInfo target = new GetSummaryInfo();
target.MsiFileName = databaseFileName;
target.PropertyIdentifier = expectedValue.Identifier.ToString();
Assert.IsTrue(target.Execute(), "GetSummaryInfo failed");
Assert.AreEqual(expectedValue, new SummaryInfoValue(target.SummaryInfo));
}
This is similar to the last test, but instead of setting the
PID_CODEPAGE (which is an int) of the summary info stream, I set the
PID_TITLE (a string) to be null. The test compiles but fails when run:
Failed GetNullValueTest Test method SetupProjects.Tasks.UnitTests.SummaryInformationTest.GetNullValueTest threw exception: System.InvalidOperationException: Task attempted to log before it was initialized. Message was: Object reference not set to an instance of an object..
The result is an InvalidOperationException, which seems a little strange. Why isn't it a NullReferenceException? I don't know for sure, but I think the message from the InvalidOperationException is pretty telling: "Task attempted to log before it was initialized". Presumably the "it" referred to here is the log, and not the task. If the task was created through the normal usage as an MSBuild task, the log would have been initialized by MSBuild when the task was created. Instantiating the task directly like this, I don't bother to fully initialize the task. Then, presumably, the Task base class has the call to Execute wrapped in a try-catch block which catches the NullReferenceException and attempts to log the exception.
Regardless, it looks like the task needs to handle a null value better. Piece of cake:
protected override bool ExecuteTask()
{
// Only reading out of the summary info, so there is no need to specify anything other than 0 here
SummaryInfo info = Msi.get_SummaryInformation(0);
object value = info.get_Property(Utilities.ParseInt(PropertyIdentifier));
summaryInfo = new TaskItem(propertyIdentifier);
if (value != null)
{
summaryInfo.SetMetadata("Value", value.ToString());
summaryInfo.SetMetadata("Type", value.GetType().ToString());
}
return true;
}
The tests pass after that modification.
The final modification I want to make to the GetSummaryInfo is the ability to access multiple fields with one task call. This is similar to what was done with the Select and Update tasks, so it makes some sense here as well. The test for this is going to look like its a little bit longer, but actually follows the same pattern as the other tests. This time, though, I want to make sure I hit all 3 datatypes of summary info.
[TestMethod()]
public void GetMultipleValuesTest()
{
Database msi = OpenDatabase(MsiOpenDatabaseMode.msiOpenDatabaseModeTransact);
SummaryInfoValue[] testValues = new SummaryInfoValue[] {
new SummaryInfoValue("Test Title", 2),
new SummaryInfoValue(new DateTime(2000, 1, 1, 0, 0, 0), 10),
new SummaryInfoValue(200, 14),
};
SummaryInfo info = msi.get_SummaryInformation(testValues.Length);
for (int i = 0; i < testValues.Length; i++)
{
info.set_Property(testValues[i].Identifier, testValues[i].Value);
}
info.Persist();
msi.Commit();
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(info);
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(msi);
List<string> propertyIdentifiers = new List<string>();
for (int i = 0; i < testValues.Length; i++)
{
propertyIdentifiers.Add(testValues[i].Identifier.ToString());
}
GetSummaryInfo target = new GetSummaryInfo();
target.MsiFileName = databaseFileName;
target.PropertyIdentifiers = propertyIdentifiers.ToArray();
Assert.IsTrue(target.Execute(), "GetSummaryInfo failed");
Assert.AreEqual(testValues.Length, target.SummaryInfo.Length);
for (int i = 0; i < testValues.Length; i++)
{
Assert.AreEqual(testValues[i], new SummaryInfoValue(target.SummaryInfo[i]));
}
}
It is now expected that the task takes an array of strings for the PropertyIdentifiers, and returns an array of ITaskItems as the SummaryInfo output. So right now, this test doesn't compile. Let's update the GetSummaryInfo class to take this into account:
public class GetSummaryInfo : SetupProjectTask
{
protected string[] propertyIdentifiers;
protected List<ITaskItem> summaryInfoList = new List<ITaskItem>();
[Required]
public string[] PropertyIdentifiers
{
get { return propertyIdentifiers; }
set { propertyIdentifiers = value; }
}
[Output]
public ITaskItem[] SummaryInfo
{
get { return summaryInfoList.ToArray(); }
set { }
}
protected override bool ExecuteTask()
{
// Only reading out of the summary info, so there is no need to specify anything other than 0 here
SummaryInfo info = Msi.get_SummaryInformation(0);
for (int i = 0; i < PropertyIdentifiers.Length; i++)
{
object value = info.get_Property(Utilities.ParseInt(PropertyIdentifiers[i]));
TaskItem summaryInfo = new TaskItem(propertyIdentifiers[i]);
if (value != null)
{
summaryInfo.SetMetadata("Value", value.ToString());
summaryInfo.SetMetadata("Type", value.GetType().ToString());
}
summaryInfoList.Add(summaryInfo);
}
return true;
}
}
This gets the last test compiling, but now the other 2 no longer compile, what with the addition of PropertyIdentifiers and SummaryInfo[]. How necessary are those other 2 tests? The single value case isn't compelling anymore (if its possible to get multiple values, getting a single value is probably redundant). So, let's just add the null case into this bigger test (renamed GetSummaryInfoTest):
[TestMethod()]
public void GetSummaryInfoTest()
{
Database msi = OpenDatabase(MsiOpenDatabaseMode.msiOpenDatabaseModeTransact);
SummaryInfoValue[] testValues = new SummaryInfoValue[] {
new SummaryInfoValue("Test Title", 2),
new SummaryInfoValue(null, 3),
new SummaryInfoValue(new DateTime(2000, 1, 1, 0, 0, 0), 10),
new SummaryInfoValue(200, 14),
};
This test passes.
Setting Summary Info Stream Values
Let's take a similar baby-step approach for a corresponding SetSummaryInfo task. The first test will set a single value (the codepage) and verify the value comes out correctly:
[TestMethod]
public void SetSingleValueTest()
{
Database msi = OpenDatabase(MsiOpenDatabaseMode.msiOpenDatabaseModeTransact);
SummaryInfo info = msi.get_SummaryInformation(1);
info.set_Property(1, 1252);
info.Persist();
msi.Commit();
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(info);
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(msi);
SummaryInfoValue expectedValue = new SummaryInfoValue(932, 1);
SetSummaryInfo target = new SetSummaryInfo();
target.MsiFileName = databaseFileName;
target.SummaryInfo = expectedValue.ToTaskItem();
Assert.IsTrue(target.Execute(), "SetSummaryInfo failed");
msi = OpenDatabase(MsiOpenDatabaseMode.msiOpenDatabaseModeReadOnly);
info = msi.get_SummaryInformation(0);
Assert.AreEqual(expectedValue, new SummaryInfoValue(info.get_Property(1), 1));
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(info);
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(msi);
}
This required one new addition to the
SummaryInfoValue class:
ToTaskItem() which returns the
TaskItem representation of the object:
public ITaskItem ToTaskItem()
{
TaskItem item = new TaskItem(Identifier.ToString());
item.SetMetadata("Value", Value.ToString());
item.SetMetadata("Type", Value.GetType().ToString());
return item;
}
Right now, this is starting to feel very familiar, but lets forge ahead. This test doesn't compile because we don't have anything called
SetSummaryInfo. Stubs are always easy:
public class SetSummaryInfo : SetupProjectTask
{
ITaskItem summaryInfo;
[Required]
public ITaskItem SummaryInfo
{
get { return summaryInfo; }
set { summaryInfo = value; }
}
protected override bool ExecuteTask()
{
return true;
}
}
This gets everything to compile, but of course the test doesn't pass, since there isn't actually any "setting" going on here:
Failed Assert.AreEqual failed. Expected:<SummaryInfo[1]: 932 (System.Int32)>, Actual:<SummaryInfo[1]: 1252 (System.Int32)>.
To get this working, I need to somehow convert the input TaskItem into something that the summary info stream can handle. Hmmm... that sounds familiar. The test helper class already does that! Let's copy ParseValue from SummaryInfoValue into the task and use it:
protected override bool ExecuteTask()
{
WindowsInstaller.SummaryInfo info = Msi.get_SummaryInformation(1);
info.set_Property(Utilities.ParseInt(SummaryInfo.ItemSpec), ParseValue(SummaryInfo));
info.Persist();
Msi.Commit();
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(info);
return true;
}
With the help of this code, the test passes. And because
ParseValue already handles null correctly, I'd expect the next test to pass right off the bat:
public void SetNullValueTest()
{
Database msi = OpenDatabase(MsiOpenDatabaseMode.msiOpenDatabaseModeTransact);
SummaryInfo info = msi.get_SummaryInformation(1);
info.set_Property(2, "Test Title");
info.Persist();
msi.Commit();
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(info);
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(msi);
SummaryInfoValue expectedValue = new SummaryInfoValue(null, 2);
SetSummaryInfo target = new SetSummaryInfo();
target.MsiFileName = databaseFileName;
target.SummaryInfo = expectedValue.ToTaskItem();
Assert.IsTrue(target.Execute(), "SetSummaryInfo failed");
msi = OpenDatabase(MsiOpenDatabaseMode.msiOpenDatabaseModeReadOnly);
info = msi.get_SummaryInformation(0);
Assert.AreEqual(expectedValue, new SummaryInfoValue(info.get_Property(2), 2));
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(info);
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(msi);
}
This test is basically the same as the first, except that it the PID_TITLE getting set to null. But the test doesn't pass on first run because of a NullReferenceException. This time, though, the exception is coming from the test code and not from the task. Remember when I said the ToTaskItem() method was sounding familiar? That's because I went through the same steps in getting a null value for the GetSummaryInfo task. Let's fix that up now:
public ITaskItem ToTaskItem()
{
TaskItem item = new TaskItem(Identifier.ToString());
if (Value != null)
{
item.SetMetadata("Value", Value.ToString());
item.SetMetadata("Type", Value.GetType().ToString());
}
return item;
}
And now the test passes.
The final test will be to set multiple values, 1 each of int, string, and date types, and an additional null value:
[TestMethod()]
public void SetSummaryInfoTest()
{
Database msi = OpenDatabase(MsiOpenDatabaseMode.msiOpenDatabaseModeTransact);
SummaryInfo info = msi.get_SummaryInformation(4);
info.set_Property(1, 1252);
info.set_Property(2, "Temporary Title");
info.set_Property(3, "Temporary Subject");
info.set_Property(10, DateTime.Now);
info.Persist();
msi.Commit();
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(info);
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(msi);
SummaryInfoValue[] expectedValues = new SummaryInfoValue[] {
new SummaryInfoValue(932, 1),
new SummaryInfoValue("Test Title", 2),
new SummaryInfoValue(null, 3),
new SummaryInfoValue(new DateTime(2000, 1, 1, 0, 0, 0), 10),
};
List<ITaskItem> infoItems = new List<ITaskItem>();
foreach (SummaryInfoValue expectedValue in expectedValues)
{
infoItems.Add(expectedValue.ToTaskItem());
}
SetSummaryInfo target = new SetSummaryInfo();
target.MsiFileName = databaseFileName;
target.SummaryInfo = infoItems.ToArray();
Assert.IsTrue(target.Execute(), "SetSummaryInfo failed");
msi = OpenDatabase(MsiOpenDatabaseMode.msiOpenDatabaseModeReadOnly);
info = msi.get_SummaryInformation(0);
foreach (SummaryInfoValue expectedValue in expectedValues)
{
Assert.AreEqual(expectedValue, new SummaryInfoValue(info.get_Property(expectedValue.Identifier), expectedValue.Identifier));
}
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(msi);
}
The test doesn't compile because the expected type of
SummaryInfo was changed from
ITaskItem to
ITaskItem[]. A corresponding change in the
SetSummaryInfo task is necessary:
private ITaskItem[] summaryInfo;
[Required]
public ITaskItem[] SummaryInfo
{
get { return summaryInfo; }
set { summaryInfo = value; }
}
protected override bool ExecuteTask()
{
WindowsInstaller.SummaryInfo info = Msi.get_SummaryInformation(1);
info.set_Property(Utilities.ParseInt(SummaryInfo[0].ItemSpec), ParseValue(SummaryInfo[0]));
info.Persist();
Msi.Commit();
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(info);
return true;
}
Of course, now the previous tests don't compile. So cut them just like the last time around. The test still fails because I was only setting the initial value in the array. The task needs to change to iterate over all the input Items:
protected override bool ExecuteTask()
{
if (summaryInfo.Length > 0)
{
WindowsInstaller.SummaryInfo info = Msi.get_SummaryInformation(summaryInfo.Length);
foreach (ITaskItem item in SummaryInfo)
{
info.set_Property(Utilities.ParseInt(item.ItemSpec), ParseValue(item));
}
info.Persist();
Msi.Commit();
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(info);
}
return true;
}
And now the test passes.
Rant/Additional Cleanup
I decided to do something different with these classes in declaring them "sealed", so that nothing can inherit from them (to limit the support requirements for the task). Well, it turns out that the I had an OpenMode property from the SetupProjectTask base class, that I had incorrectly changed to Mode in several derived classes. After fixing this, imagine my Java-based surprise when I got a compiler warning for this:
'SetupProjects.Tasks.GetPrimaryKeys.OpenMode' hides inherited member 'SetupProjects.Tasks.SetupProjectTask.OpenMode'. Use the new keyword if hiding was intended.
Due to some unbelievable lameness somewhere, in order for a derived class to override a base class's implementation of something, the bas class has to declare the method/property as "virtual". Given that everything I learned about object-oriented programming languages I learned from Java, this struck me as extremely lame. I'm sure there is a very good reason for this design, and not just one of the two reasons I secretly suspect (either MSIL can't handle something like this (if Java can do it, you would think that MSIL could), or this was the behavior in Visual Basic, and VB.NET wanted to continue this lameness, and there was no good way to do it without affecting C#). Or possibly someone felt that the ability to hide would be very valuable, so decided this was the only way to do this.
Anyway, I have now declared that property as virtual, and modified my other tasks to be sealed as well.
However, it seems a little strange that derived classes which don't have to specify an OpenMode do have to call Msi.Commit at the end of the task. so, I also changed the SetupProjectTask to have a read-only OpenMode, and made the classes which call Commit set to transact.
While I was snooping around, I also merged the common code from the SummaryInfoTest, UpdateTest, and UtilitiesTest classes into a base class (SetupProjectTaskTest) class from which the others now inherit.
Okay, You Can Wake Up Now
Now that all of the heavy lifting has been done, its time to add modify an MSI so that there will be no prompt for elevation when installing on Windows Vista. You may recall (from about 13 pages ago) that to enable this scenario, it is necessary to set third-order bit of the PID_WORDCOUNT in the summary info stream.
Let's create a setup project (SetupNoElevation) that will do that. To make the application slightly more "real-world", let's add some files and shortcuts to be installed when the setup is run. I created a very simple application and added the primary project output group to the Application folder in the setup project. Of course, the Application Folder is currently set to install under the Program Files directory. Let's modify that to live under the User's application data folder by setting the DefaultLocation to [AppDataFolder][Manufacturer]\[ProductName]. I also added a shortcut to the User's Programs Menu to the installed file. Also, just to be super safe, I have removed all registry keys in the registry editor. I also removed the Installation Folder dialog so that the user doesn't try to install the app somewhere they shouldn't (or try to install for all users, which lives on that same dialog). Finally, set the PostBuildEvent property to the usual value and add a PostBuild.proj file to the project with the following contents:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="PostBuild">
<Import Project="$(MSBuildExtensionsPath)\\SetupProjects\\SetupProjects.Targets" />
<PropertyGroup>
<PID_WORDCOUNT>15</PID_WORDCOUNT>
</PropertyGroup>
<Target Name="PostBuild">
<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>
There is a call to
GetSummaryInfo (get the
PID_WORDCOUNT, defined earlier as 15), use the
Assign task to
Or the old value with 8, and call
SetSummaryInfo to update the summary info stream.