4

I was trying to create an Excel button in a sheet from a C# code, using a C# event hander for the button, so I don't have to involve VBA and text scripts; and I found this great solution from @ Dummy yoyo that works but "only for a while".

The solution is simple: add a shape, get it as an ole object, get the ole as a button and add a click event:

int i = 1;

private void CreateButton()
{
    Microsoft.Office.Interop.Excel.Worksheet sheet = #get a sheet from Excel

    sheet.Shapes.AddOLEObject("Forms.CommandButton.1", Type.Missing, false, false, Type.Missing, Type.Missing, Type.Missing, 10, 10, 100, 30);
    Microsoft.Office.Interop.Excel.OLEObject oleShape = sheet.OLEObjects(1);
    Microsoft.Vbe.Interop.Forms.CommandButton button = oleShape.Object;
    button.Caption = "Custom Buttom" + i;

    button.Click += Button_Click;
    i++; //for future use, see below
}

private void Button_Click()
{
    MessageBox.Show("It works!");
}

The problem:

Now, the first time I click the button on the sheet, it works! The second time "might" work. The more clicks the less chance of it working. Inevitably, the button "loses" the click event and just does nothing. I can't figure out why, what can be causing this.

I even tried to recreate the button every click with:

private void Button_Click()
{
    MessageBox.Show("It Works!");
    sheet.Shapes.Item(1).Delete();
    CreateButton();
}

I can see the button is being recreated because of the i++ that shows in the button's caption. Even though, some clicks later, even the newly created button simply does nothing when clicked.

What can be going on? How can I solve this?

Edit:

A partial solution was found, as @Hans Passant suggested: if I keep the shape, the ole object and the button as static members outside the method, the garbage collector doesn't kill them. Nevertheless, I would like to save, close and reopen the sheet and still get the events.

Great solutions could be:

  • Identifying why it happens and solving it
  • Suggesting another way of creating the button using a c# click handler
  • Making it possible to save this sheet, close and reopen it with the events still attached.

A few more details:

  • I am creating a new Excel application from C# code and taking the first sheet of the first workbook.
  • This is not a VSTO Addin, I don't have access to the Globals.Factory, although I'd love to be able to get an instance of this factory and try Microsoft.Office.Tools.Worksheet methods to add controls.
    • My project is inevitably a class library consumed by another non-related software
  • The sheet must be saved and reopened later independently from the source project.
  • The sheet is populated before adding the button, also via C# code.
  • Visual Studio debugger is able to step into the button's event handler without any special action
Daniel Möller
  • 84,878
  • 18
  • 192
  • 214
  • 1
    Move oleShape outside of the method so the garbage collector knows not to destroy it too early. – Hans Passant Mar 24 '22 at 17:19
  • @HansPassant, this does work, but the event is lost when I save and reopen the sheet... Well, you answered the original question, so, if you want to add it as an answer, if there is no solution for saving/reopening with events, you can get the bounty. – Daniel Möller Mar 24 '22 at 17:27
  • You can subscribe for the "workbook opened" event and then in the even handler get the button and subscribe to the "click" event for it. – Nikolay Mar 24 '22 at 21:29
  • I can't reproduce your issue with this simple winforms code: https://pastebin.com/raw/GaJS0WxY I always see the messagebox pop. Do you have a full simple reproducing case? Could it be a message pump issue? – Simon Mourier Mar 28 '22 at 09:36
  • 1
    @DanielMöller ??? – MD. RAKIB HASAN Apr 01 '22 at 08:20

2 Answers2

2

