автоматизация c # и excel - завершение работающего экземпляра - PullRequest
13 голосов
/ 25 июня 2009

Я пытаюсь автоматизировать Excel через C #. Я следовал всем инструкциям Microsoft о том, как это сделать, но я все еще изо всех сил пытаюсь отбросить окончательную ссылку (и) в Excel, чтобы она закрылась, и чтобы GC мог ее собрать.

Ниже приведен пример кода. Когда я закомментирую блок кода, который содержит строки, похожие на:

Sheet.Cells[iRowCount, 1] = data["fullname"].ToString();

затем файл сохраняется и Excel выходит. В противном случае файл сохраняется, но Excel остается запущенным как процесс. В следующий раз, когда этот код запускается, он создает новый экземпляр, и они в конечном итоге собираются.

Любая помощь приветствуется. Спасибо.

Это скелеты моего кода:

        Excel.Application xl = null;
        Excel._Workbook wBook = null;
        Excel._Worksheet wSheet = null;
        Excel.Range range = null;

        object m_objOpt = System.Reflection.Missing.Value;

        try
        {
            // open the template
            xl = new Excel.Application();
            wBook = (Excel._Workbook)xl.Workbooks.Open(excelTemplatePath + _report.ExcelTemplate, false, false, m_objOpt, m_objOpt, m_objOpt, m_objOpt, m_objOpt, m_objOpt, m_objOpt, m_objOpt, m_objOpt, m_objOpt, m_objOpt, m_objOpt);
            wSheet = (Excel._Worksheet)wBook.ActiveSheet;

            int iRowCount = 2;

            // enumerate and drop the values straight into the Excel file
            while (data.Read())
            {

                wSheet.Cells[iRowCount, 1] = data["fullname"].ToString();
                wSheet.Cells[iRowCount, 2] = data["brand"].ToString();
                wSheet.Cells[iRowCount, 3] = data["agency"].ToString();
                wSheet.Cells[iRowCount, 4] = data["advertiser"].ToString();
                wSheet.Cells[iRowCount, 5] = data["product"].ToString();
                wSheet.Cells[iRowCount, 6] = data["comment"].ToString();
                wSheet.Cells[iRowCount, 7] = data["brief"].ToString();
                wSheet.Cells[iRowCount, 8] = data["responseDate"].ToString();
                wSheet.Cells[iRowCount, 9] = data["share"].ToString();
                wSheet.Cells[iRowCount, 10] = data["status"].ToString();
                wSheet.Cells[iRowCount, 11] = data["startDate"].ToString();
                wSheet.Cells[iRowCount, 12] = data["value"].ToString();

                iRowCount++;
            }

            DirectoryInfo saveTo = Directory.CreateDirectory(excelTemplatePath + _report.FolderGuid.ToString() + "\\");
            _report.ReportLocation = saveTo.FullName + _report.ExcelTemplate;
            wBook.Close(true, _report.ReportLocation, m_objOpt);
            wBook = null;

        }
        catch (Exception ex)
        {
            LogException.HandleException(ex);
        }
        finally
        {
            NAR(wSheet);
            if (wBook != null)
                wBook.Close(false, m_objOpt, m_objOpt);
            NAR(wBook);
            xl.Quit();
            NAR(xl);
            GC.Collect();
        }

private void NAR(object o)
{
    try
    {
        System.Runtime.InteropServices.Marshal.ReleaseComObject(o);
    }
    catch { }
    finally
    {
        o = null;
    }
}

Обновление

Независимо от того, что я пытаюсь, «чистый» метод или «уродливый» метод (см. Ответы ниже), экземпляр Excel все еще зависает, как только эта строка нажата:

wSheet.Cells[iRowCount, 1] = data["fullname"].ToString();

Если я прокомментирую эту строку (и, очевидно, другие похожие ниже), приложение Excel завершит работу изящно. Как только одна строка выше не комментируется, Excel остается без изменений.

Я думаю, мне нужно будет проверить, есть ли работающий экземпляр, прежде чем присваивать переменную xl, и вместо этого подключиться к ней. Я забыл упомянуть, что это служба Windows, но это не должно иметь значения, не так ли?


Ответы [ 14 ]

12 голосов
/ 26 июня 2009

ОБНОВЛЕНИЕ (ноябрь 2016 г.)

Я только что прочитал убедительный аргумент Ханса Пассанта о том, что использование GC.Collect на самом деле правильный путь. Я больше не работаю с Office (слава богу), но если бы я это сделал, я бы, вероятно, хотел бы попробовать еще раз - это, безусловно, упростило бы большую часть (тысячи строк) кода, который я написал, пытаясь сделать все правильно "путь (как я тогда это видел).

