How to get [ProgramFilesFolder] to populate to "C:\Program Files" burn UI project - wix

I have a Burn Bundle with the following variable
<Variable Name="INSTALLFOLDER" Type="string "Value="[ProgramFilesFolder]" />
With the following property in my bootstrapper UI project's main view model
public string InstallDirectory
{
get
{
if (_Engine.StringVariables.Contains("INSTALLFOLDER"))
return _Engine.StringVariables["INSTALLFOLDER"];
return string.Empty;
}
set
{
if (_Engine.StringVariables.Contains("INSTALLFOLDER"))
{
_Engine.StringVariables["INSTALLFOLDER"] = value;
OnPropertyChanged("InstallDirectory");
}
}
}
In my WPF view which has a textbox bound to the InstallDirectory property I only see "[ProgramFilesfolder]" but I was hoping to see something like "C:\Program Files"
I would like to end up with something like the following which will populate my install directory textbox with the default install folder and give the user the option to change it there.
<Variable Name='INSTALLFOLDER' Type='string' Value='[ProgramFilesFolder]$(var.AppName)' />
I could use the Net Framework to get the program files folder for my WPF UI but seems like I should be able to get it from the Wix Bundle. Also the Wix log shows that I am setting the INSTALLFOLDER property from my UI.
My bootstrapper Run looks like this:
protected override void Run()
{
this.Engine.Log(LogLevel.Verbose, "Run has been called on the UI application.");
CurrentDispatcher = Dispatcher.CurrentDispatcher;
_MainWindow = new MainWindow(new MainWindowViewModel(this));
Engine.Detect();
_MainWindow.Show();
Dispatcher.Run();
Engine.Quit(0);
}
I have thought I might need to listen to some event on the BootstrapperApplication after which I could fire on property changed for the InstallDirectory property but haven't found anything interesting yet.
I have been through the Developer Guide book for 3.6 and it doesn't seem to address this exact issue although the final two chapters do deal with burn projects and WPF.

In your get method you should be able to use this to get the actual value of the property:
get
{
if (_Engine.StringVariables.Contains("INSTALLFOLDER"))
return _Engine.FormatString("[INSTALLFOLDER]");
return string.Empty;
}

Related

WinUI notifications: COM exception is raised when instanciating instance of "AppNotification"

