Search code examples
vstooffice365excel-interopexcel-2016shared-addin

Sort Excel table (ListObject) on protected sheet using Excel Interop


Sorting Excel tables (ListObjects) is not allowed on protected sheets. You'll see the following error message:

enter image description here

I spent weeks looking for a solution with no success. Everything out there is outdated with Excel 2007 code samples. There are no tutorials or guides on how circumvent this limitation.

Here's how I was able to finally overcome..


Solution

  • There is no trappable event when sorting from the Excel's filter drop-down menu of a table. You can, however, trap the events when an ascending, descending or sort dialog commands are invoked from the Ribbon's Home and Data tabs.

    Using Excel 2016 Interop (document-level customization), Visual Studio 2015 and C#:


    1. Right-click on your project -> Add -> New Item -> Ribbon (XML)

    2. On your Ribbon.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <customUI xmlns="http://schemas.microsoft.com/office/2009/07/customui" onLoad="Ribbon_Load">
      <commands>  
        <command idMso="SortAscendingExcel" onAction="SortNoAlerts" />
        <command idMso="SortDescendingExcel" onAction="SortNoAlerts" />
        <command idMso="SortCustomExcel" onAction="SortDialogNoAlerts" /><!--TabHome-->
        <command idMso="SortDialog" onAction="SortDialogNoAlerts" /><!--TabData-->
      </commands>
    </customUI>
    

    Next, add the events' callback functions. SortNoAlerts unprotects the sheet for ascending / descending button clicks. But if the user chooses 'Custom Sort' (Home tab) - or - 'Sort' (Data tab), a dialog will appear, sure it will unprotect the sheet and protect it right back if OK is pressed, but if the user Cancels, ThisWorkbook_SheetCalculate will never trigger leaving the sheet unprotected. So we add the SortDialogNoAlerts which unprotects the sheet but also starts a timer that uses p/Invoke FindWindow to look for the Sort dialog window. When the Window is no longer found, it protects it if not already protected.

    1. On your Ribbon.cs callbacks:
        public void SortNoAlerts(Office.IRibbonControl control, ref bool cancelDefault)
        {
            Excel.Worksheet ws = null;
            try
            {
                ws = (Excel.Worksheet)Globals.ThisWorkbook.ActiveSheet;
                ws.Unprotect("your password");
                cancelDefault = false;
            }
            catch (Exception) { }
            finally
            {
                if (ws != null) Marshal.ReleaseComObject(ws); ws = null;
            }
        }
    
        public void SortDialogNoAlerts(Office.IRibbonControl control, ref bool cancelDefault)
        {
            Excel.Worksheet ws = null;
            try
            {
                ws = (Excel.Worksheet)Globals.ThisWorkbook.ActiveSheet;
                ws.Unprotect("your password");
                Globals.ThisWorkbook._myActionPane.tmrWaitSortWinClose.Enabled = true;
                cancelDefault = false;
            }
            catch (Exception) {
                Globals.ThisWorkbook._myActionPane.tmrWaitSortWinClose.Enabled = false;
            }
            finally
            {
                if (ws != null) Marshal.ReleaseComObject(ws); ws = null;
            }
        }
    
    1. On ThisWorkbook.cs -> InternalStartup() add this:
    this.SheetCalculate += new Excel.WorkbookEvents_SheetCalculateEventHandler(ThisWorkbook_SheetCalculate);
    
    1. On ThisWorkbook.cs -> add this:
    public bool sortDialogVisible;
    
    private void ThisWorkbook_SheetCalculate(object sh)
    {
      Excel.Worksheet ws = (Excel.Worksheet)sh;
      ws.EnableOutlining = true;
      ws.Protect("your password", true, Type.Missing, Type.Missing, true, true, true, Type.Missing, Type.Missing, true, Type.Missing, Type.Missing, true, true, true, Type.Missing);
      Marshal.ReleaseComObject(ws); ws = null;
    }
    
    1. Add a timer named tmrWaitSortWinClose and set Interval = 750:
    private void tmrWaitSortWinClose_Tick(object sender, EventArgs e)
    {
        Globals.ThisWorkbook.sortDialogVisible = Native.FindWindow("NUIDialog", "Sort") == IntPtr.Zero;
    
        if (Globals.ThisWorkbook.sortDialogVisible)
        {
            Excel.Worksheet ws = null;
            try
            {
                ws = (Excel.Worksheet)Globals.ThisWorkbook.ActiveSheet;
    
                if (!ws.ProtectContents)
                {
                   ws.Protect("your password", true, Type.Missing, Type.Missing, true, true, true, Type.Missing, Type.Missing, true, Type.Missing, Type.Missing, true, true, true, Type.Missing);
                }
                tmrWaitSortWinClose.Enabled = false;
            }
            catch (Exception) { tmrWaitSortWinClose.Enabled = false; }
            finally
            {
                if (ws != null) Marshal.ReleaseComObject(ws); ws = null;
            }
        }
    }
    
    1. Add a class named Native.cs:
    public class Native
    {
        [DllImport("user32.dll", SetLastError = true)]
        public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
    }
    

    That will allow sorting tables on protected sheets. Don't be confused, the AllowSort option of worksheet.Protect() it's only for the cells of the sheet that are not part of a table (ListObject).