Getting/Setting INSTALLDIR from WPF external UI

Jul 8, 2015 at 4:11 PM
Is there an equivalent property to GetProductVersion or GetProductName for the INSTALLDIR so that this can be retrieved and set from a WPF external UI?

If not, what options are there for getting the MSI's INSTALLDIR so that it can be displayed and configured from a WPF external UI context?

It seems it should be possible to get and set this from the GenericSetup object, but nothing of the sort is available.
Jul 9, 2015 at 1:00 AM
I'll answer my own question, though this is not a satisfying answer.

As part of this class:

public class ProductSetup : GenericSetup
{
// ... add the following
    string installDirectory;

    public string InstallDirectory
    {
        get { return installDirectory; }
        set
        {
            installDirectory = value;
            OnPropertyChanged("InstallDirectory");
        }
    }
// ... modify this as shown
    public void StartInstall()
    {
        string param = string.Format("CUSTOM_UI=true INSTALLDIR=\"{0}\"", InstallDirectory);

        base.StartInstall(param);
    }
Then modify MainWindow.xaml and add a binding to some input mechanism for InstallDirectory. Set InstallDirectory to something like:

InsatallDirectory = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + "xxxProductDir"

during initialization.

What I was hoping for was a way to extract the "INSTALLDIR" details that are embedded in the MSI when it is built. Having the ability to extract this information would be more correct since everything is being driven by the setup project rather than duplication of some details in another layer. I haven't dug deeply enough to find out what would be involved in supporting the ability to extract the INSTALLDIR value, but it looks like this is doable.
Coordinator
Jul 9, 2015 at 11:06 AM
Calculating target OS installdir is not fully trivial. The only way to do this is to build the chain of the linked directories from the 'Directory' table for the INSTALLDIR. There are the challenges there: recursion, MSI vs. .NET special folders mapping and interpretation of x64 specifics from x86 MSI runtime on the target system.

I had to solve this problem for the ManagedUI standard dialogs (part of Managed Setup effort). While this feature is already available from Git it is yet to be released publicly.

This is how it's implemented:
//usage
string installDirPath = session.GetDirectoryPath("INSTALLDIR");
...

//actual extensions
public static string GetDirectoryPath(this Session session, string name)
{
    string[] subDirs = session.GetDirectoryPathParts(name)
                              .Select(x => x.AsWixVarToPath())
                              .ToArray();
    return string.Join(@"\", subDirs);
}

static string[] GetDirectoryPathParts(this Session session, string name)
{
    var path = new List<string>();
    var names = new Queue<string>(new[] { name });

    while (names.Any())
    {
        var item = names.Dequeue();

        using (var sql = session.Database.OpenView("select * from Directory where Directory = '" + item + "'"))
        {
            sql.Execute();
            using (var record = sql.Fetch())
            {
                var subDir = record.GetString("DefaultDir").Split('|').Last();
                path.Add(subDir);

                if (!record.IsNull("Directory_Parent"))
                {
                    var parent = record.GetString("Directory_Parent");
                    if (parent != "TARGETDIR")
                        names.Enqueue(parent);
                }
            }
        }
    }
    path.Reverse();
    return path.ToArray();
}

//will always be called from x86 runtime as MSI always loads ManagedUI in x86 host.
//Though CustomActions are called in the deployment specific CPU type context.
public static string AsWixVarToPath(this string path)
{
    switch (path)
    {
        case "AdminToolsFolder": return io.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), @"Start Menu\Programs\Administrative Tools");

        case "AppDataFolder": return Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
        case "CommonAppDataFolder": return Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);

        case "CommonFiles64Folder": return Environment.GetFolderPath(Environment.SpecialFolder.CommonProgramFiles).Replace(" (x86)", "");
        case "CommonFilesFolder": return Environment.GetFolderPath(Environment.SpecialFolder.CommonProgramFiles);

        case "DesktopFolder": return Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
        case "FavoritesFolder": return Environment.GetFolderPath(Environment.SpecialFolder.Favorites);