Following way every time I click on this button not getting any issue.

        using Excel = Microsoft.Office.Interop.Excel;
        Excel.Application xlApp;
        Excel.Workbook xlWorkBook;
        Excel.Worksheet xlWorkSheet;

        object misValue = System.Reflection.Missing.Value;

        int i = 1;
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {

            xlApp = new Excel.Application();
            xlWorkBook = xlApp.Workbooks.Add(misValue);
            xlWorkSheet = (Excel.Worksheet)xlWorkBook.Worksheets.get_Item(1);

            //add data 
            xlWorkSheet.Cells[1, 1] = "";
            xlWorkSheet.Cells[1, 2] = "Student1";
            xlWorkSheet.Cells[1, 3] = "Student2";
            xlWorkSheet.Cells[1, 4] = "Student3";

            xlWorkSheet.Cells[2, 1] = "Term1";
            xlWorkSheet.Cells[2, 2] = "80";
            xlWorkSheet.Cells[2, 3] = "65";
            xlWorkSheet.Cells[2, 4] = "45";

            xlWorkSheet.Cells[3, 1] = "Term2";
            xlWorkSheet.Cells[3, 2] = "78";
            xlWorkSheet.Cells[3, 3] = "72";
            xlWorkSheet.Cells[3, 4] = "60";
            xlApp.Visible = true;

            #region

            xlWorkSheet.Shapes.AddOLEObject("Forms.CommandButton.1", Type.Missing, false, false, Type.Missing, Type.Missing, Type.Missing, 10, 10, 100, 30);
            Microsoft.Office.Interop.Excel.OLEObject oleShape = xlWorkSheet.OLEObjects(1);
            Microsoft.Vbe.Interop.Forms.CommandButton button = oleShape.Object;
            button.Caption = "Custom Buttom" +i ;

            button.Click += Button_Click;
            i++; 
            #endregion
        }

        private void Button_Click()
        {
            MessageBox.Show("It works!");
        }

enter image description here

but sometime button seems not clickable or not fire click even. see below screenshot.

enter image description here

Message show already, but we do not see it in top of the worksheet. too many time click on the button it may happened. so if you do not see the dialog just open it from taskbar.

enter image description here Reopen: Controls that are added at run time are not persisted when the document or workbook is saved and closed. The exact behavior is different for host controls and Windows Forms controls. In both cases, you can add code to your solution to re-create the controls when the user reopens the document.

           xlApp = new Excel.Application();
            string workbookPath = @"c:/Book.xlsx";
            xlWorkBook = xlApp.Workbooks.Open(workbookPath,
                    0, false, 5, "", "", false, Excel.XlPlatform.xlWindows, "",
                    true, false, 0, true, false, false);

            xlWorkSheet = (Excel.Worksheet)xlWorkBook.Worksheets.get_Item(1);

            xlApp.Visible = true;

            Microsoft.Office.Interop.Excel.OLEObject oleShape = xlWorkSheet.OLEObjects(1);
            Microsoft.Vbe.Interop.Forms.CommandButton button = oleShape.Object;
            button.Caption = "Custom Buttom" + i;

            button.Click += Button_Click;
            i++;

NB: Microsoft Excel 2013, project reference Microsoft office 15.0 object Library and VS 2022 work perfectly but Excel 2016 has this issue you are telling about. so I think this issue has come from Microsoft Office specific version.

MD. RAKIB HASAN
  • 3,670
  • 4
  • 22
  • 35
  • Thank you for the answer. My problem is not hidden messages. @Hans Passant solved my question in the first comment: the garbage collector was killing my objects. As for reopening the sheet, unfortunately I cannot depend on my application for that every time the user needs the sheet. – Daniel Möller Apr 01 '22 at 11:59
  • behavior is different for host controls and Windows Forms controls. In both cases, you can add code to your solution to re-create the controls when the user reopens the document. https://learn.microsoft.com/en-us/visualstudio/vsto/persisting-dynamic-controls-in-office-documents?redirectedfrom=MSDN&view=vs-2022 – MD. RAKIB HASAN Apr 01 '22 at 13:01
0

if you want to make it count clicks try this..

const btn = document.querySelector('.btn');
btn.onclick = Counter;
const clicks = document.querySelector('.clicks');
clicks.id = document.querySelector('clicks');

var a = 0;

function Counter() {
    a += 1;
    clicks.innerHTML = a;
}

const reset = document.querySelector('.reset');
reset.onclick = resetCounter;

function resetCounter() {
    a = 0;
    clicks.innerHTML = a;
}