Archive for November, 2012

Handle shortcut with AppUserModelID in C#

AppUserModelID is, in terms of shortcut, a property of shortcut file which is used for Windows 7 or later. It has some functinalities and especially it is prerequisite for sending a toast notification from desktop app on Windows 8.

On the other hand, no simple method is provided for handling a shortcut file with AppUserModelID on .NET Framework. So, I wrote a code (wrapper class) to create or read a shortcut file containing AppUserModelID.

Developed in C# on .NET Framwork 4.0 with Visual Studio 2012.

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text;

using ComTypes = System.Runtime.InteropServices.ComTypes;

namespace ShellLinkPlus
{
    // Modified from http://smdn.jp/programming/tips/createlnk/
    // Originally from http://www.vbaccelerator.com/home/NET/Code/Libraries/Shell_Projects
    // /Creating_and_Modifying_Shortcuts/article.asp
    // Partly based on Sending toast notifications from desktop apps sample
    public class ShellLink : IDisposable
    {
        #region Win32 and COM

        // IShellLink Interface
        [ComImport,
         InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
         Guid("000214F9-0000-0000-C000-000000000046")]
        private interface IShellLinkW
        {
            uint GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile,
                         int cchMaxPath, ref WIN32_FIND_DATAW pfd, uint fFlags);
            uint GetIDList(out IntPtr ppidl);
            uint SetIDList(IntPtr pidl);
            uint GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName,
                                int cchMaxName);
            uint SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
            uint GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir,
                                     int cchMaxPath);
            uint SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
            uint GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs,
                              int cchMaxPath);
            uint SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
            uint GetHotKey(out ushort pwHotkey);
            uint SetHotKey(ushort wHotKey);
            uint GetShowCmd(out int piShowCmd);
            uint SetShowCmd(int iShowCmd);
            uint GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath,
                                 int cchIconPath, out int piIcon);
            uint SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
            uint SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel,
                                 uint dwReserved);
            uint Resolve(IntPtr hwnd, uint fFlags);
            uint SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
        }

        // ShellLink CoClass (ShellLink object)
        [ComImport,
         ClassInterface(ClassInterfaceType.None),
         Guid("00021401-0000-0000-C000-000000000046")]
        private class CShellLink { }

        // WIN32_FIND_DATAW Structure
        [StructLayout(LayoutKind.Sequential, Pack = 4, CharSet = CharSet.Unicode)]
        private struct WIN32_FIND_DATAW
        {
            public uint dwFileAttributes;
            public ComTypes.FILETIME ftCreationTime;
            public ComTypes.FILETIME ftLastAccessTime;
            public ComTypes.FILETIME ftLastWriteTime;
            public uint nFileSizeHigh;
            public uint nFileSizeLow;
            public uint dwReserved0;
            public uint dwReserved1;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_PATH)]
            public string cFileName;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
            public string cAlternateFileName;
        }

        // IPropertyStore Interface
        [ComImport,
         InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
         Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")]
        private interface IPropertyStore
        {
            uint GetCount([Out] out uint cProps);
            uint GetAt([In] uint iProp, out PropertyKey pkey);
            uint GetValue([In] ref PropertyKey key, [Out] PropVariant pv);
            uint SetValue([In] ref PropertyKey key, [In] PropVariant pv);
            uint Commit();
        }

        // PropertyKey Structure
        // Narrowed down from PropertyKey.cs of Windows API Code Pack 1.1
        [StructLayout(LayoutKind.Sequential, Pack = 4)]
        private struct PropertyKey
        {
            #region Fields

            private Guid formatId;    // Unique GUID for property
            private Int32 propertyId; // Property identifier (PID)

            #endregion

            #region Public Properties

            public Guid FormatId
            {
                get
                {
                    return formatId;
                }
            }

            public Int32 PropertyId
            {
                get
                {
                    return propertyId;
                }
            }

            #endregion

            #region Constructor

            public PropertyKey(Guid formatId, Int32 propertyId)
            {
                this.formatId = formatId;
                this.propertyId = propertyId;
            }

            public PropertyKey(string formatId, Int32 propertyId)
            {
                this.formatId = new Guid(formatId);
                this.propertyId = propertyId;
            }

            #endregion
        }

        // PropVariant Class (only for string value)
        // Narrowed down from PropVariant.cs of Windows API Code Pack 1.1
        // Originally from http://blogs.msdn.com/b/adamroot/archive/2008/04/11
        // /interop-with-propvariants-in-net.aspx
        [StructLayout(LayoutKind.Explicit)]
        private sealed class PropVariant : IDisposable
        {
            #region Fields

            [FieldOffset(0)]
            ushort valueType;     // Value type

            // [FieldOffset(2)]
            // ushort wReserved1; // Reserved field
            // [FieldOffset(4)]
            // ushort wReserved2; // Reserved field
            // [FieldOffset(6)]
            // ushort wReserved3; // Reserved field

            [FieldOffset(8)]
            IntPtr ptr;           // Value

            #endregion

            #region Public Properties

            // Value type (System.Runtime.InteropServices.VarEnum)
            public VarEnum VarType
            {
                get { return (VarEnum)valueType; }
                set { valueType = (ushort)value; }
            }

            // Whether value is empty or null
            public bool IsNullOrEmpty
            {
                get
                {
                    return (valueType == (ushort)VarEnum.VT_EMPTY ||
                            valueType == (ushort)VarEnum.VT_NULL);
                }
            }

            // Value (only for string value)
            public string Value
            {
                get
                {
                    return Marshal.PtrToStringUni(ptr);
                }
            }

            #endregion

            #region Constructor

            public PropVariant()
            { }

            // Construct with string value
            public PropVariant(string value)
            {
                if (value == null)
                    throw new ArgumentException("Failed to set value.");

                valueType = (ushort)VarEnum.VT_LPWSTR;
                ptr = Marshal.StringToCoTaskMemUni(value);
            }

            #endregion

            #region Destructor

            ~PropVariant()
            {
                Dispose();
            }

            public void Dispose()
            {
                PropVariantClear(this);
                GC.SuppressFinalize(this);
            }

            #endregion
        }

        [DllImport("Ole32.dll", PreserveSig = false)]
        private extern static void PropVariantClear([In, Out] PropVariant pvar);

        #endregion

        #region Fields

        private IShellLinkW shellLinkW = null;

        // Name = System.AppUserModel.ID
        // ShellPKey = PKEY_AppUserModel_ID
        // FormatID = 9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3
        // PropID = 5
        // Type = String (VT_LPWSTR)
        private readonly PropertyKey AppUserModelIDKey =
            new PropertyKey("{9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}", 5);

        private const int MAX_PATH = 260;
        private const int INFOTIPSIZE = 1024;

        private const int STGM_READ = 0x00000000;     // STGM constants
        private const uint SLGP_UNCPRIORITY = 0x0002; // SLGP flags

        #endregion

        #region Private Properties (Interfaces)

        private IPersistFile PersistFile
        {
            get
            {
                IPersistFile PersistFile = shellLinkW as IPersistFile;

                if (PersistFile == null)
                    throw new COMException("Failed to create IPersistFile.");
                else
                    return PersistFile;
            }
        }

        private IPropertyStore PropertyStore
        {
            get
            {
                IPropertyStore PropertyStore = shellLinkW as IPropertyStore;

                if (PropertyStore == null)
                    throw new COMException("Failed to create IPropertyStore.");
                else
                    return PropertyStore;
            }
        }

        #endregion

        #region Public Properties (Minimal)

        // Path of loaded shortcut file
        public string ShortcutFile
        {
            get
            {
                string shortcutFile;

                PersistFile.GetCurFile(out shortcutFile);

                return shortcutFile;
            }
        }

        // Path of target file
        public string TargetPath
        {
            get
            {
                // No limitation to length of buffer string in the case of Unicode though.
                StringBuilder targetPath = new StringBuilder(MAX_PATH);

                WIN32_FIND_DATAW data = new WIN32_FIND_DATAW();

                VerifySucceeded(shellLinkW.GetPath(targetPath, targetPath.Capacity, ref data,
                                                   SLGP_UNCPRIORITY));

                return targetPath.ToString();
            }
            set
            {
                VerifySucceeded(shellLinkW.SetPath(value));
            }
        }

        public string Arguments
        {
            get
            {
                // No limitation to length of buffer string in the case of Unicode though.
                StringBuilder arguments = new StringBuilder(INFOTIPSIZE);

                VerifySucceeded(shellLinkW.GetArguments(arguments, arguments.Capacity));

                return arguments.ToString();
            }
            set
            {
                VerifySucceeded(shellLinkW.SetArguments(value));
            }
        }

        // AppUserModelID to be used for Windows 7 or later.
        public string AppUserModelID
        {
            get
            {
                using (PropVariant pv = new PropVariant())
                {
                    VerifySucceeded(PropertyStore.GetValue(AppUserModelIDKey, pv));

                    if (pv.Value == null)
                        return "Null";
                    else
                        return pv.Value;
                }
            }
            set
            {
                using (PropVariant pv = new PropVariant(value))
                {
                    VerifySucceeded(PropertyStore.SetValue(AppUserModelIDKey, pv));
                    VerifySucceeded(PropertyStore.Commit());
                }
            }
        }

        #endregion

        #region Constructor

        public ShellLink()
            : this(null)
        { }

        // Construct with loading shortcut file.
        public ShellLink(string file)
        {
            try
            {
                shellLinkW = (IShellLinkW)new CShellLink();
            }
            catch
            {
                throw new COMException("Failed to create ShellLink object.");
            }

            if (file != null)
                Load(file);
        }

        #endregion

        #region Destructor

        ~ShellLink()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (shellLinkW != null)
            {
                // Release all references.
                Marshal.FinalReleaseComObject(shellLinkW);
                shellLinkW = null;
            }
        }

        #endregion

        #region Methods

        // Save shortcut file.
        public void Save()
        {
            string file = ShortcutFile;

            if (file == null)
                throw new InvalidOperationException("File name is not given.");
            else
                Save(file);
        }

        public void Save(string file)
        {
            if (file == null)
                throw new ArgumentNullException("File name is required.");
            else
                PersistFile.Save(file, true);
        }

        // Load shortcut file.
        public void Load(string file)
        {
            if (!File.Exists(file))
                throw new FileNotFoundException("File is not found.", file);
            else
                PersistFile.Load(file, STGM_READ);
        }

        // Verify if operation succeeded.
        public static void VerifySucceeded(uint hresult)
        {
            if (hresult > 1)
                throw new InvalidOperationException("Failed with HRESULT: " +
                                                    hresult.ToString("X"));
        }

        #endregion
    }
}