        case "ProgramFiles64Folder": return Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles).Replace(" (x86)", "");
        case "ProgramFilesFolder": return Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);

        case "MyPicturesFolder": return Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
        case "SendToFolder": return Environment.GetFolderPath(Environment.SpecialFolder.SendTo);
        case "LocalAppDataFolder": return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
        case "PersonalFolder": return Environment.GetFolderPath(Environment.SpecialFolder.Personal);

        case "StartMenuFolder": return Environment.GetFolderPath(Environment.SpecialFolder.StartMenu);
        case "StartupFolder": return Environment.GetFolderPath(Environment.SpecialFolder.Startup);
        case "ProgramMenuFolder": return Environment.GetFolderPath(Environment.SpecialFolder.Programs);

        case "System16Folder": return io.Path.Combine("WindowsFolder".AsWixVarToPath(), "System");
        case "System64Folder": return Environment.GetFolderPath(Environment.SpecialFolder.System);
        case "SystemFolder": return Is64OS() ? io.Path.Combine("WindowsFolder".AsWixVarToPath(), "SysWow64") : Environment.GetFolderPath(Environment.SpecialFolder.System);

        case "TemplateFolder": return Environment.GetFolderPath(Environment.SpecialFolder.Templates);
        case "WindowsVolume": return io.Path.GetPathRoot(Environment.GetFolderPath(Environment.SpecialFolder.Programs));
        case "WindowsFolder": return io.Path.GetDirectoryName(Environment.GetFolderPath(Environment.SpecialFolder.System));
        case "FontsFolder": return io.Path.Combine(io.Path.GetDirectoryName(Environment.GetFolderPath(Environment.SpecialFolder.System)), "Fonts");
        case "TempFolder": return io.Path.Combine(io.Path.GetDirectoryName(Environment.GetFolderPath(Environment.SpecialFolder.Desktop)), @"Local Settings\Temp");
        default:
            return path;
    }
}
You can implement your own equivalent or get the latest dlls from Git.
Jul 9, 2015 at 2:03 PM
Nice solution! It would be very useful to have this accessible through the GenericSetup object as a property or set of properties.

Thanks.
Jul 9, 2015 at 3:48 PM
In public class MsiParser ... having something like this is pretty close to what I was looking for:
    static bool Is64OS()
    {
        //cannot use Environment.Is64BitOperatingSystem class as it is v3.5
        string progFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
        string progFiles32 = progFiles;
        if (!progFiles32.EndsWith(" (x86)"))
            progFiles32 += " (x86)";

        return Directory.Exists(progFiles32);
    }

    //will always be called from x86 runtime as MSI always loads ManagedUI in x86 host.
    //Though CustomActions are called in the deployment specific CPU type context.
    static string AsWixVarToPath(string path)
    {
        switch (path)
        {
            case "AdminToolsFolder": return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), @"Start Menu\Programs\Administrative Tools");

            case "AppDataFolder": return Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
            case "CommonAppDataFolder": return Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);

            case "CommonFiles64Folder": return Environment.GetFolderPath(Environment.SpecialFolder.CommonProgramFiles).Replace(" (x86)", "");
            case "CommonFilesFolder": return Environment.GetFolderPath(Environment.SpecialFolder.CommonProgramFiles);

            case "DesktopFolder": return Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
            case "FavoritesFolder": return Environment.GetFolderPath(Environment.SpecialFolder.Favorites);

            case "ProgramFiles64Folder": return Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles).Replace(" (x86)", "");
            case "ProgramFilesFolder": return Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);

            case "MyPicturesFolder": return Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
            case "SendToFolder": return Environment.GetFolderPath(Environment.SpecialFolder.SendTo);
            case "LocalAppDataFolder": return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
            case "PersonalFolder": return Environment.GetFolderPath(Environment.SpecialFolder.Personal);

            case "StartMenuFolder": return Environment.GetFolderPath(Environment.SpecialFolder.StartMenu);
            case "StartupFolder": return Environment.GetFolderPath(Environment.SpecialFolder.Startup);
            case "ProgramMenuFolder": return Environment.GetFolderPath(Environment.SpecialFolder.Programs);

            case "System16Folder": return Path.Combine(AsWixVarToPath("WindowsFolder"), "System");
            case "System64Folder": return Environment.GetFolderPath(Environment.SpecialFolder.System);
            case "SystemFolder": return Is64OS() ? Path.Combine(AsWixVarToPath("WindowsFolder"), "SysWow64") : Environment.GetFolderPath(Environment.SpecialFolder.System);

            case "TemplateFolder": return Environment.GetFolderPath(Environment.SpecialFolder.Templates);
            case "WindowsVolume": return Path.GetPathRoot(Environment.GetFolderPath(Environment.SpecialFolder.Programs));
            case "WindowsFolder": return Path.GetDirectoryName(Environment.GetFolderPath(Environment.SpecialFolder.System));
            case "FontsFolder": return Path.Combine(Path.GetDirectoryName(Environment.GetFolderPath(Environment.SpecialFolder.System)), "Fonts");
            case "TempFolder": return Path.Combine(Path.GetDirectoryName(Environment.GetFolderPath(Environment.SpecialFolder.Desktop)), @"Local Settings\Temp");
            default:
                return path;
        }
    }

    /// <summary>
    /// Extracts the INSTALLDIR value from the encapsulated MSI database.
    /// <para>
    /// <remarks>The DB view is not closed after the call</remarks>
    /// </para>
    /// </summary>
    /// <returns>Product code.</returns>
    public string GetINSTALLDIR()
    {
        string installDirElements = this.db.View("SELECT * FROM Directory WHERE Directory = 'INSTALLDIR'").NextRecord().GetString(2);

        string[] paths = installDirElements.Split('.');

        string completePath = string.Empty;

        for (int i = 0; i < paths.Length; i++ )
        {
            completePath += (i == 0 ? AsWixVarToPath(paths[i]) : paths[i]) + (i == paths.Length - 1 ? string.Empty : @"\");
        }

        return completePath;
    }

What I'm not sure about is the additional checks and qualifiers that need to be in place to make this code completely safe. I also think it would be sufficient to just return:

AsWixVarToPath(paths[0])

Instead of looping through all the other bits.


What are your thoughts Oleg?

Thank you for your help so far. Fantastic tool by the way.
Coordinator
Jul 10, 2015 at 1:24 PM
I have mixed feelings about your proposal :)

