65

I've written a program that edits a specific filetype , and I want to give the user the option to set my application as the default editor for this filetype (since I don't want an installer) on startup.

I've tried to write a re-useable method that associates a file for me (preferably on any OS, although I'm running Vista) by adding a key to HKEY_CLASSES_ROOT, and am using it with my application, but it doesn't seem to work.

public static void SetAssociation(string Extension, string KeyName, string OpenWith, string FileDescription)
{
    RegistryKey BaseKey;
    RegistryKey OpenMethod;
    RegistryKey Shell;
    RegistryKey CurrentUser;

    BaseKey = Registry.ClassesRoot.CreateSubKey(Extension);
    BaseKey.SetValue("", KeyName);

    OpenMethod = Registry.ClassesRoot.CreateSubKey(KeyName);
    OpenMethod.SetValue("", FileDescription);
    OpenMethod.CreateSubKey("DefaultIcon").SetValue("", "\"" + OpenWith + "\",0");
    Shell = OpenMethod.CreateSubKey("Shell");
    Shell.CreateSubKey("edit").CreateSubKey("command").SetValue("", "\"" + OpenWith + "\"" + " \"%1\"");
    Shell.CreateSubKey("open").CreateSubKey("command").SetValue("", "\"" + OpenWith + "\"" + " \"%1\"");
    BaseKey.Close();
    OpenMethod.Close();
    Shell.Close();

    CurrentUser = Registry.CurrentUser.CreateSubKey(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\" + Extension);
    CurrentUser = CurrentUser.OpenSubKey("UserChoice", RegistryKeyPermissionCheck.ReadWriteSubTree, System.Security.AccessControl.RegistryRights.FullControl);
    CurrentUser.SetValue("Progid", KeyName, RegistryValueKind.String);
    CurrentUser.Close();
}

Any idea why it doesn't work? An example use might be

SetAssociation(".ucs", "UCS_Editor_File", Application.ExecutablePath, "UCS File"); 

The part of the method that uses "CurrentUser" seems to work if I do the same using regedit, but using my application it doesn't.

xmedeko
  • 7,336
  • 6
  • 55
  • 85
User2400
  • 2,373
  • 2
  • 20
  • 22
  • have you tried running your program as admin? – Colin Newell Apr 21 '10 at 10:05
  • UAC means that your app doesn't run as admin unless you explicitly require it. You run Vista, Vista includes UAC. Can you double check if the program runs as administrator? – Benjamin Podszun Apr 21 '10 at 10:12
  • I've tried "Run as Administrator" plus UAC has been turned off anyway but the file still isn't associated after the program runs. – User2400 Apr 21 '10 at 10:15
  • I think the 3rd to last line in your method may be incorrect. I don't think you want to set "CurrentUser" to be the subkey. – Maestro1024 May 25 '11 at 12:33
  • Related question [Filetype association with application (C#)](http://stackoverflow.com/questions/222561/filetype-association-with-application-c) – Deanna May 17 '13 at 10:43

9 Answers9

39

The answer was a lot simpler than I expected. Windows Explorer has its own override for the open with application, and I was trying to modify it in the last lines of code. If you just delete the Explorer override, then the file association will work.

I also told explorer that I had changed a file association by calling the unmanaged function SHChangeNotify() using P/Invoke

public static void SetAssociation(string Extension, string KeyName, string OpenWith, string FileDescription)
{
    // The stuff that was above here is basically the same

    // Delete the key instead of trying to change it
    var CurrentUser = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\" + Extension, true);
    CurrentUser.DeleteSubKey("UserChoice", false);
    CurrentUser.Close();

    // Tell explorer the file association has been changed
    SHChangeNotify(0x08000000, 0x0000, IntPtr.Zero, IntPtr.Zero);
}

[DllImport("shell32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern void SHChangeNotify(uint wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2);
Aurumaker72
  • 33
  • 1
  • 7
User2400
  • 2,373
  • 2
  • 20
  • 22
  • 8
    I know this is old and you may have caught this already, but I noticed in this code segment and in your first post that the first line of CurrentUser = you have the .ucs extension hard coded into your OpenSubKey() call. – Chuck Savage Apr 20 '11 at 23:07
  • Worked very good for me ! but pay attention - requires administrative rights – Kamornik Cola Jul 09 '17 at 20:19
  • Deleting subkey for known extensions like .png is working but as soon as I notify explorer, it restores back the UserChoice. How can I stop explorer restoring the UserChoice? – HGMamaci Feb 03 '18 at 12:34
36

Here's a complete example:

public class FileAssociation
{
    public string Extension { get; set; }
    public string ProgId { get; set; }
    public string FileTypeDescription { get; set; }
    public string ExecutableFilePath { get; set; }
}

public class FileAssociations
{
    // needed so that Explorer windows get refreshed after the registry is updated
    [System.Runtime.InteropServices.DllImport("Shell32.dll")]
    private static extern int SHChangeNotify(int eventId, int flags, IntPtr item1, IntPtr item2);

    private const int SHCNE_ASSOCCHANGED = 0x8000000;
    private const int SHCNF_FLUSH = 0x1000;

    public static void EnsureAssociationsSet()
    {
        var filePath = Process.GetCurrentProcess().MainModule.FileName;
        EnsureAssociationsSet(
            new FileAssociation
            {
                Extension = ".ucs",
                ProgId = "UCS_Editor_File",
                FileTypeDescription = "UCS File",
                ExecutableFilePath = filePath
            });
    }

    public static void EnsureAssociationsSet(params FileAssociation[] associations)
    {
        bool madeChanges = false;
        foreach (var association in associations)
        {
            madeChanges |= SetAssociation(
                association.Extension,
                association.ProgId,
                association.FileTypeDescription,
                association.ExecutableFilePath);
        }

        if (madeChanges)
        {
            SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_FLUSH, IntPtr.Zero, IntPtr.Zero);
        }
    }

    public static bool SetAssociation(string extension, string progId, string fileTypeDescription, string applicationFilePath)
    {
        bool madeChanges = false;
        madeChanges |= SetKeyDefaultValue(@"Software\Classes\" + extension, progId);
        madeChanges |= SetKeyDefaultValue(@"Software\Classes\" + progId, fileTypeDescription);
        madeChanges |= SetKeyDefaultValue($@"Software\Classes\{progId}\shell\open\command", "\"" + applicationFilePath + "\" \"%1\"");
        return madeChanges;
    }

    private static bool SetKeyDefaultValue(string keyPath, string value)
    {
        using (var key = Registry.CurrentUser.CreateSubKey(keyPath))
        {
            if (key.GetValue(null) as string != value)
            {
                key.SetValue(null, value);
                return true;
            }
        }

        return false;
    }
Kirill Osenkov
  • 8,786
  • 2
  • 33
  • 37
18

You can do that in a managed way via ClickOnce. No fussing with the registry yourself. This is available via tooling (i.e. no xml) in VS2008 and above (including Express) on Project Properties => Publish => Options => File Associations

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • 2
    Nice answer, but unfortunately I'm using VS2005 so will I have to wait until I get VS2010? – User2400 Apr 21 '10 at 10:41
  • That's nice and all, but, how do you use this method and set custom command-line arguments? This method forces you to use `app.exe "%1"`, but what if I wanted it to do `app.exe /config "%1"`? – Andy Jan 30 '17 at 15:00
11

Solution above did not work for me with Windows 10. Here is my solution to open files with the .myExt extension with %localappdata%\MyApp\MyApp.exe for current user. Optimised after reading comments.

 String App_Exe = "MyApp.exe";
 String App_Path = "%localappdata%";
 SetAssociation_User("myExt", App_Path + App_Exe, App_Exe);

 public static void SetAssociation_User(string Extension, string OpenWith, string ExecutableName)
 {
    try {
                using (RegistryKey User_Classes = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Classes\\", true))
                using (RegistryKey User_Ext = User_Classes.CreateSubKey("." + Extension))
                using (RegistryKey User_AutoFile = User_Classes.CreateSubKey(Extension + "_auto_file"))
                using (RegistryKey User_AutoFile_Command = User_AutoFile.CreateSubKey("shell").CreateSubKey("open").CreateSubKey("command"))
                using (RegistryKey ApplicationAssociationToasts = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\Windows\\CurrentVersion\\ApplicationAssociationToasts\\", true))
                using (RegistryKey User_Classes_Applications = User_Classes.CreateSubKey("Applications"))
                using (RegistryKey User_Classes_Applications_Exe = User_Classes_Applications.CreateSubKey(ExecutableName))
                using (RegistryKey User_Application_Command = User_Classes_Applications_Exe.CreateSubKey("shell").CreateSubKey("open").CreateSubKey("command"))
                using (RegistryKey User_Explorer = Registry.CurrentUser.CreateSubKey("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\." + Extension))
                using (RegistryKey User_Choice = User_Explorer.OpenSubKey("UserChoice"))
                {
                    User_Ext.SetValue("", Extension + "_auto_file", RegistryValueKind.String);
                    User_Classes.SetValue("", Extension + "_auto_file", RegistryValueKind.String);
                    User_Classes.CreateSubKey(Extension + "_auto_file");
                    User_AutoFile_Command.SetValue("", "\"" + OpenWith + "\"" + " \"%1\"");
                    ApplicationAssociationToasts.SetValue(Extension + "_auto_file_." + Extension, 0);
                    ApplicationAssociationToasts.SetValue(@"Applications\" + ExecutableName + "_." + Extension, 0);
                    User_Application_Command.SetValue("", "\"" + OpenWith + "\"" + " \"%1\"");
                    User_Explorer.CreateSubKey("OpenWithList").SetValue("a", ExecutableName);
                    User_Explorer.CreateSubKey("OpenWithProgids").SetValue(Extension + "_auto_file", "0");
                    if (User_Choice != null) User_Explorer.DeleteSubKey("UserChoice");
                    User_Explorer.CreateSubKey("UserChoice").SetValue("ProgId", @"Applications\" + ExecutableName);
                }
                SHChangeNotify(0x08000000, 0x0000, IntPtr.Zero, IntPtr.Zero);
            }
            catch (Exception excpt)
            {
                //Your code here
            }
        }

  [DllImport("shell32.dll", CharSet = CharSet.Auto, SetLastError = true)]
  public static extern void SHChangeNotify(uint wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2);
sofsntp
  • 1,964
  • 23
  • 34
  • From [this answer](http://stackoverflow.com/a/13535039/1497596): *"The `RegistryKey` class implements `IDisposable` and so you should wrap your keys in a `using` statement."* Or, alternatively, you should call [Close or Dispose](http://stackoverflow.com/a/37059244/1497596) on the `RegistryKey` when you are done with it. This means that **chaining calls to `CreateSubKey` as shown in this example is a bad idea**. – DavidRR Jan 05 '17 at 19:16
  • Agree with you it's cleaner but it is still interesting to understand what is the worst side effect I can expect from the code above. Ideas ? – sofsntp Jan 06 '17 at 09:17
  • If you do not dispose an *unmanaged* resource (e.g., `RegistryKey`), your application will suffer from memory leaks. See [Relation between resource leaks and memory leaks and performance](http://stackoverflow.com/q/11879701/1497596) and [IDisposable Interface](https://msdn.microsoft.com/en-us/library/System.IDisposable.aspx). Note that the code examples in both the question and the accepted answer include calls to `RegistryKey.Close` as required. – DavidRR Jan 06 '17 at 13:14
  • yikes. spending 30 seconds looking at this code, it's all wrong. The `User_Extension` key creates a `.ext` key in `CURRENT_USER`. It should be `CLASSES_ROOT`. I stopped looking after that. If that's wrong, the rest is probably wrong too. – Andy Jan 30 '17 at 13:52
  • @Sonic, no, it is better, because CURRENT_USER\Software\Classes is the current user equivalent of CLASSES_ROOT and it doesn't require admin privileges. The code is wrong for other reasons. – Kirill Osenkov Jun 29 '17 at 03:39
8

If you write the keys into HKEY_CURRENT_USER\Software\Classes instead of HKEY_CLASSES_ROOT, this should work without administrator privileges under Vista and later.

gammelgul
  • 3,450
  • 1
  • 20
  • 16
4

You are using an old version of Visual Studio, Vista is going to treat your program as a "legacy" Windows app. And redirect the registry writes you make. Include a manifest in your program so you'll look Vista-aware. This manifest is automatically included by VS2008 and up.

Beware that this still won't solve the problem for your user, she's very unlikely to run your app with UAC turned off. You'll need to write a separate app that has a manifest as linked and asks for administrator privileges. It needs the manifest with requestedExecutionLevel set to requireAdministrator.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • I'm unable to add a manifest to the project because the actual exe isn't in my project. Any way to make it appear so I can add a resource? (going add -> existing item and selecting the .exe in the obj folder just copies it) – User2400 Apr 22 '10 at 08:16
  • You are likely to break things if this really is a legacy app. But you can inject a manifest with the mt.exe SDK tool. – Hans Passant Apr 22 '10 at 10:50
4

If you're using Visual Studio 2015 then install the setup and deployment extension. Create a Setup Wizard, and then attach your .exe file to it. Right click your main program in the solution explorer go to -view, -file types, and then right click on the file types and select add new file type. Change all the properties to your needs and then build the MSI installer.

NOTE: I re-read your question and realized that you did not want an installer. Sorry about that, although you should consider using one because it gives you a lot more customization over your program(s).

2

the actual way to associate your file extension with your own programm:

using Microsoft.Win32;
using System;
using System.IO;
using System.Runtime.InteropServices;
 
private static void RegisterForFileExtension(string extension, string applicationPath)
    {
        RegistryKey FileReg = Registry.CurrentUser.CreateSubKey("Software\\Classes\\" + extension);
        FileReg.CreateSubKey("shell\\open\\command").SetValue("", $"\"{applicationPath}\" \"%1\"");
        FileReg.Close();

        SHChangeNotify(0x08000000, 0x0000, IntPtr.Zero, IntPtr.Zero);
    }
[DllImport("shell32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern void SHChangeNotify(uint wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2);

EDIT: Thx, i changed the solution with your suggestion.

  • 1
    Actually, the only answer that really helped me. Be sure to escape your application-path and the parameter, however. Otherwise you'll run into issues when you're dealing with filenames that contain a space: I'd set the value like this instead: ...SetValue("", $"\"{applicationPath}\" \"%1\"") – Alan Oct 19 '22 at 21:08
1

the line

FileReg.CreateSubKey("shell\open\command").SetValue("", applicationPath + " %1");

should be modified to

FileReg.CreateSubKey("shell\open\command").SetValue("", $"\"{applicationPath}\" \"%1\"");

if you don't want have problem with spaces in the path like:

C:\my folder\my file.txt