I wanted to include notifications in my existing WinUI 3 application which uses Windows App SDK 1.1.4 and .NET 6 (The application does not and shall not use packaging / MSIX).
In order to achieve this, I tried to extract some code of an example application that I created with the "Template studio for WinUI" project template (assistant), see https://github.com/microsoft/TemplateStudio/ (The sample application also works with the 'unpackaged' deployment model).
The code which I extracted from the example application looks like this (the relevant parts should be the methods 'Initialize' and 'Show'):
public class AppNotificationService : IAppNotificationService
{
public AppNotificationService()
{
}
~AppNotificationService()
{
Unregister();
}
public void Initialize()
{
AppNotificationManager.Default.NotificationInvoked += OnNotificationInvoked;
AppNotificationManager.Default.Register();
}
public void OnNotificationInvoked(AppNotificationManager sender, AppNotificationActivatedEventArgs args)
{
// TODO: Handle notification invocations when your app is already running.
//// // Navigate to a specific page based on the notification arguments.
//// if (ParseArguments(args.Argument)["action"] == "Settings")
//// {
//// App.MainWindow.DispatcherQueue.TryEnqueue(() =>
//// {
//// _navigationService.NavigateTo(typeof(SettingsViewModel).FullName!);
//// });
//// }
App.MainWindow.DispatcherQueue.TryEnqueue(() =>
{
App.MainWindow.ShowMessageDialogAsync("TODO: Handle notification invocations when your app is already running.", "Notification Invoked");
App.MainWindow.BringToFront();
});
}
// EXCEPTION IN THIS METHOD
public bool Show(string payload)
{
var appNotification = new AppNotification(payload); // COM EXCEPTION HERE
AppNotificationManager.Default.Show(appNotification);
return appNotification.Id != 0;
}
public NameValueCollection ParseArguments(string arguments)
{
return HttpUtility.ParseQueryString(arguments);
}
public void Unregister()
{
AppNotificationManager.Default.Unregister();
}
}
As you can see, the code contains a method "Show" that has a string for the payload that represents the notification message. In addition there is a "Initialize" method that the example code calls upon application startup.
In order to call the "Show" method of the code above, I created some small event handler in my application that gets called when I click a button:
private void CreateNotification_Click(object sender, RoutedEventArgs e)
{
AppNotificationService notificationService = new AppNotificationService();
notificationService.Initialize();
string notificationContent = "test";
notificationService.Show(notificationContent);
}
However, the call to "notificationService.Show(notificationContent);" always causes a ComException "0xC00CE556" that is raised when the code tries to instanciate the AppNotification instance see here:
I do not know what I am missing here. It seems that the template studio application does something additional to get the notification working, that I am currently not doing in my code. But I have no idea what that is. Any suggestions?
I couldn't reproduce your COM Exception but these steps worked.
Create a simple WinUI 3 app project.
Bring AppNotificationService.cs and IAppNotificationService.cs from a TemplateStudio project with app notifications.
Open Package.appxmanifest using a text editor (VSCode).
Add these namespaces:
<Package
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10">
</Package>
Declare these Extensions inside Applications:
<Applications>
<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$">
<uap:VisualElements DisplayName="WinUI3BlankAppProjectTemplate" Description="WinUI3BlankAppProjectTemplate" BackgroundColor="transparent" Square150x150Logo="Assets\Square150x150Logo.png" Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
<Extensions>
<!--Specify which CLSID to activate when notification is clicked-->
<desktop:Extension Category="windows.toastNotificationActivation">
<desktop:ToastNotificationActivation ToastActivatorCLSID="12345678-9ABC-DEFG-HIJK-LMNOPQRSTUVW" />
</desktop:Extension>
<!--Register COM CLSID-->
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:ExeServer Executable="AppNotifications.exe" Arguments="----AppNotificationActivated:" DisplayName="Toast activator">
<com:Class Id="12345678-9ABC-DEFG-HIJK-LMNOPQRSTUVW" DisplayName="Toast activator" />
</com:ExeServer>
</com:ComServer>
</com:Extension>
</Extensions>
</Application>
</Applications>
Replace Executable="AppNotifications.exe" with your app name.
Create a GUID from [Tools]-[Create GUID] in VisualStudio menu.
Replace the two GUIDs in the Extensions with the GUID you created.
Save the Package.appxmanifest file and reopen and rebuild the solution.
Call the Show method passing a valid payload. For example:
var xmlPayload =
#"
<toast launch=""action=ToastClick"">
<visual>
<binding template=""ToastGeneric"">
<text>App Notification</text>
<text></text>
</binding>
</visual>
<actions>
<action content=""Settings"" arguments=""action=Settings""/>
</actions>
</toast>
";
appNotificationService.Show(xmlPayload);
UPDATE
For un-packaged(non-packaged) apps you get a COM Exception if you don't call the Initialize() method. So, the step 10. should be something like this:
Call Initialize then Show method passing a valid payload. For example:
AppNotificationService appNotificationService = new();
appNotificationService.Initialize();
var xmlPayload =
#"
<toast launch=""action=ToastClick"">
<visual>
<binding template=""ToastGeneric"">
<text>App Notification</text>
<text></text>
</binding>
</visual>
<actions>
<action content=""Settings"" arguments=""action=Settings""/>
</actions>
</toast>
";
appNotificationService.Show(xmlPayload);
I found out what the problem was in my case.
I turned out, that the problem was related to the string I used as notification content. This must not be an arbitrary string (like "test" in the example I used in the question), but an xml string that has a specific format which is needed to represent a "toast" message.
This xml string for the toast message can contain specific elements for text, images and buttons that may appear in the message.
Simple example:
<?xml version="1.0" encoding="utf-8"?>
<toast>
<visual>
<binding template="ToastGeneric">
<text>Some text</text>
</binding>
</visual>
</toast>
This article shows an example of the possible syntax of this xml string:
Quickstart: App notifications in the Windows App SDK - 4 Display an app notification
There are also classes and helpers for the construction of the xml string that you can use by installing the "CommunityToolkit.WinUI.Notifications"
Nuget package:
ToastContent class
ToastContentBuilder class
Example code
Basically, all you need to do to create a toast notification is this:
private void CreateNotification_Click(object sender, RoutedEventArgs e)
{
// Version 1: Directly define the xmlPayload string:
// string xmlPayload = #"<?xml version=""1.0"" encoding=""utf-8""?><toast><visual><binding template=""ToastGeneric""><text>Some text</text></binding></visual></toast>";
// Version 2: Create the the xmlPayload string using the ToastContentBuilder
// ToastContentBuilder comes with the "CommunityToolkit.WinUI.Notifications" Nuget package
ToastContent toastContent = new ToastContentBuilder()
.AddText("Some text")
.GetToastContent();
string xmlPayload = toastContent.GetContent();
var toast = new AppNotification(xmlPayload);
AppNotificationManager.Default.Show(toast);
}
In the example code, I have two versions on how to create the xml string that represents the toast message. You can create the xml yourself or use the ToastContentBuilder class from the "CommunityToolkit.WinUI.Notifications" Nuget package.
Personal opinion
I know that StackOverflow does no focus on personal opinions. However, I would like to express that I am quite disappointed to see that a code like
new AppNotification("test");
raises an ComException lacking any useful information instead of an meaningful exception that contains some hint for the developer complaining about the incorrect format of the provided xml string.

