Wixsharp: Browse to file or folder and save value in the session

Created on 12 Jul 2018  路  19Comments  路  Source: oleg-shilo/wixsharp

Part of the setup is to install a new ODBCDataSource. The user needs to choose (browse to) either a folder or a file on the computer. This value, I assume, needs to be saved in a session variable and used later in the setup when the ODBCDataSource is created. What is the best way to get the input from the user and how can it be transferred to the session to be used in a later stage of the install? If CustomCLRDialog is used, how is the valued saved in the session to be used at a later stage in the install?
Thanks

question

All 19 comments

In both CustomCLRDialog and ManagedUI the approach is the same:
C# e.Session["property-name"] = "property value";

I have modified the CustomCLRDialog example to have a button which will open up the File Browser Dialog Box

// member variable
public FolderBrowserDialog folderBrowserDialog1;

private void button1_Click(object sender, EventArgs e)
{
folderBrowserDialog1 = new FolderBrowserDialog();
if (folderBrowserDialog1.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
}
}

When it tries to open up the dialog box, the program becomes unresponsive. What is wrong and how can I fix this?
Thanks

You can try to put a break point there and see what is happening under debugger. Most likely it throws the exception on .ShowDialog().

One of the possible reasons for that is the FolderBrowserDialog is a COM object and it requires the AppDomain that hosts it to be started with "SingleThreadAppartment". It is a difficult problem to fix as you cannot control how MSI initializes the AppDomain.

One of the possible solutions is to fork a temporary apartment that is initialized correctly and popup the dialog from there. This technique is not trivial and I am going to demonstrate it in the code sample in a day or two. Just don't have the time right now.

Though I can be wrong and the problem can be unrelated completely.

It simply reminds me very much the canonical Console app problem:
```C#
using System;
. . .

class Program
{
static void Main()
{
var folderBrowserDialog1 = new FolderBrowserDialog();
if (folderBrowserDialog1.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
. . .
}
}
}
```

The code above will always fail because the method Main is not marked with the [STAThread] attribute.

Is there any way to somehow duplicate the way that the regular install has a file browse dialog to specify the install folder and use it for our own function?

Sorry, but tried to investigate it under debugger as I asked? Without it it is hard to choose the solution - you just don't know the problem.

... somehow duplicate the way that the regular install ...

In anticipation of your response on my first question, this is how you ensure correct invoking of FolderBrowserDialog from the incompatible thread:

```C#
// [STAThread]
static void Main(string[] args)
{
string folder = "";

var thread = new Thread(x =>
{
    folder = SelectFolder();
});
thread.TrySetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();

Console.WriteLine(folder);

}

static string SelectFolder()
{
using (var dialog = new FolderBrowserDialog())
{
if (dialog.ShowDialog() == DialogResult.OK)
return dialog.SelectedPath;
return null;
}
}
```

Thank you so much. I did try to run the debugger but the code also hung the debugger. On initial testing, it looks like this code will work.

We are nearly there!! I need a little more help, if you don't mind, on how to connect the function to the setting up of an ODBC connection dynamically. In the following code snippet, I need to put the value returned from the GetFolder routine into the dynamically created ODBC connection - to somehow create a way of transferring the value from a custom action to the main Project creation.

```C#
//dynamically add the ODBC connection
string myDataBaseFile;

project.AddActions(new ManagedAction(CustomActions.GetFolder, Return.check, When.Before, Step.InstallODBC, Condition.NOT_Installed));

// This line needs to be replaced with valid code
myDataBaseFile = session["getfolder"]

project.Dirs[0].Dirs[0].ODBCDataSources = project.Dirs[0].Dirs[0].ODBCDataSources.Combine(
new ODBCDataSource("DSN_Name", "Some ODBC Driver", true, true,
new Property("DataBaseFile", myDataBaseFile)
)
);

public class CustomActions
{
[CustomAction]
public static ActionResult GetFolder(Session session)
{
string folder = "";
var thread = new Thread(x =>
{
folder = SelectFolder();
});
thread.TrySetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
session["getfolder"] = folder;
return ActionResult.Success;
}

static string SelectFolder()
{
    using (var dialog = new FolderBrowserDialog())
    {
        dialog.Description = "Select the database you would like to use for the data source.";
        if (dialog.ShowDialog() == DialogResult.OK)
            return dialog.SelectedPath;
        return null;
    }
}

}
```