Я оставлю свой оригинальный ответ для потомков ...


Как говорит Майк в своем ответе, есть простой и трудный способ справиться с этим. Майк предлагает использовать простой способ, потому что ... это проще. Лично я не верю, что это достаточно веская причина, и я не верю, что это правильный путь. Мне нравится "выключи и включи".

У меня есть многолетний опыт разработки приложений автоматизации Office для .NET, и эти проблемы взаимодействия COM мучили меня в течение первых нескольких недель и месяцев, когда я впервые столкнулся с этой проблемой, не в последнюю очередь потому, что Microsoft очень скромно признает, что есть во-первых, проблема, и в то время в интернете трудно было найти хороший совет.

У меня есть способ работы, который я сейчас использую практически, не задумываясь об этом, и вот уже много лет, как у меня была проблема. По-прежнему важно быть живым со всеми скрытыми объектами, которые вы, возможно, создаете - и да, если вы пропустите один из них, у вас может быть утечка, которая станет очевидной гораздо позже. Но это не хуже, чем было в старые дурные времена malloc / free.

Я действительно думаю, что нужно кое-что сказать для того, чтобы убирать за собой по ходу дела, а не в конце. Если вы только запускаете Excel, чтобы заполнить несколько ячеек, то, возможно, это не имеет значения - но если вы собираетесь делать тяжелую работу, то это другой вопрос.

В любом случае, я использую технику, которая заключается в использовании класса-обертки, который реализует IDisposable и который в своем методе Dispose вызывает ReleaseComObject. Таким образом, я могу использовать операторы using, чтобы гарантировать удаление объекта (и освобождение объекта COM), как только я закончу с ним.

Важно отметить, что он будет удален / освобожден, даже если моя функция вернется рано, или будет исключение и т. Д. Кроме того, он только будет удален / освобожден, если он был фактически создан в первом место - назовите меня педантом, но предлагаемый код, который пытается выпустить объекты, которые, возможно, на самом деле не были созданы, выглядит для меня как неаккуратный код. У меня есть подобное возражение против использования FinalReleaseComObject - вы должны знать, сколько раз вы вызывали создание ссылки COM, и поэтому должны иметь возможность выпускать ее такое же количество раз.

Типичный фрагмент моего кода может выглядеть так (или будет, если бы я использовал C # v2 и мог бы использовать обобщенные значения: -)):

using (ComWrapper<Excel.Application> application = new ComWrapper<Excel.Application>(new Excel.Application()))
{
  try
  {
    using (ComWrapper<Excel.Workbooks> workbooks = new ComWrapper<Excel.Workbooks>(application.ComObject.Workbooks))
    {
      using (ComWrapper<Excel.Workbook> workbook = new ComWrapper<Excel.Workbook>(workbooks.ComObject.Open(...)))
      {
        using (ComWrapper<Excel.Worksheet> worksheet = new ComWrapper<Excel.Worksheet>(workbook.ComObject.ActiveSheet))
        {
          FillTheWorksheet(worksheet);
        }
        // Close the workbook here (see edit 2 below)
      }
    }
  }
  finally
  {
    application.ComObject.Quit();
  }
}

Теперь я не собираюсь притворяться, что это не многословно, и отступ, вызванный созданием объекта , может выйти из-под контроля, если вы не разделите вещи на более мелкие методы. Этот пример - худший случай, так как все, что мы делаем, - это создание объектов. Обычно между скобками происходит гораздо больше, а накладные расходы намного меньше.

Обратите внимание, что в соответствии с приведенным выше примером я всегда передавал «обернутые» объекты между методами, а не голым COM-объектом, и вызывающий объект должен был бы распорядиться им (обычно с помощью оператора using) , Точно так же я всегда возвращал бы завернутый объект, а не голый, и опять же вызывающая сторона должна была бы освободить его. Вы можете использовать другой протокол, но важно иметь четкие правила, как это было, когда мы привыкли делать свое собственное управление памятью.

Класс ComWrapper<T>, используемый здесь, надеюсь, не требует особых пояснений. Он просто сохраняет ссылку на обернутый COM-объект и освобождает его явно (используя ReleaseComObject) в своем методе Dispose. Метод ComObject просто возвращает типизированную ссылку на обернутый COM-объект.

Надеюсь, это поможет!

EDIT : я только сейчас перешел по ссылке на ответ Майка на другой вопрос , и я вижу, что в другом ответе на этот вопрос есть ссылка на класс-оболочку так же, как я предлагаю выше.

Кроме того, что касается ответа Майка на этот другой вопрос, я должен сказать, что меня почти соблазнил аргумент «просто используйте GC.Collect». Тем не менее, я был в основном привлечен к этому на ложной предпосылке; На первый взгляд все выглядело так, как будто бы вообще не нужно беспокоиться о ссылках на COM. Однако, как говорит Майк, вам все равно нужно явно освобождать COM-объекты, связанные со всеми вашими переменными в области видимости - и поэтому все, что вы сделали, это уменьшили, а не удалили необходимость в управлении COM-объектами. Лично я бы предпочел пройти всю свинью.

Я также отмечаю тенденцию во многих ответах писать код, где все освобождается в конце метода, в большом блоке ReleaseComObject вызовов. Это все очень хорошо, если все работает как запланировано, но я призываю любого, кто пишет серьезный код, подумать, что произойдет, если возникнет исключение или если у метода будет несколько точек выхода (код не будет выполнен, и, следовательно, объекты COM). не будет выпущен). Вот почему я предпочитаю использовать «обертки» и using s. Это многословно, но делает пуленепробиваемый код.