Project Doesn't Build After Installed Sitecore TDS

After setting up Sitecore TDS, my project will not build. I'm new to Visual Studio and also new to working with Sitecore. It seems that it cannot find a particular setting, but a Google search is not coming up with anything:
Severity Code Description Project Path File Line Suppression State
Error The "AnalyzeProject" task failed unexpectedly.
System.MissingFieldException: Field not found: 'HedgehogDevelopment.SitecoreProject.Tasks.SitecoreDeployInfo.ParsedItem'.
at HedgehogDevelopment.SitecoreProject.Analysis.TemplateStructure.Validate(Dictionary`2 projectItems, XDocument scprojDocument)
at HedgehogDevelopment.SitecoreProject.Tasks.ProjectAnalysis.AnalysisEngine.<>c__DisplayClass4_1.<GetReport>b__0()
at HedgehogDevelopment.SitecoreProject.Tasks.ProjectAnalysis.ExecutionTimer.Time(Action action)
at HedgehogDevelopment.SitecoreProject.Tasks.ProjectAnalysis.AnalysisEngine.GetReport(Dictionary`2 projectItems, XDocument scprojDocument)
at HedgehogDevelopment.SitecoreProject.Tasks.AnalyzeProject.Execute()
at Microsoft.Build.BackEnd.TaskExecutionHost.Microsoft.Build.BackEnd.ITaskExecutionHost.Execute()
at Microsoft.Build.BackEnd.TaskBuilder.<ExecuteInstantiatedTask>d__26.MoveNext() B2B.Core C:\Program Files (x86)\MSBuild\HedgehogDevelopment\SitecoreProject\v9.0 C:\Program Files (x86)\MSBuild\HedgehogDevelopment\SitecoreProject\v9.0\HedgehogDevelopment.SitecoreProject.targets 144
Apparently my project does still build and will run, but that error pops up each time regardless.
This can happen when you have TDS validations enabled and are missing some DLLs.
In this directory:
C:\Program Files (x86)\MSBuild\HedgehogDevelopment\SitecoreProject\v9.0\
Add the following DLLs:
Microsoft.Web.Infrastructure.dll
TDSWebDeploy.Services.Contracts.dll
If you have TDS installed, you may be able to source those DLLs from somewhere in your C:\Program Files (x86)\MSBuild\HedgehogDevelopment directory. If not, someone else on your team may have them.
You can also try disabling and enabling validations by right clicking your TDS project --> Properties --> Validations tab.
I have seen this issue on numerous occasions across numerous dev boxes. We have opened support tickets regarding it, but no conclusions were drawn.
The specific error is that the HedgehogDevelopment.SitecoreProject.Tasks.SitecoreDeployInfo.ParsedItem field is missing on an object. The fact that ParsedItem is mentioned implies that some form of parsing may be occurring, and that it didn't work as expected. This is conjecture, but if parsing is occurring, it would be worth ensuring that your serialized item files are accessible to the user/group that is performing this parsing.
Here is the code that is failing:
public override IEnumerable<Problem> Validate(Dictionary<Guid, SitecoreDeployInfo> projectItems, XDocument scprojDocument)
{
List<Problem> problems = new List<Problem>();
foreach (KeyValuePair<Guid, SitecoreDeployInfo> projectItem in projectItems)
{
// ** Presumably, it's failing here **
string item = projectItem.Value.ParsedItem.Properties["template"];
if (string.IsNullOrEmpty(item))
{
continue;
}
Guid guid = new Guid(item);
if (guid == TemplateStructure.TEMPLATE)
{
problems.AddRange(this.ValidateTemplate(projectItems, projectItem.Value.Item));
}
else if (guid != TemplateStructure.TEMPLATE_SECTION)
{
if (guid != TemplateStructure.TEMPLATE_FIELD)
{
continue;
}
problems.AddRange(this.ValidateField(projectItems, projectItem.Value.Item));
}
else
{
problems.AddRange(this.ValidateSection(projectItems, projectItem.Value.Item));
}
}
problems.RemoveAll((Problem r) => r == null);
return problems;
}
Here is the definition for SitecoreDeployInfo:
using HedgehogDevelopment.SitecoreCommon.Data.Items;
using System;
namespace HedgehogDevelopment.SitecoreProject.Tasks
{
public class SitecoreDeployInfo
{
public IItem ParsedItem;
public SitecoreItem Item;
public SitecoreDeployInfo()
{
}
}
}