I would expect that you would be able to do it directly without the use of any intermediate property:
C# thread.Start(); thread.Join(); // session["getfolder"] = folder; session["DataBaseFile"] = folder; return ActionResult.Success;
The code above will override the property initial value (myDataBaseFile) with the one returned by SelectFolder.

The problem is that the code does not compile. I am referring specifically to the code which is in
static public void Main(string[] Args) which is show below. There is no session variable so the following line gives a compilation error.

myDataBaseFile = session["DataBaseFile"]

Once we can retrieve the value from the session we can call
project.Dirs[0].Dirs[0].ODBCDataSources = project.Dirs[0].Dirs[0].ODBCDataSources.Combine(
new ODBCDataSource("DSN_Name", "Some ODBC Driver", true, true,
new Property("DataBaseFile", session["DataBaseFile"] )
)
);

Can you please provide a complete code. Can you also please use formatting:

``` C#
<your code>
```

It is difficult to navigate in non-formatted code.

I have modified your ODBCDataSource example. The problem is that this will not compile because of the line accessing the session variable.
``` C#
new Property("Database", session["DataBaseFile"]),


Here is the full source

``` C#
using Microsoft.Deployment.WindowsInstaller;
using System;
using System.Threading;
using System.Windows.Forms;
using WixSharp;
using WixSharp.CommonTasks;

class Script
{
    static public void Main(string[] args)
    {
        var project = new Project("My Product",
                         new Dir(@"%ProgramFiles%\My Company\My Product",
                             new ODBCDataSource("DsnName", "SQL Server", true, true,
                                 new Property("Database", session["DataBaseFile"]),
                                  new Property("Location", "MyDb"),
                                 new Property("Server", "MyServer"))));

        project.GUID = new Guid("6f330b47-2577-43ad-9095-1861ba25889b");
        project.PreserveTempFiles = true;
        project.AddActions(new ManagedAction(CustomActions.GetFolder, Return.check, When.Before, Step.InstallODBC, Condition.NOT_Installed));
        project.BuildMsi();
    }
}

public class CustomActions
{
    [CustomAction]
    public static ActionResult GetFolder(Session session)
    {
        string folder = "<unknown>";
        var thread = new Thread(x =>
        {
            folder = SelectFolder();
        });
        thread.TrySetApartmentState(ApartmentState.STA);
        thread.Start();
        thread.Join();
        session["DataBaseFile"] = folder;
        return ActionResult.Success;
    }

    static string SelectFolder()
    {
        using (var dialog = new FolderBrowserDialog())
        {
            dialog.Description = "Select the database you would like to use for the data source.";
            if (dialog.ShowDialog() == DialogResult.OK)
                return dialog.SelectedPath;
            return null;
        }
    }
}

Te problem is that you are trying to use the runtime session at compile time:

The correct code should be like that:

```C#
var project = new Project("My Product",
new Dir(@"%ProgramFiles%\My Company\My Product",
new ODBCDataSource("DsnName", "SQL Server", true, true,
new Property("Database", ""),
new Property("Location", "MyDb"),
new Property("Server", "MyServer"))));

. . .

    thread.Start();
    thread.Join();
    session["Database"] = folder;
    return ActionResult.Success;
}

`` Though I am not sure how it is going to work if you are assigningDatabase` property initial value to a file path and at runtime overwriting it with the directory pat.

Here is the issue. The session variable
``` C#
session["Database"] = folder;

refers to the global session whereas  
``` C#
new Property("Database", "<default database dir>"), 