EDIT2 : я обновил код выше, чтобы указать, где должна быть закрыта рабочая книга с сохранением изменений или без них. Вот код для сохранения изменений:

object saveChanges = Excel.XlSaveAction.xlSaveChanges;

workbook.ComObject.Close(saveChanges, Type.Missing, Type.Missing);

... и не сохранить изменения, просто измените xlSaveChanges на xlDoNotSaveChanges.

6 голосов
/ 25 июня 2009

Происходит то, что ваш звонок:

Sheet.Cells[iRowCount, 1] = data["fullname"].ToString();

По сути то же самое, что и

Excel.Range cell = Sheet.Cells[iRowCount, 1];
cell.Value = data["fullname"].ToString();

Делая это таким образом, вы можете видеть, что вы создаете объект Excel.Range, а затем присваиваете ему значение. Этот способ также дает нам именованную ссылку на нашу переменную диапазона, переменную cell, которая позволяет нам освобождать ее напрямую, если мы хотим. Таким образом, вы можете очистить ваши объекты одним из двух способов:

(1) Сложный и безобразный путь:

while (data.Read())
{
    Excel.Range cell = Sheet.Cells[iRowCount, 1];
    cell.Value = data["fullname"].ToString();
    Marshal.FinalReleaseComObject(cell);

    cell = Sheet.Cells[iRowCount, 2];
    cell.Value = data["brand"].ToString();
    Marshal.FinalReleaseComObject(cell);

    cell = Sheet.Cells[iRowCount, 3];
    cell.Value = data["agency"].ToString();
    Marshal.FinalReleaseComObject(cell);

    // etc...
}

В приведенном выше примере мы освобождаем каждый объект диапазона посредством вызова Marshal.FinalReleaseComObject(cell) по мере продвижения.

(2) Простой и чистый способ:

Оставьте свой код в точности так, как он у вас есть, и в конце вы можете очистить его следующим образом:

GC.Collect();
GC.WaitForPendingFinalizers();

if (wSheet != null)
{
    Marshal.FinalReleaseComObject(wSheet)
}
if (wBook != null)
{
    wBook.Close(false, m_objOpt, m_objOpt);
    Marshal.FinalReleaseComObject(wBook);
}
xl.Quit();
Marshal.FinalReleaseComObject(xl);

Короче говоря, ваш существующий код очень близко . Если вы просто добавляете вызовы в GC.Collect () и GC.WaitForPendingFinalizers () до ваших вызовов 'NAR', я думаю, что это должно сработать для вас. (Короче говоря, и код Джейми, и код Ахмада верны. Код Джейми чище, но код Ахмада является более простым «быстрым решением» для вас, потому что вам нужно будет только добавить вызовы к вызовам в GC.Collect () и GC.WaitForPendingFinalizers () к существующему коду.)

Джейми и Амхад также перечислили ссылки на .NET Automation Forum , в котором я участвую (спасибо, ребята!) Вот пара похожих постов, которые я сделал здесь на StackOverflow :

(1) Как правильно очистить объекты взаимодействия Excel в C #

(2) C # Автоматизация PowerPoint Excel - PowerPoint не завершает работу

Надеюсь, это поможет, Шон ...

Mike

2 голосов
/ 09 ноября 2010

Поскольку другие уже рассмотрели InterOp, я бы посоветовал, что если вы работаете с файлами Excel с расширением XLSX, вы должны использовать EPPlus , что сделает ваши кошмары Excel исчезающими.

2 голосов
/ 25 июня 2009

Добавьте следующее перед вызовом xl.Quit ():

GC.Collect(); 
GC.WaitForPendingFinalizers(); 