Then, this is a demo app using this class.

using System;
using System.IO;
using System.Windows.Forms;

namespace ManageShellLink
{
    public partial class Main : Form
    {
        public Main()
        {
            InitializeComponent();
        }

        private void Main_Load(object sender, EventArgs e)
        {
            // To make a shortcut of Calc.exe on the desktop.
            textBox_ShortcutFile.Text = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "Calc.lnk");
            textBox_TargetPath.Text = "%SystemRoot%\\System32\\calc.exe";
            textBox_Arguments.Text = " ";

            // An AppUserModelID must be in the following form:
            // CompanyName.ProductName.SubProduct.VersionInformation
            // No more than 128 characters and cannot contain spaces.
            // Each section should be camel-cased.
            // CompanyName and ProductName should always be used.
            // From http://msdn.microsoft.com/en-us/library/windows/desktop/dd378459.aspx
            textBox_AppUserModelID.Text = "Microsoft.Windows.Calc";
        }

        private void button_Create_Click(object sender, EventArgs e)
        {
            if (String.IsNullOrWhiteSpace(textBox_ShortcutFile.Text))
            {
                MessageBox.Show("Shortcut File is not provided.", "",
                                MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }

            if (!String.IsNullOrWhiteSpace(textBox_AppUserModelID.Text) &
                ((128 < textBox_AppUserModelID.Text.Length) |
                 textBox_AppUserModelID.Text.Contains(" ")))
            {
                MessageBox.Show("AppUserModelID must be no more than 128 characters " +
                                "and cannot contain spaces.", "",
                                MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }

            // Create shortcut file.
            try
            {
                using (ShellLink shortcut = new ShellLink())
                {
                    shortcut.TargetPath = textBox_TargetPath.Text;
                    shortcut.Arguments = textBox_Arguments.Text;
                    shortcut.AppUserModelID = textBox_AppUserModelID.Text;

                    shortcut.Save(textBox_ShortcutFile.Text);

                    MessageBox.Show("Created shortcut file.", "",
                                    MessageBoxButtons.OK, MessageBoxIcon.Information);
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show("Could not created shortcut file. " + ex.Message, "",
                                MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        private void button_Read_Click(object sender, EventArgs e)
        {
            if (!File.Exists(textBox_ShortcutFile.Text))
            {
                MessageBox.Show("Such shortcut file does not exist.", "",
                                MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }

            // Read shortcut file.
            try
            {
                using (ShellLink shortcut = new ShellLink(textBox_ShortcutFile.Text))
                {
                    textBox_TargetPath.Text = shortcut.TargetPath;
                    textBox_Arguments.Text = shortcut.Arguments;
                    textBox_AppUserModelID.Text = shortcut.AppUserModelID;

                    MessageBox.Show("Red shortcut file.", "",
                                    MessageBoxButtons.OK, MessageBoxIcon.Information);
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show("Could not read shortcut file. " + ex.Message, "",
                                MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }
    }
}

The app looks like this:
ShellLinkPlus

It can create a shortcut file setting target path, arguments and AppUserModelID and read these properties from an existing shortcut file.

Complete source code