As you may have gathered from my last post, I’ve been playing around with ClickOnce a bit lately.  The latest head-scratcher is how to get a ClickOnce deployed application to run at startup.  It should be a no-brainer (just copy a shortcut in the startup folder, right?) but when you add Vista to the mix all hell breaks loose (and when I say “all hell breaks loose” I mean nothing happens at startup.)  I finally sat down this weekend and put together a sample application and some documentation to support my findings.  Any help or feedback is greatly appreciated.  The main goal is to determine a reliable, reusable way to add AutoStart functionality to any ClickOnce deployed application for any Windows operating system.  Again, piece of cake, right?

Quick Overview

An application shortcut is copied to the Start Menu Programs directory as part of the installation process for a ClickOnce application which is configured to be available offline1. For example, for me on Vista, the attached sample application’s shortcut is placed in the following location: C:\Users\Ben Griswold\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Ben Griswold\ClickOnceAutoStart.appref-ms. 

The shortcut location and file name are based on the Publisher name (Ben Griswold) and the Product name (ClickOnceAutoStart) as defined in the project’s Publishing Options2.

clip_image002

It is important to note that the shortcut’s location and file name are NOT based on the executing assembly’s Company and Product values3, however, the Publishing Options and Assembly Information values are often identical (as is the case in the attached sample application.)

clip_image004

Though Visual Studio provides a means to add an application shortcut to the products folder in the Start Menu Programs directory as well as the Desktop4, there does not appear to be an out-of-the-box means to add a shortcut to Startup Folder. In other words, there is no configuration setting to enable the ClickOnce application to run at startup.

ClickOnce AutoStart Solutions

There are two sited ways to implement run at startup functionality. Both options, however, come with drawbacks and issues.

Solution 1

Programmatically copy the application’s Start Menu Program shortcut (the Application Reference file) into the Startup folder.

clip_image006

Start Menu\Programs\Ben Griswold

clip_image008

Start Menu\Programs\Startup

Issue 1 - There doesn’t appear to be a reliable way to determine where the ClickOnce installer placed the Start Menu Program .appref-ms shortcut. As stated above, the shortcut location is based on the Publishing Options. Can one programmatically read the Publisher and Product name? Is this information is available via the AppDomain.ActivationContext Property? The bottom-line is, if the shortcut location can’t be determined then the file can’t be copied.

Arguably, this isn’t a serious issue since the shortcut location could be “pre-calculated” and stored elsewhere in the application. For example, in Application Settings or within AssemblyInfo which seems to be the de facto standard. All the same, since this Publisher and Product are displayed on the Deployment Web Page5, it is reasonable to assume the values must be exposed.

clip_image010

Issue 2 – Copying the Application Reference shortcut into the Startup folder doesn’t work reliably in Windows Vista. This is the case whether UAC is enabled or not. Interestingly, the same shortcut can successfully launch the application if it is manually executed by the user, but under standard OS startup the shortcut is never triggered.

Issue 3 – Another issue is tied to the uninstall. Is there a way to hook into the uninstall of a ClickOnce application? If not, there doesn’t appear to be a way to remove any/all artifacts which may have been programmatically added to the end users system. In other words, after the product uninstall, it would become the end user’s responsibility to remove the shortcut file which was programmatically copied into the Startup folder. Coincidently, the Start Menu Programs shortcut and desktop shortcut are all removed when the ClickOnce application is uninstalled.

Solution 2

Programmatically create a URL shortcut file which maps to the ClickOnce application’s update location and place it in the Startup folder. I am happy to say this method does work reliably in Windows Vista.

Issue 1 - Though creating an URL shortcut file which maps to the ApplicationDeployment.CurrentDeployment.UpdateLocation.AbsoluteUri works consistently, an empty browser window is launched when the Uri is being queried. Again, this solution works but it isn’t particularly clean.

Issue 2 – Same uninstall issue as outline above.

Sample Application

The accompanying sample application illustrates how to enable run at startup using either of the two techniques stated above. One may validated the functionality and issues by launching the application (after installing via ClickOnce) and then enabling the AutoStart options, verifying shortcut are valid and testing the AutoStart functionality per a system restart.