1 - Your proposed code.
Unfortunately it's not safe. It relies on splitting the id of the INSTALLDIR parent directory, Thus it will not work if user defines explicit Id(s) or auto-id(s) are generated from the directories containing spaces (e.g. 'MyCompany/MyProduct' scenario). That is exactly why looping is needed. Another problem is that you don't even have the warranty that the installdir Id will be 'INSTALLDIR'. The Session extensions take care about that and all is handled automatically:
string installDirProperty = session.GetInstallDirectoryName(); 
string installDirPath = session.GetDirectoryPath(installDirProperty);
2 - Placing it into MsiParser...
MsiParser is designed to be used from the external UI, which is normally not a generic UI (like MSI standard dilogs) but rather a product specific custom GUI. Thus you have very good idea what the install dir is and no discovery mechanizm is needed. However if you are indeed looking for a generic UI then you are better of with the EmbeddedUI not ExternalUI. The WinForm and WPF samples are included in the distro. And in the EmbeddedUI you have the full access to the Session so you can just use the code from the above paragraph.

3 - The compromise.
Of course I can see that extracting INSTALLDIR path from msi file may still have some benefits. Thus I have moved AsWixVarToPath into WixSharp assembly so MsiParser can access it. But implementing the GetDirectoryPathParts equivalent in MsiParser is too painful - it is basically naked Interop. Thus I will leave it fro you :) If you indeed do this you may share your code with me and I will include it into the codebase.

But I think it is just so simpler to use EmbeddedUI instead. Particularly because the EmbeddedUI is about to get a significant boost. The next release will include all major MSI standard dialogs re-implemented as managed (WinForm) UI.
Jul 10, 2015 at 1:54 PM
I appreciate your reservations. I think something like the following is a reasonable compromise:

--- MsiParser.cs ---

using System;
using System.Text;
using System.Text.RegularExpressions;
using WindowsInstaller;
using io = System.IO;