Вы также можете использовать Marshal.FinalReleaseComObject() в своем методе NAR вместо ReleaseComObject. ReleaseComObject уменьшает счетчик ссылок на 1, в то время как FinalReleaseComObject освобождает все ссылки, поэтому счетчик равен 0.

Итак, ваш блок finally будет выглядеть так:

finally
{
    GC.Collect(); 
    GC.WaitForPendingFinalizers(); 

    NAR(wSheet);
    if (wBook != null)
        wBook.Close(false, m_objOpt, m_objOpt);
    NAR(wBook);
    xl.Quit();
    NAR(xl);
}

Обновлен метод NAR:

private void NAR(object o)
{
    try
    {
        System.Runtime.InteropServices.Marshal.FinalReleaseComObject(o);
    }
    catch { }
    finally
    {
        o = null;
    }
}

Я исследовал это некоторое время назад, и в примерах я обнаружил, что звонки, связанные с GC, заканчивались после закрытия приложения. Тем не менее, есть MVP (Майк Розенблюм), который упоминает, что он должен быть вызван в начале. Я пробовал оба пути, и они сработали. Я также попробовал это без WaitForPendingFinalizers, и это работало, хотя это не должно повредить ничему. YMMV.

Вот соответствующие ссылки от MVP, о которых я упоминал (они в VB, но они не такие разные):

1 голос
/ 18 декабря 2013

Прошло более 4 лет с тех пор, как это было опубликовано, но я столкнулся с той же проблемой и смог ее решить. Очевидно, что простой доступ к массиву Cells создает COM-объект. Так что, если вы должны были сделать:

    wSheet = (Excel._Worksheet)wBook.ActiveSheet;
    Microsoft.Office.Interop.Excel.Range cells = wSheet.Cells;

    int iRowCount = 2;

    // enumerate and drop the values straight into the Excel file
    while (data.Read())
    {
        Microsoft.Office.Interop.Excel.Range cell = cells[iRowCount, 1];
        cell  = data["fullname"].ToString();
        Marshal.FinalReleaseComObject(cell);
    }
    Marshal.FinalReleaseComObject(cells);

и затем остальная часть вашей очистки должна решить проблему.

1 голос
/ 12 декабря 2009

Я только что ответил на этот вопрос здесь:

Завершение процесса Excel в главном окне hWnd

0 голосов
/ 09 ноября 2010

У меня была похожая проблема. Я удалил _worksheet и _workbook, и все было хорошо.

0 голосов
/ 13 июля 2010

Нет необходимости использовать объекты Excel com из C #. Вы можете использовать OleDb для изменения листов.

http://www.codeproject.com/KB/office/excel_using_oledb.aspx

0 голосов
/ 01 июля 2009

Шон,

Я собираюсь повторно опубликовать ваш код с моими изменениями (ниже). Я избегал слишком частого изменения кода, поэтому не добавил обработки исключений и т. Д. Этот код не надежный.

private bool GenerateDailyProposalsReport(ScheduledReport report)
{
    Excel.Application xl = null;
    Excel.Workbooks wBooks = null;
    Excel.Workbook wBook = null;
    Excel.Worksheet wSheet = null;
    Excel.Range xlrange = null;
    Excel.Range xlcell = null;

    xl = new Excel.Application();

    wBooks = xl.Workbooks;

    wBook = wBooks.Open(@"DailyProposalReport.xls", false, false, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing);

    wSheet = wBook.ActiveSheet;

    xlrange = wSheet.Cells;

    xlcell = xlrange[2, 1] as Excel.Range;

    xlcell.Value2 = "fullname";

    Marshal.ReleaseComObject(xlcell);
    Marshal.ReleaseComObject(xlrange);
    Marshal.ReleaseComObject(wSheet);

    wBook.Close(true, @"c:\temp\DailyProposalReport.xls", Type.Missing);

    Marshal.ReleaseComObject(wBook);
    Marshal.ReleaseComObject(wBooks);

    xl.Quit();

    Marshal.ReleaseComObject(xl);

    return true;
}