clip_image012

Again, any help or feedback is greatly appreciated. The main goal is to determine a reliable, reusable way to add AutoStart functionality to any ClickOnce deployed application for any Windows operating system.

Download ClickOnce AutoStart Sample:ClickOnceAutoStart.zip

References:

ClickOnce Startup Folder

Tip: ClickOnce Deployment

1 Project Properties > Publish > Install Model and Settings

2 Project Properties > Publish > Install Model and Settings > Options… > Description

3 AssemblyInfo.cs or Properties > Application > Assembly Information…

4 Project Properties > Publish > Install Model and Settings > Options… > Manifest > Create desktop shortcut

5 Project Properties > Publish > Install Model and Settings > Options… > Deployment.

 

18 Responses to “ClickOnce Run at Startup”

  1. The uninstall problem is a real annoyance on this one.

  2. Keep checking back, Tim. I’m hopeful I’ll be posting a resolution in the next couple of weeks — fingers crossed.

  3. Here is how I create short cuts for a ClickOnce app. It uses System.Reflection and the Environment.GetFolderPath. Uninstall still leaves the shortcuts, however.

    Assembly assembly = Assembly.GetEntryAssembly();
    string company = string.Empty;
    string description = string.Empty;

    if (Attribute.IsDefined(assembly, typeof(AssemblyCompanyAttribute)))
    {
    AssemblyCompanyAttribute ascompany = (AssemblyCompanyAttribute)Attribute.GetCustomAttribute(assembly, typeof(AssemblyCompanyAttribute));
    company = ascompany.Company;
    }
    if (Attribute.IsDefined(assembly, typeof(AssemblyDescriptionAttribute)))
    {
    AssemblyDescriptionAttribute asdescription = (AssemblyDescriptionAttribute)Attribute.GetCustomAttribute(assembly, typeof(AssemblyDescriptionAttribute));
    if (description == “”)
    description = appName.Replace(”.exe”, “”);
    }
    if (!string.IsNullOrEmpty(company))
    {
    string desktopPath = string.Empty;
    string startUp = string.Empty;
    string startMenu = string.Empty;

    desktopPath = string.Concat(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), “\\”, description, “.appref-ms”);

    startUp = string.Concat(Environment.GetFolderPath(Environment.SpecialFolder.Startup), “\\”, description, “.appref-ms”);
    startMenu = string.Concat(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), “\\”, description, “.appref-ms”);

    shortcutName = string.Empty;
    shortcutName = string.Concat(Environment.GetFolderPath(Environment.SpecialFolder.Programs), “\\”, company, “\\”, description, “.appref-ms”);

    try
    {
    System.IO.File.Copy(shortcutName, desktopPath, true);
    }
    catch
    {

    }
    try
    {
    System.IO.File.Copy(shortcutName, startUp, true);
    }
    catch
    {

    }

    try
    {
    System.IO.File.Copy(shortcutName, startMenu, true);
    }
    catch
    {

    }
    }

  4. @Brian Thanks for providing your sample. You will find similiar logic inside of the downloadable application included with this post. I’ve fallen back to using the executing assembly’s Company and Product values as well, but these values aren’t necessarily correct as ClickOnce uses the Publishing Options (not Assembly Information) to define file names and paths. In my opinion, that’s the big gotcha. Let me know if I’m missing anything. Thanks again.

  5. Hi, Just thought I’d leave a quick post in case youve not already resolved the issue yourself.

    I have a similar issue trying to resolve the ‘publisher/suiteName/product’ information - these values are required if you need to launch a click once application programmatically, the information in the assembly does not reliably describe the ’start menu’ path to the click once app.

    The information you need is in the .application deployment manifest which is located at the URI where the application is installed from.

    I have written some c# code to extract this information into a ‘DeploymentDescription’ class which has ‘Publisher’,'SuiteName’ & ‘Product’ properties. The code employs the services of an InPlaceHostingManager to asynchronously aquire the manifest from the application host URI. It is then possible to use an XmlReader to extract the required info.

    Post back if you would like the source code.

    Cheers,

    Andy

  6. Hi Ben,

    Here’s the code. You’ll notice the GetDeploymentDescription() method is asynchronous so you may want to wire up an event to fire when the information is loaded. In my case the code is contained within a class that has a string field ’startMenuCommand’ which is set when the deployment information has been loaded.

    Hope this works ok for you.

    Cheers,

    Andy

    // utility class to extract publisher/suiteName/Product

    public class DeploymentDescription
    {
    private const string descriptionElement = “description”;
    private const string publisherAttribute = “publisher”;
    private const string suiteNameAttribute = “suiteName”;
    private const string productAttribute = “product”;
    private string publisher;
    private string suiteName;
    private string product;

    public DeploymentDescription(XmlReader xmlDeploymentManifest)
    {
    ExtractDescriptions(xmlDeploymentManifest);
    }

    private void ExtractDescriptions(XmlReader appManifest)
    {
    while (appManifest.Read())
    {
    if (appManifest.NodeType == XmlNodeType.Element)
    {
    if (appManifest.Name == descriptionElement)
    {
    appManifest.MoveToFirstAttribute();
    do
    {
    if (appManifest.Name.Contains(publisherAttribute))
    publisher = appManifest.Value;
    else if (appManifest.Name.Contains(suiteNameAttribute))
    suiteName = appManifest.Value;
    else if (appManifest.Name.Contains(productAttribute))
    product = appManifest.Value;
    } while (appManifest.MoveToNextAttribute());
    return;
    }
    }
    }
    }

    public string Publisher
    {
    get { return publisher; }
    }

    public string SuiteName
    {
    get { return suiteName; }
    }

    public string Product
    {
    get { return product; }
    }
    }

    // utility class to build executable command

    public class StartMenuCommandBuilder
    {
    private readonly string command;

    public StartMenuCommandBuilder(DeploymentDescription deploymentDescription)
    {
    if (deploymentDescription == null) throw new ArgumentNullException(”deploymentDescription”);
    command = Environment.GetFolderPath(Environment.SpecialFolder.Programs);
    if (!string.IsNullOrEmpty(deploymentDescription.Publisher))
    command = Path.Combine(command, deploymentDescription.Publisher);
    if (!string.IsNullOrEmpty(deploymentDescription.SuiteName))
    command = Path.Combine(command, deploymentDescription.SuiteName);
    command = Path.Combine(command, deploymentDescription.Product);
    command = Path.ChangeExtension(command, “.appref-ms”);
    }

    public string Command
    {
    get{return command;}
    }
    }

    // your code should do something like ……

    if (ApplicationDeployment.IsNetworkDeployed)
    {
    GetDeploymentDescription(ApplicationDeployment.CurrentDeployment);
    }

    // method to asynchronously retrieve ClickOnce ‘Descriptions’ data from host URI

    private void GetDeploymentDescription(ApplicationDeployment currentDeployment)
    {
    var inPlaceHostingManager = new InPlaceHostingManager(currentDeployment.UpdateLocation, false);
    inPlaceHostingManager.GetManifestCompleted += ((sender, e) =>
    {
    var deploymentDescription = new DeploymentDescription(e.DeploymentManifest);
    var commandBuilder = new StartMenuCommandBuilder(deploymentDescription);
    startMenuCommand = commandBuilder.Command;
    });
    inPlaceHostingManager.GetManifestAsync();
    }

  7. @Andy - Thanks for your comments and thanks for sharing your code. This issue (particularly the common misunderstanding of where the start menu information is derived) has been driving me nuts for weeks. I can’t tell you how excited I am that you know how to get at the “real” ClickOnce publisher and product values and I can hardly wait to play around with your code tonight. Thanks for the contribution!

  8. Solution is may not be as reliable in Vista if you look closer..

    If the user is offline when logging on to windows, the blank IE windows opens (is it actually trying to connect to the server specified in the short cut) and “Page cannot be displayed” pops up. Everything stops here.

    This is not reliable because majority of the users wont be connected to the internet when they are logging on to windows unless they are on a network.

  9. @Dipesh A. - That’s a valid point regarding URL Shortcut solution for Vista. Assuming the user has an Internet connection, it will work just fine, but, you’re right, the solution doesn’t gracefully handle itself in the case of the user who is working offline. Thanks for the comment.

  10. Why don’t you use a batch file? For example, a file named “AutoStartClickOnceApp.bat” would contain the following single line of text: “C:\Documents and Settings\[User Name]\Start Menu\Programs\[ClickOnce Application Name].appref-ms”

    It should be that simple! Sure, you’ll get a Command Prompt window that will appear for a brief moment but that is typical of MS Windows (i.e. logging into some domain).

    I hope this helps. Good luck!

  11. Er.. what about simply updating the registry to put the path of the executing assembly in HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Run?

    This will work on Vista and XP. When the app updates you can simply update the registry with the new exe path (since it changed on every deployment).

  12. @Mark Fulton - That’s an interesting idea. So, I would have my ClickOnce application create a bat file with the appropriate launch information (appref-ms reference) rather than including the shortcut to the appref-ms file in the Startup folder itself. I’ll give it a shot.

    @Dirtel Minder - Unfortunately that approach doesn’t work on Vista for ClickOnce hosted applications. It doesn’t work, however, if the application were running as a non-ClickOnce deployed application.

  13. @Ben Did you try out the batch file solution? Did it work?

  14. I used the executable of my application to launch itself via the appref-ms shortcut. This doesn’t require a browser and works fine on vista. I’ll post a piece on this one on my blog. I didn’t solve the uninstall issue yet.

  15. Here is what I’m using, I’m pretty sure it is going to work but I haven’t fully tested it. I’ve got a lot more hard coding in here than you would want but that can be gathered easily if necessary. (Publisher name, resultant executable, name you can choose for the key can also be derived from the assembly) etc…

    You need to import Microsoft.Win32

    Dim key As RegistryKey = Registry.CurrentUser.OpenSubKey(”Software”)
    key = key.OpenSubKey(”Microsoft”)
    key = key.OpenSubKey(”Windows”)
    key = key.OpenSubKey(”CurrentVersion”)
    key = key.OpenSubKey(”Run”, True)
    If key.GetValue(”YOUR_KEY_NAME”) Is Nothing Then

    If System.IO.Directory.Exists(”C:\Users”) Then
    key.SetValue(”YOUR_KEY_NAME”, “C:\Users\” + Environment.UserName + “\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\”+”PUBLISHER_NAME_HERE” + “\” + “YOUR_EXECUTABLE”)
    Else
    key.SetValue(”YOUR_KEY_NAME”, “C:\Documents and Settings\” + Environment.UserName + “\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\”+”PUBLISHER_NAME_HERE” + “\” + “YOUR_EXECUTABLE”)

  16. Here is a simplified version with a few bug fixes from above, only think that would make this a little better would be pulling the app info and publisher info, this just uses the Assembly info.

    Dim key As RegistryKey = Registry.CurrentUser.OpenSubKey(”Software”)
    key = key.OpenSubKey(”Microsoft”)
    key = key.OpenSubKey(”Windows”)
    key = key.OpenSubKey(”CurrentVersion”)
    key = key.OpenSubKey(”Run”, True)
    Dim rootdir = “”
    If System.IO.Directory.Exists(”C:\Users”) Then rootdir = “C:\Users\” Else rootdir = “C:\Documents and Settings\”

    key.SetValue(”QBCommenter”, rootdir + Environment.UserName + “\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\” + My.Application.Info.CompanyName + “\” + My.Application.Info.ProductName + “.appref-ms”)

  17. I have the uninstall issue but for a different reason…

    I am publishing a new version of my app that specifies a specific CPU in order to support 64 bit systems. This “breaks” the clickonce automatic update. My workaround is to provide users the URL to launch the latest version and then, since it will have created a 2nd application, I want to programaitically uninstall the old version.

    Any update on solving the uninstall issue?

  18. suggested workaround
    click once can create desktop shortcut on update

    when onload copy the new desktop shortcut to the applications executing assembly folder then delete desktop one if necessary

    create registry run key with value set to the shortcut path now residing in the executing assembly
    ie my_new_key c:\users\what-en-what\appref-ms

    on restart isnetworkdeployed = true

    hope this helps

Leave a Reply

You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>