namespace WixSharp.UI
{
internal static class UIExtensions
{
    static bool Is64OS()
    {
        //cannot use Environment.Is64BitOperatingSystem class as it is v3.5
        string progFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
        string progFiles32 = progFiles;
        if (!progFiles32.EndsWith(" (x86)"))
            progFiles32 += " (x86)";

        return io.Directory.Exists(progFiles32);
    }


    //will always be called from x86 runtime as MSI always loads ManagedUI in x86 host.
    //Though CustomActions are called in the deployment specific CPU type context.
    public static string AsWixVarToPath(this string path)
    {
        switch (path)
        {
            case "AdminToolsFolder": return io.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), @"Start Menu\Programs\Administrative Tools");

            case "AppDataFolder": return Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
            case "CommonAppDataFolder": return Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);

            case "CommonFiles64Folder": return Environment.GetFolderPath(Environment.SpecialFolder.CommonProgramFiles).Replace(" (x86)", "");
            case "CommonFilesFolder": return Environment.GetFolderPath(Environment.SpecialFolder.CommonProgramFiles);

            case "DesktopFolder": return Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
            case "FavoritesFolder": return Environment.GetFolderPath(Environment.SpecialFolder.Favorites);

            case "ProgramFiles64Folder": return Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles).Replace(" (x86)", "");
            case "ProgramFilesFolder": return Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);

            case "MyPicturesFolder": return Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
            case "SendToFolder": return Environment.GetFolderPath(Environment.SpecialFolder.SendTo);
            case "LocalAppDataFolder": return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
            case "PersonalFolder": return Environment.GetFolderPath(Environment.SpecialFolder.Personal);

            case "StartMenuFolder": return Environment.GetFolderPath(Environment.SpecialFolder.StartMenu);
            case "StartupFolder": return Environment.GetFolderPath(Environment.SpecialFolder.Startup);
            case "ProgramMenuFolder": return Environment.GetFolderPath(Environment.SpecialFolder.Programs);

            case "System16Folder": return io.Path.Combine("WindowsFolder".AsWixVarToPath(), "System");
            case "System64Folder": return Environment.GetFolderPath(Environment.SpecialFolder.System);
            case "SystemFolder": return Is64OS() ? io.Path.Combine("WindowsFolder".AsWixVarToPath(), "SysWow64") : Environment.GetFolderPath(Environment.SpecialFolder.System);

            case "TemplateFolder": return Environment.GetFolderPath(Environment.SpecialFolder.Templates);
            case "WindowsVolume": return io.Path.GetPathRoot(Environment.GetFolderPath(Environment.SpecialFolder.Programs));
            case "WindowsFolder": return io.Path.GetDirectoryName(Environment.GetFolderPath(Environment.SpecialFolder.System));
            case "FontsFolder": return io.Path.Combine(io.Path.GetDirectoryName(Environment.GetFolderPath(Environment.SpecialFolder.System)), "Fonts");
            case "TempFolder": return io.Path.Combine(io.Path.GetDirectoryName(Environment.GetFolderPath(Environment.SpecialFolder.Desktop)), @"Local Settings\Temp");
            default:
                return path;
        }
    }
}

/// <summary>
/// Utility class for simplifying MSI interpreting tasks DB querying, message data parsing  
/// </summary>
public class MsiParser
{
    string msiFile;
    IntPtr db;