Wix, setting a custom property to true does not work

Wix fragment: I am setting a property FEATURE_IS_SELECTED
<SetProperty Id="FEATURE_IS_SELECTED" Value="1" After="InstallFiles" Sequence="execute"><![CDATA[&MyFeature=3]]></SetProperty>
then calling a Custom Action:
<Custom Action="ConfigureMyXml" Before="InstallFinalize">NOT Installed OR MaintenanceMode="Modify"</Custom>
Custom action:
public const string IsFeatureSelected = "FEATURE_IS_SELECTED";
[CustomAction]
public static ActionResult ConfigureMyXml(Session session)
{
string value;
MessageBox.Show("I will check if value is set");
if (session.CustomActionData.TryGetValue(IsFeatureSelected, out value))
{
//do sth here
}
return ActionResult.Success;
}
When debugging this, the action is called, but the if condition is not true. Why the FEATURE_IS_SELECTED is not set ? and //do sth here does not execute?
Your configure custom action is not marked with an "execute" value, so the default value is immediate. That means CustomActionData isn't involved in getting the value. However, it looks like you want to alter an installed file, so the only change you need is to mark the CA as execute deferred so it runs after the files are actually installed, and then you will also need a separate custom action to prepare CustomActionData, as here:
How to pass CustomActionData to a CustomAction using WiX?
Your sequencing may be a little suspect - you can set FEATURE_IS_SELECTED anytime after CostFinalize and it can be immediate. Configuring your Xml might be better "after install files".

How to get local path for payload in WiX/Burn Managed Bootstrapper Application?

I am currently working in a WiX/Burn Managed Bootstrapper Application and cannot figure out how to get the local path for a payload (MSI).
I let the user select which applications they want to install in my custom UI, and I want to not show applications for which the MSI is missing. I also need to see information in the MSI's database.
I know I can determine missing payloads by handling "ResolveSource" but that doesn't happen until right before the application in installed.
I deserialize the BootstrapperApplicationData.xml file first thing so I have information about which MSIs MIGHT be installed, but it still doesn't help me determine the source of the MSIs.
Does anyone know how to determine the local path to a payload?
EDIT: Here is an example for how I reference all the installers:
<MsiPackage Id="AppName"
SourceFile="$(var.ProjectName.TargetDir)ProjectName.msi"
Name="MSI\ProjectName.msi"
Compressed="no"/>
In the GetLastUsedSourceFolder function in cache.cpp, you can see that the engine gets the source folder from the WixBundleLastUsedSource variable, and the parent directory of the WixBundleOriginalSource variable if WixBundleLastUsedSource isn't set.
You can use this along with the Name attribute of the WixPayloadProperties element in the BootstrapperApplicationData.xml file to predetermine where the engine will look for a payload. Note that the engine will actually look in the cache first.
The MSI files are embedded into the bundle .exe and aren't extracted from the bundle until right before the application is installed, which corresponds to when the ResolveSource event fires. However, if you really want to get this information, you can programatically extract the MSI files yourself and inspect them using the WiX DTF library (wix.dll in the /bin folder of your WiX install).
using Microsoft.Tools.WindowsInstallerXml;
private void ExtractEmbeddedMsiInstallers()
{
var tmpFolder = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
var bundlePath = Engine.StringVariables["WixBundleOriginalSource"];
Unbinder unbinder = null;
try
{
unbinder = new Unbinder();
//The next line will extract the MSIs into the tmpFolder in a subfolder named "AttachedContainer"
unbinder.Unbind(bundlePath, OutputType.Bundle, tmpFolder);
}
finally
{
if (null != unbinder)
unbinder.DeleteTempFiles();
}
}
You also mentioned needing to inspect data in the MSI database. Here's a sample of how to do that:
using (var database = new InstallPackage(msiFilePath, DatabaseOpenMode.Transact) { WorkingDirectory = _someTempFolder })
{
if (database.Tables.Contains("CustomAction"))
{
using (View view = database.OpenView("SELECT `Action`, `Type`, `Source`, `Target` FROM `CustomAction`"))
{
view.Execute();
foreach (Record rowRecord in view)
using (rowRecord)
{
var actionName = rowRecord.GetString(1);
var actionType = rowRecord.GetInteger(2);
var binaryName = rowRecord.GetString(3);
var methodName = rowRecord.GetString(4);
//Do something with the values
}
}
}
}