refers to a property of the ODBCDataSource - an input. Setting the former will have no effect on the latter. The difficulty that I am experiencing is getting the folder value into the ODBCDataSource.

I am not sure it is the expected behavior. This is how Wix describes Property:

Value | String 
Sets a default value for the property.  The value will be overwritten if 
the Property is used for a search.

And this is your ODBCDataSource typical WiX:

<Component Id="Component.DsnName" Guid="6f330b47-2577-43ad-9095-1861c7bcf982">
  <ODBCDataSource Id="DsnName" Name="DsnName" DriverName="SQL Server" KeyPath="yes" Registration="machine">
    <Property Id="Database" Value="MyDb" />
    <Property Id="Server" Value="MyServer" />
  </ODBCDataSource>
  <RemoveFolder Id="INSTALLDIR" On="uninstall" />
</Component>

Is it possible that you set your property at runtime too late and ODBCDataSource reads the initial value instead?

I tried to put the folder browser before the LaunchConditions, but unfortunately, the value also did not get into the ODBC connection.

Based on the limitations that I have been having with the WIX Toolset - and you have done an amazing job of wrapping up the functionality in C# -, I am interested in knowing if you could suggest another installation framework instead of WIX that I could use for my specific needs which are being able to generate the installation dynamically, configure ODBC connections, install windows services, check for the existence of drivers and if not, install them, I assume that every windows machine nowadays will have at least .NET 2.0 so the ideal candidate would be a installation framework based on .NET which will be easy to customize.

You are absolutely right there is only so much you can do with WiX/MSI. But before you give it up try to do a simple test. Just to see that there is no mistake in your code:

```C#
var project = new Project("My Product",
. . .
project.Load += (e)=>
{
MessageBox.Show(e.Session["database"]);
};

This way you will ensure that the "database" property is getting the desired content.

Another thing to try is a simple referencing of the session properties:
```C#
var project = new Project("My Product",
                         new Dir(@"%ProgramFiles%\My Company\My Product",
                             new ODBCDataSource("DsnName", "SQL Server", true, true,
                                 new Property("Database", "[DBFILE]"),
                                 new Property("Location", "MyDb"),
                                 new Property("Server", "MyServer")))
                         new Property("DBFILE", @"c:/whatever/file")))
);

Sometimes these tricks may just work.

Oleg, the second suggestion. Amazing! Thank you.

Your second suggestion worked when we called FolderBrowserDialog etc. The value in the session made it into the ODBC connection

Now I am trying to get this same concept to work in my solution which uses Custom_UI which inherits from WixCLRDialog to give a list of databases. I want to get the selected database name across to the ODBC connection which is in the main project.

When the next button is pressed I set this.session["Database"] to the database name.

``` C#

private void nextBtn_Click(object sender, EventArgs e)
{
this.session["Database"] = DataBaseCmb.SelectedItem.ToString();
MSINext();
}

This then should get the value transferred to the ODBCDataSource but for some reason, it does not get across.

class Script
{
static public void Main(string[] args)
{
var project = new Project("My Product",
new Dir(@"%ProgramFiles%\My Company\My Product",
new ODBCDataSource("DsnName", "SQL Server", true, true,
new Property("Database", "[Database]"),
new Property("Location", "MyDb"),
new Property("Server", "MyServer"))));

    project.GUID = new Guid("6f330b47-2577-43ad-9095-1861ba25889b");
    project.PreserveTempFiles = true;
    project.BuildMsi();
}

}

```
Do you have any suggestions?

This is all working well now. The variable DBFILE should be in capital letters. Thank you so much!!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Eagle3386 picture Eagle3386  路  4Comments

empz picture empz  路  3Comments

marcobeninca71 picture marcobeninca71  路  4Comments

milhousemanastorm picture milhousemanastorm  路  4Comments

meytes picture meytes  路  3Comments