    /// <summary>
    /// Opens the specified MSI file and returns the database handle.
    /// </summary>
    /// <param name="msiFile">The msi file.</param>
    /// <returns>Handle to the MSI database.</returns>
    public static IntPtr Open(string msiFile)
    {
        IntPtr db = IntPtr.Zero;
        MsiExtensions.Invoke(() => MsiInterop.MsiOpenDatabase(msiFile, MsiDbPersistMode.ReadOnly, out db));
        return db;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="MsiParser" /> class.
    /// </summary>
    /// <param name="msiFile">The msi file.</param>
    public MsiParser(string msiFile)
    {
        this.msiFile = msiFile;
        this.db = MsiParser.Open(msiFile);
    }

    /// <summary>
    /// Queries the name of the product from the encapsulated MSI database.
    /// <para>
    /// <remarks>The DB view is not closed after the call</remarks>
    /// </para>
    /// </summary>
    /// <returns>Product name.</returns>
    public string GetProductName()
    {
        return this.db.View("SELECT `Value` FROM `Property` WHERE `Property` = 'ProductName'")
                      .NextRecord()
                      .GetString(1);
    }
    /// <summary>
    /// Queries the version of the product from the encapsulated MSI database.
    /// <para>
    /// <remarks>The DB view is not closed after the call</remarks>
    /// </para>
    /// </summary>
    /// <returns>Product version.</returns>
    public string GetProductVersion()
    {
        return this.db.View("SELECT `Value` FROM `Property` WHERE `Property` = 'ProductVersion'")
                      .NextRecord()
                      .GetString(1);
    }
    /// <summary>
    /// Queries the code of the product from the encapsulated MSI database.
    /// <para>
    /// <remarks>The DB view is not closed after the call</remarks>
    /// </para>
    /// </summary>
    /// <returns>Product code.</returns>
    public string GetProductCode()
    {
        return this.db.View("SELECT `Value` FROM `Property` WHERE `Property` = 'ProductCode'")
                      .NextRecord()
                      .GetString(1);
    }

    /// <summary>
    /// Determines whether the specified product code is installed.
    /// </summary>
    /// <param name="productCode">The product code.</param>
    /// <returns>Returns <c>true</c> if the product is installed. Otherwise returns <c>false</c>.</returns>
    public static bool IsInstalled(string productCode)
    {
        StringBuilder sb = new StringBuilder(2048);
        uint size = 2048;
        MsiError err = MsiInterop.MsiGetProductInfo(productCode, MsiInstallerProperty.InstallDate, sb, ref size);

        if (err == MsiError.UnknownProduct)
            return false;
        else if (err == MsiError.NoError)
            return true;
        else
            throw new Exception(err.ToString());
    }

    /// <summary>
    /// Determines whether the product from the encapsulated msi file is installed.
    /// </summary>
    /// <returns>Returns <c>true</c> if the product is installed. Otherwise returns <c>false</c>.</returns>
    public bool IsInstalled()
    {
        return IsInstalled(this.GetProductCode());
    }

    /// <summary>
    /// Parses the <c>MsiInstallMessage.CommonData</c> data.
    /// </summary>
    /// <param name="s">Message data.</param>
    /// <returns>Collection of parsed tokens (fields).</returns>
    public static string[] ParseCommonData(string s)
    {
        //Example: 1: 0 2: 1033 3: 1252 
        var res = new string[3];
        var regex = new Regex(@"\d:\s?\w+\s");

        int i = 0;

        foreach (Match m in regex.Matches(s))
        {
            if (i > 3) return null;

            res[i++] = m.Value.Substring(m.Value.IndexOf(":") + 1).Trim();
        }

        return res;
    }

    /// <summary>
    /// Parses the <c>MsiInstallMessage.Progress</c> string.
    /// </summary>
    /// <param name="s">Message data.</param>
    /// <returns>Collection of parsed tokens (fields).</returns>
    public static string[] ParseProgressString(string s)
    {
        //1: 0 2: 86 3: 0 4: 1 
        var res = new string[4];
        var regex = new Regex(@"\d:\s\d+\s");

        int i = 0;

        foreach (Match m in regex.Matches(s))
        {
            if (i > 4) return null;

            res[i++] = m.Value.Substring(m.Value.IndexOf(":") + 2).Trim();
        }

        return res;
    }

    /// <summary>
    /// Extracts the INSTALLDIR value from the encapsulated MSI database.
    /// <para>
    /// <remarks>The DB view is not closed after the call</remarks>
    /// </para>
    /// </summary>
    /// <returns>Root component of INSTALLDIR string.</returns>
    public string GetInstallDirectoryRoot()
    {
        string queryPath = string.Empty;

        var qr = this.db.View("SELECT * FROM Directory WHERE Directory = 'INSTALLDIR'").NextRecord();

        // Should be 3 if msi has expected content.
        if ((int)qr == 3)
        {
            string installDirElements = qr.GetString(2);

            string[] paths = installDirElements.Split('.');

            //for (int i = 0; i < paths.Length; i++)
            //{
            //  queryPath += (i == 0 ? paths[i].AsWixVarToPath() : paths[i]) + (i == paths.Length - 1 ? string.Empty : @"\");
            //}

            return paths[0].AsWixVarToPath();
        }
        else
            queryPath = "ProgramFilesFolder".AsWixVarToPath(); // Always default to Program Files folder.

        return queryPath;
    }

}
}