Примечания:

  1. Метод Workbooks Application класс создает Workbooks объект, который содержит ссылка на соответствующий COM объект, поэтому мы должны убедиться, что мы впоследствии выпустить эту ссылку, вот почему я добавил переменную wBooks и соответствующий вызов ReleaseComObject.

  2. Аналогично, метод Cells Worksheet объект возвращает Range объект с другой ссылкой COM, поэтому мы тоже должны это почистить. Отсюда необходимость в 2 отдельных Range переменных.

  3. Я выпустил ссылки на COM (используя ReleaseComObject) как только поскольку они больше не нужны, которые я думаю, это хорошая практика, даже если это не обязательно Также, (и это может быть суеверие) я выпустил все объекты, принадлежащие Рабочая тетрадь перед закрытием Рабочая тетрадь и выпустила рабочую тетрадь до закрытия Excel.

  4. Я не звоню GC.Collect и т. Д. потому что это не должно быть необходимо. Действительно!

  5. Я скорее использую ReleaseComObject чем FinalReleaseComObject, потому что этого должно быть вполне достаточно.

  6. Я не обнуляю переменные после использование; еще раз, это не делает что-нибудь стоящее.

  7. Не имеет значения здесь, но я использую Type.Missing вместо System.Reflection.Missing.Value для удобство. Ролл на C # v4, где необязательные параметры будут поддерживается компилятором!

Я не смог скомпилировать или запустить этот код, но я уверен, что он будет работать Удачи!

0 голосов
/ 01 июля 2009

Боюсь, у меня кончаются идеи, Шон. : - (

У Гэри могут быть какие-то мысли, но, хотя его подход обертки очень твердый, он на самом деле не поможет вам в этом случае, потому что вы уже все делаете в значительной степени правильно.

Я перечислю здесь несколько мыслей. Я не понимаю, как на самом деле будет работать любой из них, потому что ваша загадочная строка

xlrange.Value2 = "fullname";

ни одна из этих идей, похоже, не будет затронута, но здесь говорится:

(1) Не используйте интерфейсы _Workbook и _Worksheet. Вместо этого используйте Workbook и Worksheet. (Подробнее об этом см .: Взаимодействие Excel: _Worksheet или Worksheet? .)

(2) Каждый раз, когда у вас есть две точки (".") В одной строке при доступе к объекту Excel, разбейте его на две строки, назначив каждый объект именованной переменной. Затем в разделе очистки вашего кода явно освободите каждую переменную с помощью Marshal.FinalReleaseComObject ().

Например, ваш код здесь:

 wBook = (Excel._Workbook)xl.Workbooks.Open(@"E:\Development\Romain\APN\SalesLinkReportManager\ExcelTemplates\DailyProposalReport.xls", false, false, m_objOpt, m_objOpt, m_objOpt, m_objOpt, m_objOpt, m_objOpt, m_objOpt, m_objOpt, m_objOpt, m_objOpt, m_objOpt, m_objOpt);

можно разбить на:

Excel.Workbooks wBooks = xl.Workbooks;
wBook = wBooks.Open("@"E:\Development\...\DailyProposalReport.xls", etc...);

А потом, в разделе очистки, вы получите:

Marshal.FinalReleaseComObject(xlrange);
Marshal.FinalReleaseComObject(wSheet);
Marshal.FinalReleaseComObject(wBook);
Marshal.FinalReleaseComObject(wBooks); // <-- Added
Marshal.FinalReleaseComObject(xl);

(3) Я не уверен, что происходит с вашим подходом Process.Kill. Если вы вызываете wBook.Close (), а затем xl.Quit () перед вызовом Process.Kill (), у вас не должно быть проблем. Workbook.Close () не возвращает выполнение до тех пор, пока книга не будет закрыта, а Excel.Quit () не вернет выполнение до тех пор, пока Excel не завершит работу (хотя он все еще может зависать).

После вызова Process.Kill () вы можете проверить свойство Process.HasExited в цикле или, что еще лучше, вызвать метод Process.WaitForExit (), который будет приостанавливаться до тех пор, пока он не завершится для вас. Я предполагаю, что на это обычно уйдет меньше секунды. Лучше подождать меньше времени и быть уверенным, чем ждать 5 - 10 секунд и только догадываться.

(4) Вы должны попробовать эти идеи по очистке, которые я перечислил выше, но я начинаю подозревать, что у вас могут возникнуть проблемы с другими процессами, которые могут работать с Excel, такими как надстройка или антивирус. вирусная программа. Эти надстройки могут привести к зависанию Excel, если они выполнены неправильно. Если это произойдет, может быть очень трудно или невозможно заставить Excel выпустить. Вам нужно будет найти программу-нарушителя и затем отключить ее. Другая возможность заключается в том, что работа в качестве службы Windows как-то является проблемой. Я не понимаю, почему это так, но у меня нет опыта автоматизации Excel через службу Windows, поэтому я не могу сказать. Если ваши проблемы связаны с этим, то использование Process.Kill, скорее всего, будет вашим единственным средством здесь.

Это все, что я могу вспомнить, Шон. Надеюсь, это поможет. Дайте нам знать, как это происходит ...

- Майк

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...