Is it possible to read MSI properties from MSI files in chain before detect phase in bootstrapper?

In "Bundle.wxs" I have a "chain" with "MsiPackages" which contain "InstallConditions". In order for the user to decide which packages he/she wants installed/upgraded I would like to display properties found in them.
For instance, I want to read the property "ProductName" and "ProductVersion" in the "Property" table of every MSI in the chain and display it to the user next to a checkbox for every MSI in the chain. The checkbox is wired to the burn variable used in "InstallConditions".
But the problem is, it doesn't seem like I have access to the MSI files before the "Apply" step. They are not extracted from the Bootstrapper Application EXE before this step. So, my question is, Is there a way to load these values programatically in order to display them to the user before the Apply step? I could use variables and populate them myself with the values but this information is already in the MSI so this seems inefficient.
Is there a way to do this? Thanks for the help.
<Bundle>
<Variable Name="InstallProduct1" Type="string" Value="true" />
<Variable Name="ProductName1" Type="string" Value="My Product 1"/> <!-- Better way? -->
<Variable Name="ProductVersion1" Type="version" Value="1.2.3.4"/> <!-- Better way? -->
<Chain>
<MsiPackage SourceFile="my_product_1.msi"
InstallCondition="InstallProduct1">
</MsiPackage>
</Chain>
</Bundle>
WiX does generate a BootstrapperApplicationData.xml file which includes a lot of the information used to build the exe and is included in the files available at runtime. You can parse that file at runtime in order to access that metadata. Since the file, along with all of our assemblies and .msi files, are placed in a randomly-name temp folder, we can’t know ahead of time where the file will live, so we must use our assembly’s path to find it. You can then parse the XML to get the metadata.
I have a blog post with additional details here: https://www.wrightfully.com/part-3-of-writing-your-own-net-based-installer-with-wix-context-data/
In my case, I use the tag instead of '', so have some of the info available to me may not be there for you, so your experience may vary. I would suggest running a makeshift installer in debug mode and setting a breakpoint to inspect the contents of the XML in order to get a full list of what’s available.
Here’s an example of how I get data from the file in my ManagedBootstrapperApplication (c#). Note: in this example, my domain objects are MBAPrereqPackage, BundlePackage and PackageFeature, each of which take an XML node object in their constructor and further parse the data into the object’s properties.
const XNamespace ManifestNamespace = ( XNamespace) “http://schemas.microsoft.com/wix/2010/BootstrapperApplicationData” ;
public void Initialize()
{
//
// parse the ApplicationData to find included packages and features
//
var bundleManifestData = this.ApplicationData;
var bundleDisplayName = bundleManifestData
.Element(ManifestNamespace + “WixBundleProperties“ )
.Attribute( “DisplayName“)
.Value;
var mbaPrereqs = bundleManifestData.Descendants(ManifestNamespace + “WixMbaPrereqInformation“)
.Select(x => new MBAPrereqPackage(x))
.ToList();
//
//exclude the MBA prereq packages, such as the .Net 4 installer
//
var pkgs = bundleManifestData.Descendants(ManifestNamespace + “WixPackageProperties“)
.Select(x => new BundlePackage(x))
.Where(pkg => !mbaPrereqs.Any(preReq => preReq.PackageId == pkg.Id));
//
// Add the packages to a collection of BundlePackages
//
BundlePackages.AddRange(pkgs);
//
// check for features and associate them with their parent packages
//
var featureNodes = bundleManifestData.Descendants(ManifestNamespace + “WixPackageFeatureInfo“);
foreach ( var featureNode in featureNodes)
{
var feature = new PackageFeature(featureNode);
var parentPkg = BundlePackages.First(pkg => pkg.Id == feature.PackageId);
parentPkg.AllFeatures.Add(feature);
feature.Package = parentPkg;
}
}
///
/// Fetch BootstrapperApplicationData.xml and parse into XDocument.
///
public XElement ApplicationData
{
get
{
var workingFolder = Path.GetDirectoryName(this.GetType().Assembly.Location);
var bootstrapperDataFilePath = Path.Combine(workingFolder, “BootstrapperApplicationData.xml”);
using (var reader = new StreamReader(bootstrapperDataFilePath))
{
var xml = reader.ReadToEnd();
var xDoc = XDocument.Parse(xml);
return xDoc.Element(ManifestNamespace + “BootstrapperApplicationData“);
}
}
}