What are your thoughts?
Jul 10, 2015 at 2:10 PM
Edited Jul 10, 2015 at 2:11 PM
[ This comment is adding to the above remarks due to space constraints ]

The advantage of being able to pull at least the INSTALLDIR root element from the msi is that you can have an external UI that requires no changes whatsoever for multiple install scenarios. That's precisely what I'm doing, I have a shell WPF application, and into it I inject the MSI resource. For at least most of the simpler installers that I need, it alleviates having to maintain a separate WPF projects.

Everything that's needed by the GUI is pulled from the MSI, thus you can have a universal external UI project using resource injection. I use a build task to perform the injection step. I no longer have to maintain a separate GUI project thanks to your fantastic framework!


--- Generic.cs ---

...
    /// <summary>
    /// The root folder associated with INSTALLDIR.
    /// </summary>
    public string InstallDirectoryRoot;
...
    void UpdateStatus()
    {
        var msi = new MsiParser(MsiFile);

        InstallDirectoryRoot = msi.GetInstallDirectoryRoot();
        IsCurrentlyInstalled = msi.IsInstalled();
        ProductName = msi.GetProductName();
        ProductVersion = msi.GetProductVersion();

        ProductStatus = string.Format("The product is {0}INSTALLED\n\n", IsCurrentlyInstalled ? "" : "NOT ");
    }
...
Coordinator
Jul 11, 2015 at 1:56 AM
If you are interested in the root part of the installdir then indeed you can do this safely (and still without much effort). Though not the way you done it. :)
Your approach will fail if user uses an alternative (notINSTALLDIR) directory id.

I have updated the MsiParser code towith the slighlty modified version of your code:
/// <summary>
/// Extracts the root components of the top-level install directory from the encapsulated MSI database.
/// Typically it is a first child of the 'TARGETDIR' MSI directory.
/// <para><remarks>The DB view is not closed after the call</remarks></para>
/// </summary>
/// <returns>
/// Root component of install directory. If the 'TARGETDIR' cannot be located then the return value is the 
/// expanded value of 'ProgramFilesFolder' WiX constant.
/// </returns>
public string GetInstallDirectoryRoot()
{
    var qr = this.db.View("SELECT * FROM Directory WHERE Directory_Parent = 'TARGETDIR'").NextRecord();

    // Should be 3 if msi has expected content.
    if ((int)qr == 3)
    {
        string rootDirId = qr.GetString(1);
        return rootDirId.AsWixVarToPath();
    }
    else
        return "ProgramFilesFolder".AsWixVarToPath(); // Always default to Program Files folder.
}
The code and dlls are available from Git.
Jul 13, 2015 at 2:39 PM
Edited Jul 13, 2015 at 3:02 PM
With the latest build, the qr value in the above code is > 3, so I have modified the if statement to be:
        if ((int)qr > 1)
        {
            string rootDirId = qr.GetString(1);
            return rootDirId.AsWixVarToPath();
        }
        else
            return "ProgramFilesFolder".AsWixVarToPath(); // Always default to Program Files folder.
I hope this is valid (any idea why this has changed)? Also, could you please add the following code to MsiParser and Generic.cs

--- MsiParser.cs --- please add:
    /// <summary>
    /// Queries the Manufacturer name from the encapsulated MSI database.
    /// <para>
    /// <remarks>The DB view is not closed after the call</remarks>
    /// </para>
    /// </summary>
    /// <returns>Manufacturer name.</returns>
    public string GetManufacturer()
    {
        return this.db.View("SELECT `Value` FROM `Property` WHERE `Property` = 'Manufacturer'")
                      .NextRecord()
                      .GetString(1);
    }
--- Generic.cs --- please add:
    // ... added InstallDirectoryRoot = msi.GetInstallDirectoryRoot(); and Manufacturer = msi.GetManufacturer();
    void UpdateStatus()
    {
        var msi = new MsiParser(MsiFile);

        IsCurrentlyInstalled = msi.IsInstalled();
        ProductName = msi.GetProductName();
        ProductVersion = msi.GetProductVersion();
        Manufacturer = msi.GetManufacturer();
    InstallDirectoryRoot = msi.GetInstallDirectoryRoot();

        ProductStatus = string.Format("The product is {0}INSTALLED\n\n", IsCurrentlyInstalled ? "" : "NOT ");
    }

   // ... added InstallDirectoryRoot and Manufacturer  property

    string manufacturer;

    /// <summary>
    /// Gets or sets the MSI Manufacturer name.
    /// </summary>
    /// <value>
    /// The name of the manufacturer.
    /// </value>
    public string Manufacturer
    {
        get { return manufacturer; }
        set
        {
            manufacturer = value;
            OnPropertyChanged("Manufacturer");
        }
    }

    string installDirectoryRoot;

    /// <summary>
    /// Gets the root components of the top-level install directory from the encapsulated MSI database.
    /// </summary>
    /// <value>
    /// Install directory root components.
    /// </value>
    public string InstallDirectoryRoot
    {
        get { return installDirectoryRoot; }
        set
        {
            installDirectoryRoot = value;
            OnPropertyChanged("InstallDirectoryRoot");
        }
    }

Thank you.
Jul 13, 2015 at 3:56 PM
Oleg, after experimenting with using your TARGETDIR approach, I have found that if you set a shortcut within the installer, TARGETDIR is set to the path of the shortcut, not the target directory for installing the application. Overall, in the testing I have done, I have found the code I originally submitted to be more reliable:
        // Should be > 1 if msi has expected content.
        if ((int)qr > 1)
        {
            string installDirElements = qr.GetString(2);

            string[] paths = installDirElements.Split('.');

            return paths[0].AsWixVarToPath();
        }
        else
            return "ProgramFilesFolder".AsWixVarToPath(); // Always default to Program Files folder.
The TARGETDIR approach is more prone to produce an invalid result.

Also, you have introduced a needless dependency on WixSharp.dll in the latest build of WixSharp.Msi.dll. Please use a file link to the needed extension methods rather than an assembly link.
Coordinator
Jul 15, 2015 at 6:50 AM
Thank you. Your feedback halped me to identify the deficiencies in the ManagedUI feature I am currently working on.

Your latest post convinced me that the both proposed approaches (yours and mine) are not acceptable as they don't yield any reliable result,

The problem is that the detection of the installdir is not deterministic. For example 'Shortcuts' sample has three logical installdirs INSTALLDIR, DesktopFolder and ProgramMenuFolder. The INSTALLDIR is the real one that we need to discover but there is no way to understand its role by analyzing the MSI tables. And the other problem is that we cannot rely on its name as users can overwrite it.

WIX solves this problem in a straight forward way by requiring the user explicitly link the installdir ID to the WIXUI_INSTALLDIR
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR"  />
All this means that you cannot put 'InstallDirectoryRoot' into GenericSetup.cs as there is no generic way to find this directory unless the developer specifies what installdir (there can be many) to use when constructing the path value.

BTW it also means that your approach with the defaulting top 'programfiles' as a generic fallback mechanism will not work neither. The problem is that there is no generic way of not only reading but also assigning the the installdir.

Imagine you are dealing with the MSI that has the installdir maned "MY_INSTALLDIR". Your prev routine will try to expand INSTALLDIR, fail and return "C:\Program Files (x86)". Now you let user select alternative director and set the path back to the directory. But which one? If you indeed assuming that it will always be INSTALLDIR then you obviously do it as "INSTALLDIR=C:\user\custom_dir". But this will have no effect as the actual installdir "MY_INSTALLDIR" stays unchanged.

Thus we need to accept that there is no any generic solution for the problem and the installdir name should always be explicitly nominated by the author of the MSI or MSI_UI.

Thus I completely reworked the solution and scrapped MsiParser.GetInstallDirectoryRoot(). Instead you will need to call MsiParser.GetDirectoryPath(string name) and pass the name (id) of the directory you want to resolve into the target system path. In the wast majority of cases it will be "INSTALLDIR" but you can always specify the alternative name (as some Wix# users do).