Como faço para limpar corretamente os objetos de interoperabilidade do Excel?

747

Estou usando a interoperabilidade do Excel em C # ( ApplicationClass) e coloquei o seguinte código na minha cláusula finally:

while (System.Runtime.InteropServices.Marshal.ReleaseComObject(excelSheet) != 0) { }
excelSheet = null;
GC.Collect();
GC.WaitForPendingFinalizers();

Apesar desse tipo de trabalho, o Excel.exeprocesso ainda está em segundo plano, mesmo depois que eu fecho o Excel. Ele é liberado apenas quando meu aplicativo é fechado manualmente.

O que estou fazendo de errado ou existe uma alternativa para garantir que os objetos de interoperabilidade sejam descartados corretamente?

HAdes
fonte
1
Você está tentando desligar o Excel.exe sem fechar o aplicativo? Não tenho certeza se entendi completamente sua pergunta.
Bryant
3
Estou tentando garantir que os objetos de interoperabilidade não gerenciados sejam descartados corretamente. Para que não haja processos do Excel por aí, mesmo quando o usuário tiver concluído a planilha do Excel que criamos a partir do aplicativo.
HAdes 01/10/08
3
Se você pode tentar fazê-lo através da produção de arquivos XML do Excel, caso contrário, por favor considere VSTO un / Gerenciamento de memória geridos: jake.ginnivan.net/vsto-com-interop
Jeremy Thompson
Isso se traduz bem no Excel?
Coops
2
Consulte (além das respostas abaixo) este artigo de suporte da Microsoft, onde eles fornecem soluções especificamente para esse problema: support.microsoft.com/kb/317109
Arjan

Respostas:

684

O Excel não fecha porque seu aplicativo ainda mantém referências a objetos COM.

Eu acho que você está invocando pelo menos um membro de um objeto COM sem atribuí-lo a uma variável.

Para mim, foi o objeto excelApp.Worksheets que eu usei diretamente sem atribuí-lo a uma variável:

Worksheet sheet = excelApp.Worksheets.Open(...);
...
Marshal.ReleaseComObject(sheet);

Eu não sabia que internamente o C # criava um invólucro para o objeto COM de planilhas que não era liberado pelo meu código (porque eu não sabia disso) e era a causa pela qual o Excel não era descarregado.

Encontrei a solução para o meu problema nesta página , que também possui uma boa regra para o uso de objetos COM em C #:

Nunca use dois pontos com objetos COM.


Portanto, com esse conhecimento, a maneira correta de fazer o acima é:

Worksheets sheets = excelApp.Worksheets; // <-- The important part
Worksheet sheet = sheets.Open(...);
...
Marshal.ReleaseComObject(sheets);
Marshal.ReleaseComObject(sheet);

ATUALIZAÇÃO PÓS-MORTEM:

Quero que todos os leitores leiam essa resposta de Hans Passant com muito cuidado, pois explica a armadilha em que eu e muitos outros desenvolvedores nos deparamos. Quando escrevi essa resposta anos atrás, não sabia o efeito que o depurador causava no coletor de lixo e tirei conclusões erradas. Eu mantenho minha resposta inalterada para o bem da história, mas por favor leia este link e não siga o caminho dos "dois pontos": Entendendo a coleta de lixo no .NET e Limpar objetos de interoperabilidade do Excel com IDisposable

VVS
fonte
16
Sugiro não usar o Excel no COM e salve todos os problemas. Os formatos do Excel 2007 podem ser usados ​​sem abrir o Excel, lindo.
user7116
5
Não entendi o que "dois pontos" significam. Você pode explicar, por favor?
A9S6
22
Isso significa que você não deve usar o padrão comObject.Property.PropertiesProperty (você vê os dois pontos?). Em vez disso, atribua comObject.Property a uma variável e use e descarte essa variável. Uma versão mais formal da regra acima pode ser sth. como "Atribuir objeto com a variáveis ​​antes de usá-los. Isso inclui objetos com que são propriedades de outro objeto com".
VVS
5
@ Nick: Na verdade, você não precisa de nenhum tipo de limpeza, pois o coletor de lixo fará isso por você. A única coisa que você precisa fazer é atribuir cada objeto COM à sua própria variável, para que o GC saiba disso.
VVS
12
@VSS, isso é besteira, o GC limpa tudo, pois as variáveis ​​do wrapper são criadas pela estrutura .net. Pode levar uma eternidade para o GC limpá-lo. Ligar para GC.Collect após uma interoperabilidade pesada não é uma má idéia.
precisa saber é o seguinte
280

Você pode realmente liberar seu objeto Aplicativo do Excel de maneira limpa, mas é necessário ter cuidado.

O conselho para manter uma referência nomeada para absolutamente todos os objetos COM que você acessa e depois liberá-lo explicitamente Marshal.FinalReleaseComObject()é correto em teoria, mas, infelizmente, é muito difícil de gerenciar na prática. Se alguém deslizar para algum lugar e usar "dois pontos" ou iterar células por meio de umfor each iterar loop ou qualquer outro tipo de comando semelhante, você terá objetos COM não referenciados e corre o risco de travar. Nesse caso, não haveria maneira de encontrar a causa no código; você precisaria revisar todo o seu código a olho nu e, esperançosamente, encontrar a causa, uma tarefa que poderia ser quase impossível para um projeto grande.

A boa notícia é que você realmente não precisa manter uma referência de variável nomeada para todos os objetos COM que você usa. Em vez disso, chame GC.Collect()e, em seguida, GC.WaitForPendingFinalizers()libere todos os objetos (geralmente menores) aos quais você não mantém uma referência e libere explicitamente os objetos aos quais você mantém uma referência de variável nomeada.

Você também deve liberar suas referências nomeadas em ordem inversa de importância: alcance primeiro os objetos, depois as planilhas, as pastas de trabalho e, finalmente, o objeto Aplicativo do Excel.

Por exemplo, supondo que você tenha uma variável de objeto Range denominada xlRng, uma variável de planilha denominada xlSheet, uma variável de pasta de trabalho denominada xlBooke uma variável de aplicativo do Excel denominada xlApp, seu código de limpeza poderá ter a seguinte aparência:

// Cleanup
GC.Collect();
GC.WaitForPendingFinalizers();

Marshal.FinalReleaseComObject(xlRng);
Marshal.FinalReleaseComObject(xlSheet);

xlBook.Close(Type.Missing, Type.Missing, Type.Missing);
Marshal.FinalReleaseComObject(xlBook);

xlApp.Quit();
Marshal.FinalReleaseComObject(xlApp);

Na maioria dos exemplos de código que você verá para limpar objetos COM do .NET, as chamadas GC.Collect()e GC.WaitForPendingFinalizers()são feitas DUAS VEZES como em:

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

Entretanto, isso não deve ser necessário, a menos que você esteja usando o VSTO (Visual Studio Tools for Office), que usa finalizadores que fazem com que um gráfico inteiro de objetos seja promovido na fila de finalização. Esses objetos não seriam liberados até a próxima coleta de lixo. No entanto, se você não estiver usando o VSTO, poderá ligar GC.Collect()e GC.WaitForPendingFinalizers()apenas uma vez.

Eu sei que ligar explicitamente GC.Collect()é um não-não (e certamente fazê-lo duas vezes parece muito doloroso), mas não há como contornar isso, para ser honesto. Por meio de operações normais, você gerará objetos ocultos aos quais não mantém nenhuma referência que, portanto, não pode liberar por qualquer outro meio que não seja a chamada GC.Collect().

Este é um tópico complexo, mas isso realmente é tudo o que existe. Depois de estabelecer esse modelo para o procedimento de limpeza, você pode codificar normalmente, sem a necessidade de invólucros, etc. :-)

Eu tenho um tutorial sobre isso aqui:

Automatizando programas do Office com interoperabilidade VB.Net / COM

Ele foi escrito para o VB.NET, mas não se deixe levar por isso, os princípios são exatamente os mesmos que quando se usa C #.

Mike Rosenblum
fonte
3
Uma discussão relacionada pode ser encontrada no fórum do ExtremeVBTalk .NET Office Automation, aqui: xtremevbtalk.com/showthread.php?t=303928 .
Mike Rosenblum
2
E se tudo mais falhar, então Process.Kill () pode ser utilizado (como último recurso), como descrito aqui: stackoverflow.com/questions/51462/...
Mike Rosenblum
1
Que bom que funcionou Richard. :-) E aqui é um exemplo sutil onde simplesmente evitar "dois pontos" não é suficiente para evitar um problema: stackoverflow.com/questions/4964663/...
Mike Rosenblum
Aqui está uma opinião de Misha Shneerson, da Microsoft, sobre o assunto, na data do comentário em 7 de outubro de 2010. ( blogs.msdn.com/b/csharpfaq/archive/2010/09/28/… )
Mike Rosenblum
Espero que isso ajude um problema persistente que estamos tendo. É seguro fazer a coleta de lixo e liberar chamadas dentro de um bloco finalmente?
Brett Green
213

Prefácio: minha resposta contém duas soluções; portanto, tenha cuidado ao ler e não perca nada.

Existem diferentes maneiras e conselhos de como descarregar a instância do Excel, como:

  • Liberando TODOS os objetos com explicitamente com Marshal.FinalReleaseComObject () (sem esquecer os objetos com implicitamente criados). Para liberar todos os objetos com criados, você pode usar a regra de 2 pontos mencionados aqui:
    Como faço para limpar corretamente os objetos de interoperabilidade do Excel?

  • Chamando GC.Collect () e GC.WaitForPendingFinalizers () para fazer com que o CLR libere objetos-com não utilizados * (Na verdade, ele funciona, consulte minha segunda solução para obter detalhes)

  • Verificando se o aplicativo com-servidor talvez mostre uma caixa de mensagem aguardando a resposta do usuário (embora não tenha certeza de que ele possa impedir o Excel de fechar, mas ouvi falar algumas vezes)

  • Enviando mensagem WM_CLOSE para a janela principal do Excel

  • Executando a função que funciona com o Excel em um AppDomain separado. Algumas pessoas acreditam que a instância do Excel será fechada quando o AppDomain for descarregado.

  • Matar todas as instâncias do Excel que foram instanciadas após o início do código de interoperabilidade do Excel.

MAS!Às vezes, todas essas opções simplesmente não ajudam ou não podem ser apropriadas!

Por exemplo, ontem descobri que em uma das minhas funções (que funciona com o Excel) o Excel continua em execução após o término da função. Eu tentei de tudo! Eu verifiquei minuciosamente toda a função 10 vezes e adicionei Marshal.FinalReleaseComObject () para tudo! Eu também tinha GC.Collect () e GC.WaitForPendingFinalizers (). Eu verifiquei as caixas de mensagens ocultas. Tentei enviar a mensagem WM_CLOSE para a janela principal do Excel. Eu executei minha função em um AppDomain separado e descarreguei esse domínio. Nada ajudou! A opção de fechar todas as instâncias do Excel é inadequada, pois se o usuário iniciar outra instância do Excel manualmente, durante a execução da minha função que também funciona com o Excel, essa instância também será fechada pela minha função. Aposto que o usuário não será feliz! Então, honestamente, essa é uma opção esfarrapada (sem ofensas).solução : Mate o processo do excel por hWnd de sua janela principal (é a primeira solução).

Aqui está o código simples:

[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

/// <summary> Tries to find and kill process by hWnd to the main window of the process.</summary>
/// <param name="hWnd">Handle to the main window of the process.</param>
/// <returns>True if process was found and killed. False if process was not found by hWnd or if it could not be killed.</returns>
public static bool TryKillProcessByMainWindowHwnd(int hWnd)
{
    uint processID;
    GetWindowThreadProcessId((IntPtr)hWnd, out processID);
    if(processID == 0) return false;
    try
    {
        Process.GetProcessById((int)processID).Kill();
    }
    catch (ArgumentException)
    {
        return false;
    }
    catch (Win32Exception)
    {
        return false;
    }
    catch (NotSupportedException)
    {
        return false;
    }
    catch (InvalidOperationException)
    {
        return false;
    }
    return true;
}

/// <summary> Finds and kills process by hWnd to the main window of the process.</summary>
/// <param name="hWnd">Handle to the main window of the process.</param>
/// <exception cref="ArgumentException">
/// Thrown when process is not found by the hWnd parameter (the process is not running). 
/// The identifier of the process might be expired.
/// </exception>
/// <exception cref="Win32Exception">See Process.Kill() exceptions documentation.</exception>
/// <exception cref="NotSupportedException">See Process.Kill() exceptions documentation.</exception>
/// <exception cref="InvalidOperationException">See Process.Kill() exceptions documentation.</exception>
public static void KillProcessByMainWindowHwnd(int hWnd)
{
    uint processID;
    GetWindowThreadProcessId((IntPtr)hWnd, out processID);
    if (processID == 0)
        throw new ArgumentException("Process has not been found by the given main window handle.", "hWnd");
    Process.GetProcessById((int)processID).Kill();
}

Como você pode ver, forneci dois métodos, de acordo com o padrão Try-Parse (acho que é apropriado aqui): um método não lança a exceção se o Processo não puder ser eliminado (por exemplo, o processo não existe mais) , e outro método lança a exceção se o Processo não foi morto. O único ponto fraco desse código são as permissões de segurança. Teoricamente, o usuário pode não ter permissões para interromper o processo, mas em 99,99% de todos os casos, o usuário tem essas permissões. Também testei com uma conta de convidado - funciona perfeitamente.

Portanto, seu código, trabalhando com o Excel, pode ficar assim:

int hWnd = xl.Application.Hwnd;
// ...
// here we try to close Excel as usual, with xl.Quit(),
// Marshal.FinalReleaseComObject(xl) and so on
// ...
TryKillProcessByMainWindowHwnd(hWnd);

Voila! Excel é encerrado! :)

Ok, vamos voltar para a segunda solução, como prometi no início do post. A segunda solução é chamar GC.Collect () e GC.WaitForPendingFinalizers (). Sim, eles realmente funcionam, mas você precisa ter cuidado aqui!
Muitas pessoas dizem (e eu disse) que ligar para GC.Collect () não ajuda. Mas a razão pela qual não ajudaria é se ainda houver referências a objetos COM! Um dos motivos mais populares para o GC.Collect () não ser útil é executar o projeto no modo Debug. No modo de depuração, os objetos que realmente não são mais referenciados não serão coletados como lixo até o final do método.
Portanto, se você tentou GC.Collect () e GC.WaitForPendingFinalizers () e não ajudou, tente fazer o seguinte:

1) Tente executar seu projeto no modo Release e verifique se o Excel foi fechado corretamente

2) Enrole o método de trabalho com o Excel em um método separado. Então, em vez de algo como isto:

void GenerateWorkbook(...)
{
  ApplicationClass xl;
  Workbook xlWB;
  try
  {
    xl = ...
    xlWB = xl.Workbooks.Add(...);
    ...
  }
  finally
  {
    ...
    Marshal.ReleaseComObject(xlWB)
    ...
    GC.Collect();
    GC.WaitForPendingFinalizers();
  }
}

você escreve:

void GenerateWorkbook(...)
{
  try
  {
    GenerateWorkbookInternal(...);
  }
  finally
  {
    GC.Collect();
    GC.WaitForPendingFinalizers();
  }
}

private void GenerateWorkbookInternal(...)
{
  ApplicationClass xl;
  Workbook xlWB;
  try
  {
    xl = ...
    xlWB = xl.Workbooks.Add(...);
    ...
  }
  finally
  {
    ...
    Marshal.ReleaseComObject(xlWB)
    ...
  }
}

Agora, o Excel fechará =)

nightcoder
fonte
19
triste que o segmento já é tão velha que a sua excelente resposta parece tão longe abaixo, que eu acho que é a única razão para ele não ser upvoted mais vezes ...
chiccodoro
15
Devo admitir que, quando li sua resposta pela primeira vez, pensei que era um golpe gigante. Após cerca de 6 horas de luta com isso (tudo está liberado, não tenho pontos duplos, etc.), agora acho que sua resposta é genial.
Mark
3
Obrigado por isso. Estive lutando com um Excel que não fechava, não importava o que acontecesse alguns dias antes de eu encontrar isso. Excelente.
BBlake
2
@ nightcoder: resposta incrível, muito detalhada. Seus comentários com relação ao modo de depuração são muito verdadeiros e importantes que você apontou. O mesmo código no modo de liberação geralmente pode ser bom. Process.Kill, no entanto, só é bom ao usar a automação, não quando o programa está sendo executado no Excel, por exemplo, como um suplemento COM gerenciado.
Mike Rosenblum
2
@ DanM: Sua abordagem é 100% correta e nunca falha. Mantendo todas as suas variáveis ​​locais no método, todas as referências são efetivamente inacessíveis pelo código .NET. Isso significa que quando sua seção finalmente chamar GC.Collect (), os finalizadores para todos os seus objetos COM serão chamados com certeza. Cada finalizador chama Marshal.FinalReleaseComObject no objeto que está sendo finalizado. Sua abordagem é, portanto, simples e infalível. Não tenha medo de usar isso. (Apenas ressalva: se estiver usando o VSTO, o que eu duvido que você é, você precisa chamar GC.Collect () & GC.WaitForPendingFinalizers duas vezes.)
Mike Rosenblum
49

UPDATE : Adicionado código C # e link para trabalhos do Windows

Passei algum tempo tentando descobrir esse problema e, na época, o XtremeVBTalk era o mais ativo e responsivo. Aqui está um link para minha postagem original, Fechando um processo de interoperabilidade do Excel de maneira limpa, mesmo se o aplicativo falhar . Abaixo está um resumo da postagem e o código copiado para esta postagem.

  • Fechando o processo Interop com Application.Quit()e Process.Kill()funciona para a maior parte, mas não consegue se os aplicativos falhar catastroficamente. Ou seja, se o aplicativo falhar, o processo do Excel ainda estará solto.
  • A solução é permitir que o sistema operacional lide com a limpeza de seus processos por meio dos Objetos de Trabalho do Windows usando chamadas Win32. Quando seu aplicativo principal morre, os processos associados (por exemplo, o Excel) também são finalizados.

Eu achei que essa era uma solução limpa porque o sistema operacional está realizando um trabalho real de limpeza. Tudo que você precisa fazer é registrar o processo do Excel.

Código de tarefa do Windows

Quebra as chamadas de API do Win32 para registrar processos de interoperabilidade.

public enum JobObjectInfoType
{
    AssociateCompletionPortInformation = 7,
    BasicLimitInformation = 2,
    BasicUIRestrictions = 4,
    EndOfJobTimeInformation = 6,
    ExtendedLimitInformation = 9,
    SecurityLimitInformation = 5,
    GroupInformation = 11
}

[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES
{
    public int nLength;
    public IntPtr lpSecurityDescriptor;
    public int bInheritHandle;
}

[StructLayout(LayoutKind.Sequential)]
struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
    public Int64 PerProcessUserTimeLimit;
    public Int64 PerJobUserTimeLimit;
    public Int16 LimitFlags;
    public UInt32 MinimumWorkingSetSize;
    public UInt32 MaximumWorkingSetSize;
    public Int16 ActiveProcessLimit;
    public Int64 Affinity;
    public Int16 PriorityClass;
    public Int16 SchedulingClass;
}

[StructLayout(LayoutKind.Sequential)]
struct IO_COUNTERS
{
    public UInt64 ReadOperationCount;
    public UInt64 WriteOperationCount;
    public UInt64 OtherOperationCount;
    public UInt64 ReadTransferCount;
    public UInt64 WriteTransferCount;
    public UInt64 OtherTransferCount;
}

[StructLayout(LayoutKind.Sequential)]
struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
    public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
    public IO_COUNTERS IoInfo;
    public UInt32 ProcessMemoryLimit;
    public UInt32 JobMemoryLimit;
    public UInt32 PeakProcessMemoryUsed;
    public UInt32 PeakJobMemoryUsed;
}

public class Job : IDisposable
{
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
    static extern IntPtr CreateJobObject(object a, string lpName);

    [DllImport("kernel32.dll")]
    static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoType infoType, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);

    private IntPtr m_handle;
    private bool m_disposed = false;

    public Job()
    {
        m_handle = CreateJobObject(null, null);

        JOBOBJECT_BASIC_LIMIT_INFORMATION info = new JOBOBJECT_BASIC_LIMIT_INFORMATION();
        info.LimitFlags = 0x2000;

        JOBOBJECT_EXTENDED_LIMIT_INFORMATION extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
        extendedInfo.BasicLimitInformation = info;

        int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
        IntPtr extendedInfoPtr = Marshal.AllocHGlobal(length);
        Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false);

        if (!SetInformationJobObject(m_handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, (uint)length))
            throw new Exception(string.Format("Unable to set information.  Error: {0}", Marshal.GetLastWin32Error()));
    }

    #region IDisposable Members

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

    #endregion

    private void Dispose(bool disposing)
    {
        if (m_disposed)
            return;

        if (disposing) {}

        Close();
        m_disposed = true;
    }

    public void Close()
    {
        Win32.CloseHandle(m_handle);
        m_handle = IntPtr.Zero;
    }

    public bool AddProcess(IntPtr handle)
    {
        return AssignProcessToJobObject(m_handle, handle);
    }

}

Nota sobre o código do construtor

  • No construtor, o info.LimitFlags = 0x2000;é chamado. 0x2000é o JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSEvalor da enumeração e esse valor é definido pelo MSDN como:

Faz com que todos os processos associados ao trabalho sejam finalizados quando o último identificador do trabalho é fechado.

Chamada extra da API do Win32 para obter o ID do processo (PID)

    [DllImport("user32.dll", SetLastError = true)]
    public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

Usando o código

    Excel.Application app = new Excel.ApplicationClass();
    Job job = new Job();
    uint pid = 0;
    Win32.GetWindowThreadProcessId(new IntPtr(app.Hwnd), out pid);
    job.AddProcess(Process.GetProcessById((int)pid).Handle);
Joshgo
fonte
2
Matar explicitamente um servidor COM fora de processo (que pode estar atendendo a outros clientes COM) é uma péssima idéia. Se você está recorrendo a isso, é porque seu protocolo COM está quebrado. Servidores COM não deve ser tratado como uma janela app regulares
MickyD
39

Isso funcionou para um projeto no qual eu estava trabalhando:

excelApp.Quit();
Marshal.ReleaseComObject (excelWB);
Marshal.ReleaseComObject (excelApp);
excelApp = null;

Aprendemos que era importante definir todas as referências a um objeto COM do Excel como nulo quando você terminasse. Isso incluía células, folhas e tudo mais.

Philip Fourie
fonte
30

Qualquer coisa que esteja no espaço para nome do Excel precisa ser liberada. Período

Você não pode estar fazendo:

Worksheet ws = excel.WorkBooks[1].WorkSheets[1];

Você tem que estar fazendo

Workbooks books = excel.WorkBooks;
Workbook book = books[1];
Sheets sheets = book.WorkSheets;
Worksheet ws = sheets[1];

seguido pela liberação dos objetos.

MagicKat
fonte
3
Como sobre xlRange.Interior.Color por exemplo.
HAdes 01/10/08
Interior precisa ser release (a sua no namespace) ... Cor por outro lado não (causa a sua partir System.Drawing.Color, IIRC)
MagicKat
2
na verdade, Cor é uma cor do Excel e não uma cor .Net. você acabou de passar um longo. Também pastas de trabalho def. precisa ser liberado, planilhas ... menos.
Tipo anônimo
Isso não é verdade, a regra de dois pontos ruins e um ponto bom é um absurdo. Além disso, você não precisa liberar pastas de trabalho, planilhas, intervalos, esse é o trabalho do GC. A única coisa que você nunca deve esquecer é chamar Quit no primeiro e único objeto Excel.Application. Após chamar Quit, anule o objeto Excel.Application e chame GC.Collect (); GC.WaitForPendingFinalizers (); duas vezes.
Dietrich Baumgarten
29

Primeiro - você nunca precisa ligar Marshal.ReleaseComObject(...)ou Marshal.FinalReleaseComObject(...)fazer interoperabilidade no Excel. É um anti-padrão confuso, mas qualquer informação sobre isso, inclusive da Microsoft, que indique que você deve liberar manualmente as referências COM do .NET está incorreta. O fato é que o tempo de execução .NET e o coletor de lixo controlam e limpam corretamente as referências COM. Para o seu código, isso significa que você pode remover todo o loop `while (...) na parte superior.

Segundo, se você deseja garantir que as referências COM a um objeto COM fora de processo sejam limpas quando o processo terminar (para que o processo do Excel seja fechado), você deverá garantir que o coletor de lixo seja executado. Você faz isso corretamente com chamadas para GC.Collect()e GC.WaitForPendingFinalizers(). Chamar isso duas vezes é seguro e garante que os ciclos também sejam definitivamente limpos (embora eu não tenha certeza de que seja necessário e que aprecie um exemplo que mostre isso).

Terceiro, quando executadas no depurador, as referências locais serão artificialmente mantidas ativas até o final do método (para que a inspeção de variáveis ​​locais funcione). Portanto, as GC.Collect()chamadas não são eficazes para limpar objetos como rng.Cellsno mesmo método. Você deve dividir o código que está fazendo a interoperabilidade COM da limpeza do GC em métodos separados. (Esta foi uma descoberta importante para mim, de uma parte da resposta postada aqui por @nightcoder.)

O padrão geral seria assim:

Sub WrapperThatCleansUp()

    ' NOTE: Don't call Excel objects in here... 
    '       Debugger would keep alive until end, preventing GC cleanup

    ' Call a separate function that talks to Excel
    DoTheWork()

    ' Now let the GC clean up (twice, to clean up cycles too)
    GC.Collect()    
    GC.WaitForPendingFinalizers()
    GC.Collect()    
    GC.WaitForPendingFinalizers()

End Sub

Sub DoTheWork()
    Dim app As New Microsoft.Office.Interop.Excel.Application
    Dim book As Microsoft.Office.Interop.Excel.Workbook = app.Workbooks.Add()
    Dim worksheet As Microsoft.Office.Interop.Excel.Worksheet = book.Worksheets("Sheet1")
    app.Visible = True
    For i As Integer = 1 To 10
        worksheet.Cells.Range("A" & i).Value = "Hello"
    Next
    book.Save()
    book.Close()
    app.Quit()

    ' NOTE: No calls the Marshal.ReleaseComObject() are ever needed
End Sub

Há muitas informações falsas e confusão sobre esse problema, incluindo muitas postagens no MSDN e no Stack Overflow (e especialmente nesta pergunta!).

O que finalmente me convenceu a olhar mais de perto e descobrir o conselho certo foi o post no blog Marshal.ReleaseComObject Considered Dangerous, além de encontrar o problema com referências mantidas vivas no depurador que estava confundindo meus testes anteriores.

Govert
fonte
5
Virtualmente, TODAS AS RESPOSTAS DESTA PÁGINA ESTÃO ERRADAS, EXCETO ESTA. Eles funcionam, mas são muito trabalhosos. IGNORE a regra "2 PONTOS". Deixe o GC fazer seu trabalho por você. Provas suplementares de .Net GURU Hans Passant: stackoverflow.com/a/25135685/3852958
Alan Baljeu
1
Govert, que alívio encontrar a maneira correta de fazê-lo. Obrigado.
Dietrich Baumgarten
18

Eu encontrei um modelo genérico úteis que podem ajudar a implementar o padrão de descarte correto para objetos COM, essa necessidade Marshal.ReleaseComObject chamado quando eles saem de escopo:

Uso:

using (AutoReleaseComObject<Application> excelApplicationWrapper = new AutoReleaseComObject<Application>(new Application()))
{
    try
    {
        using (AutoReleaseComObject<Workbook> workbookWrapper = new AutoReleaseComObject<Workbook>(excelApplicationWrapper.ComObject.Workbooks.Open(namedRangeBase.FullName, false, false, missing, missing, missing, true, missing, missing, true, missing, missing, missing, missing, missing)))
        {
           // do something with your workbook....
        }
    }
    finally
    {
         excelApplicationWrapper.ComObject.Quit();
    } 
}

Modelo:

public class AutoReleaseComObject<T> : IDisposable
{
    private T m_comObject;
    private bool m_armed = true;
    private bool m_disposed = false;

    public AutoReleaseComObject(T comObject)
    {
        Debug.Assert(comObject != null);
        m_comObject = comObject;
    }

#if DEBUG
    ~AutoReleaseComObject()
    {
        // We should have been disposed using Dispose().
        Debug.WriteLine("Finalize being called, should have been disposed");

        if (this.ComObject != null)
        {
            Debug.WriteLine(string.Format("ComObject was not null:{0}, name:{1}.", this.ComObject, this.ComObjectName));
        }

        //Debug.Assert(false);
    }
#endif

    public T ComObject
    {
        get
        {
            Debug.Assert(!m_disposed);
            return m_comObject;
        }
    }

    private string ComObjectName
    {
        get
        {
            if(this.ComObject is Microsoft.Office.Interop.Excel.Workbook)
            {
                return ((Microsoft.Office.Interop.Excel.Workbook)this.ComObject).Name;
            }

            return null;
        }
    }

    public void Disarm()
    {
        Debug.Assert(!m_disposed);
        m_armed = false;
    }

    #region IDisposable Members

    public void Dispose()
    {
        Dispose(true);
#if DEBUG
        GC.SuppressFinalize(this);
#endif
    }

    #endregion

    protected virtual void Dispose(bool disposing)
    {
        if (!m_disposed)
        {
            if (m_armed)
            {
                int refcnt = 0;
                do
                {
                    refcnt = System.Runtime.InteropServices.Marshal.ReleaseComObject(m_comObject);
                } while (refcnt > 0);

                m_comObject = default(T);
            }

            m_disposed = true;
        }
    }
}

Referência:

http://www.deez.info/sengelha/2005/02/11/useful-idisposable-class-3-autoreleasecomobject/

Edward Wilde
fonte
3
Sim, este é bom. Acho que esse código pode ser atualizado, agora que o FinalReleaseCOMObject está disponível.
Tipo anônimo
Ainda é irritante ter um bloco de uso para cada objeto. Veja aqui stackoverflow.com/questions/2191489/… para uma alternativa.
Henrik
16

Não acredito que esse problema assombre o mundo há cinco anos ... Se você criou um aplicativo, é necessário desligá-lo primeiro antes de remover o link.

objExcel = new Excel.Application();  
objBook = (Excel.Workbook)(objExcel.Workbooks.Add(Type.Missing)); 

ao fechar

objBook.Close(true, Type.Missing, Type.Missing); 
objExcel.Application.Quit();
objExcel.Quit(); 

Quando você cria um aplicativo Excel, ele abre um programa Excel em segundo plano. Você precisa comandar o encerramento do programa do Excel antes de liberar o link, porque esse programa do Excel não faz parte do seu controle direto. Portanto, ele permanecerá aberto se o link for lançado!

Boa programação a todos ~~

Colin
fonte
Não, não é, especialmente se você deseja permitir a interação do usuário com o aplicativo COM (neste caso, o Excel - o OP não especifica se a interação do usuário é pretendida, mas não faz referência ao desligamento). Você só precisa solicitar ao chamador que o usuário possa sair do Excel ao fechar, digamos, um único arquivo.
downwitch
isso funcionou perfeitamente para mim - travou quando eu só tinha objExcel.Quit (), mas quando eu também fiz objExcel.Application.Quit () de antemão, fechei corretamente.
Mcmillab
13

Desenvolvedores comuns, nenhuma de suas soluções funcionou para mim, então eu decido implementar um novo truque .

Primeiro vamos especificar "Qual é o nosso objetivo?" => "Não ver o objeto do Excel após nosso trabalho no gerenciador de tarefas"

Está bem. Não deixe de desafiar e comece a destruí-lo, mas considere não destruir outra instância do Excel que esteja sendo executada em paralelo.

Portanto, obtenha a lista de processadores atuais e obtenha o PID dos processos EXCEL; depois que seu trabalho estiver concluído, teremos um novo convidado na lista de processos com um PID exclusivo, localize e destrua apenas esse.

<lembre-se de que qualquer novo processo do Excel durante o seu trabalho do Excel será detectado como novo e destruído> <Uma solução melhor é capturar o PID do novo objeto criado do Excel e apenas destruí-lo>

Process[] prs = Process.GetProcesses();
List<int> excelPID = new List<int>();
foreach (Process p in prs)
   if (p.ProcessName == "EXCEL")
       excelPID.Add(p.Id);

.... // your job 

prs = Process.GetProcesses();
foreach (Process p in prs)
   if (p.ProcessName == "EXCEL" && !excelPID.Contains(p.Id))
       p.Kill();

Isso resolve o meu problema, espero que o seu também.

Mohsen Afshin
fonte
1
.... esta é uma abordagem um pouco de sledghammer? - é semelhante ao que eu também estou usando :( mas preciso mudar. O problema de usar essa abordagem é que, muitas vezes, ao abrir o Excel, na máquina em que isso foi executado, ele tem o alerta na barra esquerda, dizendo algo como "Excel foi encerrado incorretamente":.. não muito elegante Eu acho que uma das sugestões anteriores neste tópico é preferível
whytheq
11

Isso com certeza parece ter sido muito complicado. De acordo com minha experiência, existem apenas três itens principais para que o Excel seja fechado corretamente:

1: verifique se não há referências restantes para o aplicativo excel que você criou (você só deve ter um de qualquer maneira; defina-o como null)

2: ligar GC.Collect()

3: O Excel deve ser fechado, pelo usuário que fecha o programa manualmente ou por você chamar Quito objeto do Excel. (Observe que Quitfuncionará como se o usuário tentasse fechar o programa e apresentará uma caixa de diálogo de confirmação se houver alterações não salvas, mesmo que o Excel não esteja visível. O usuário pode pressionar cancelar e o Excel não será fechado. )

1 precisa acontecer antes de 2, mas 3 pode acontecer a qualquer momento.

Uma maneira de implementar isso é agrupar o objeto de interoperabilidade do Excel com sua própria classe, criar a instância de interoperabilidade no construtor e implementar IDisposable com Dispose parecido com

if (!mDisposed) {
   mExcel = null;
   GC.Collect();
   mDisposed = true;
}

Isso limpará o Excel do lado das coisas do seu programa. Depois que o Excel for fechado (manualmente pelo usuário ou por você ligando Quit), o processo desaparecerá. Se o programa já foi fechado, o processo desaparecerá na GC.Collect()chamada.

(Não tenho certeza do quanto é importante, mas você pode querer uma GC.WaitForPendingFinalizers()ligação após a GC.Collect()ligação, mas não é estritamente necessário se livrar do processo do Excel.)

Isso funcionou para mim sem problemas por anos. Lembre-se de que, enquanto isso funciona, você realmente precisa fechar normalmente para que funcione. Você ainda acumulará processos excel.exe se interromper o programa antes da limpeza do Excel (geralmente pressionando "stop" enquanto o programa está sendo depurado).

Dave Cousineau
fonte
1
Essa resposta funciona e é infinitamente mais conveniente que a resposta aceita. Não se preocupe com "dois pontos" com objetos COM, sem uso de Marshal.
22615 Andrew.cuthbert
9

Para adicionar aos motivos pelos quais o Excel não fecha, mesmo quando você cria referências diretas para cada objeto na leitura, criação, é o loop 'For'.

For Each objWorkBook As WorkBook in objWorkBooks 'local ref, created from ExcelApp.WorkBooks to avoid the double-dot
   objWorkBook.Close 'or whatever
   FinalReleaseComObject(objWorkBook)
   objWorkBook = Nothing
Next 

'The above does not work, and this is the workaround:

For intCounter As Integer = 1 To mobjExcel_WorkBooks.Count
   Dim objTempWorkBook As Workbook = mobjExcel_WorkBooks.Item(intCounter)
   objTempWorkBook.Saved = True
   objTempWorkBook.Close(False, Type.Missing, Type.Missing)
   FinalReleaseComObject(objTempWorkBook)
   objTempWorkBook = Nothing
Next
Grimfort
fonte
E outro motivo: reutilizar uma referência sem liberar o valor anterior primeiro.
Grimfort
Obrigado. Eu também estava usando um "para cada" sua solução funcionou para mim.
Chad Braun-Duin
+1 Obrigado. Além disso, observe que, se você possui um objeto para um intervalo do Excel e deseja alterar o intervalo durante a vida útil do objeto, descobri que precisava liberar o ReleaseComObject antes de reatribuí-lo, o que torna o código um pouco desarrumado!
AJV JSY
9

Tradicionalmente, segui os conselhos encontrados na resposta do VVS . No entanto, em um esforço para manter essa resposta atualizada com as opções mais recentes, acho que todos os meus projetos futuros usarão a biblioteca "NetOffice".

O NetOffice é um substituto completo para as PIAs do Office e é totalmente independente da versão. É uma coleção de wrappers COM gerenciados que podem lidar com a limpeza que geralmente causa essas dores de cabeça ao trabalhar com o Microsoft Office no .NET.

Alguns recursos principais são:

  • Principalmente independente da versão (e os recursos dependentes da versão estão documentados)
  • Sem dependências
  • Sem PIA
  • Sem registro
  • Sem VSTO

Eu não sou de forma alguma afiliado ao projeto; Eu realmente aprecio genuinamente a redução gritante de dores de cabeça.

BTownTKD
fonte
2
Isso deve ser marcado como a resposta, realmente. O NetOffice abstrai toda essa complexidade.
C. Augusto Proiete
1
Eu uso o NetOffice há bastante tempo escrevendo um suplemento do Excel e funcionou perfeitamente. A única coisa a considerar é que, se você não descarta os objetos usados ​​explicitamente, ele o fará quando você sair do aplicativo (porque ele os rastreia de qualquer maneira). Assim, a regra de ouro com NetOffice é sempre usar "usando" teste padrão com cada objeto Excel como célula, conjunto ou folha etc.
Stas Ivanov
8

A resposta aceita aqui está correta, mas também observe que não apenas as referências de "dois pontos" precisam ser evitadas, mas também objetos recuperados por meio do índice. Você também não precisa esperar até terminar o programa para limpar esses objetos; é melhor criar funções que os limpem assim que possível, quando possível. Aqui está uma função que eu criei que atribui algumas propriedades de um objeto Style chamado xlStyleHeader:

public Excel.Style xlStyleHeader = null;

private void CreateHeaderStyle()
{
    Excel.Styles xlStyles = null;
    Excel.Font xlFont = null;
    Excel.Interior xlInterior = null;
    Excel.Borders xlBorders = null;
    Excel.Border xlBorderBottom = null;

    try
    {
        xlStyles = xlWorkbook.Styles;
        xlStyleHeader = xlStyles.Add("Header", Type.Missing);

        // Text Format
        xlStyleHeader.NumberFormat = "@";

        // Bold
        xlFont = xlStyleHeader.Font;
        xlFont.Bold = true;

        // Light Gray Cell Color
        xlInterior = xlStyleHeader.Interior;
        xlInterior.Color = 12632256;

        // Medium Bottom border
        xlBorders = xlStyleHeader.Borders;
        xlBorderBottom = xlBorders[Excel.XlBordersIndex.xlEdgeBottom];
        xlBorderBottom.Weight = Excel.XlBorderWeight.xlMedium;
    }
    catch (Exception ex)
    {
        throw ex;
    }
    finally
    {
        Release(xlBorderBottom);
        Release(xlBorders);
        Release(xlInterior);
        Release(xlFont);
        Release(xlStyles);
    }
}

private void Release(object obj)
{
    // Errors are ignored per Microsoft's suggestion for this type of function:
    // http://support.microsoft.com/default.aspx/kb/317109
    try
    {
        System.Runtime.InteropServices.Marshal.ReleaseComObject(obj);
    }
    catch { } 
}

Observe que eu precisei definir xlBorders[Excel.XlBordersIndex.xlEdgeBottom]uma variável para limpar isso (não por causa dos dois pontos, que se referem a uma enumeração que não precisa ser liberada, mas porque o objeto ao qual estou me referindo é realmente um objeto Border isso precisa ser liberado).

Esse tipo de coisa não é realmente necessário em aplicativos padrão, que fazem um ótimo trabalho de limpeza após eles mesmos, mas nos aplicativos ASP.NET, se você perder um deles, não importa quantas vezes você chame o coletor de lixo, o Excel irá ainda está sendo executado no seu servidor.

Ele requer muita atenção aos detalhes e muitas execuções de teste enquanto monitora o Gerenciador de Tarefas ao escrever esse código, mas evita o incômodo de procurar desesperadamente pelas páginas de código para encontrar a instância que você perdeu. Isso é especialmente importante ao trabalhar em loops, onde você precisa liberar CADA INSTÂNCIA de um objeto, mesmo que ele use o mesmo nome de variável cada vez que fizer um loop.

Chris McGrath
fonte
Liberação interna, verifique se há Nulo (resposta de Joe). Isso evitará exceções nulas desnecessárias. Eu testei muitos métodos. Essa é a única maneira de liberar efetivamente o Excel.
Gerhard Powell
8

Depois de tentar

  1. Liberar objetos COM na ordem inversa
  2. Adicione GC.Collect()e GC.WaitForPendingFinalizers()duas vezes no final
  3. Não mais que dois pontos
  4. Feche a pasta de trabalho e feche o aplicativo
  5. Executar no modo de liberação

a solução final que funciona para mim é mover um conjunto de

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

que adicionamos ao final da função em um wrapper, da seguinte maneira:

private void FunctionWrapper(string sourcePath, string targetPath)
{
    try
    {
        FunctionThatCallsExcel(sourcePath, targetPath);
    }
    finally
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}
DG
fonte
Eu descobri que não precisava anular o ComObjects, apenas Quit () - Close () e FinalReleaseComObject. Não acredito que isso é tudo o que faltava para fazê-lo funcionar. Ótimo!
Carol
7

Eu segui exatamente isso ... Mas ainda enfrentei os problemas 1 em cada 1000 vezes. Quem sabe o porquê. Hora de trazer o martelo ...

Logo depois que a classe Aplicativo do Excel é instanciada, recebo o processo do Excel que acabou de ser criado.

excel = new Microsoft.Office.Interop.Excel.Application();
var process = Process.GetProcessesByName("EXCEL").OrderByDescending(p => p.StartTime).First();

Depois de concluir toda a limpeza COM acima, verifique se o processo não está em execução. Se ainda estiver em execução, mate-o!

if (!process.HasExited)
   process.Kill();
craig.tadlock
fonte
7

¨ º º¤ø „¸ Atire no Excel e mastigue chiclete ¸„ ø¤º ° ¨

public class MyExcelInteropClass
{
    Excel.Application xlApp;
    Excel.Workbook xlBook;

    public void dothingswithExcel() 
    {
        try { /* Do stuff manipulating cells sheets and workbooks ... */ }
        catch {}
        finally {KillExcelProcess(xlApp);}
    }

    static void KillExcelProcess(Excel.Application xlApp)
    {
        if (xlApp != null)
        {
            int excelProcessId = 0;
            GetWindowThreadProcessId(xlApp.Hwnd, out excelProcessId);
            Process p = Process.GetProcessById(excelProcessId);
            p.Kill();
            xlApp = null;
        }
    }

    [DllImport("user32.dll")]
    static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId);
}
Antoine Meltzheim
fonte
6

Você precisa estar ciente de que o Excel também é muito sensível à cultura em que você está executando.

Você pode achar que precisa definir a cultura como EN-US antes de chamar as funções do Excel. Isso não se aplica a todas as funções - mas a algumas delas.

    CultureInfo en_US = new System.Globalization.CultureInfo("en-US"); 
    System.Threading.Thread.CurrentThread.CurrentCulture = en_US;
    string filePathLocal = _applicationObject.ActiveWorkbook.Path;
    System.Threading.Thread.CurrentThread.CurrentCulture = orgCulture;

Isso se aplica mesmo se você estiver usando o VSTO.

Para detalhes: http://support.microsoft.com/default.aspx?scid=kb;en-us;Q320369


fonte
6

"Nunca use dois pontos com objetos COM" é uma ótima regra geral para evitar vazamentos de referências COM, mas o Excel PIA pode causar vazamentos de mais maneiras do que aparentes à primeira vista.

Uma dessas maneiras é inscrever-se em qualquer evento exposto por qualquer um dos objetos COM do modelo de objeto do Excel.

Por exemplo, assinando o evento WorkbookOpen da classe Application.

Alguma teoria sobre eventos COM

As classes COM expõem um grupo de eventos por meio de interfaces de retorno de chamada. Para se inscrever em eventos, o código do cliente pode simplesmente registrar um objeto implementando a interface de retorno de chamada e a classe COM invocará seus métodos em resposta a eventos específicos. Como a interface de retorno de chamada é uma interface COM, é dever do objeto de implementação diminuir a contagem de referência de qualquer objeto COM que ele recebe (como parâmetro) para qualquer manipulador de eventos.

Como o Excel PIA expõe eventos COM

O Excel PIA expõe eventos COM da classe Aplicativo Excel como eventos .NET convencionais. Sempre que o código do cliente se inscreve em um evento .NET (ênfase em 'a'), o PIA cria uma instância de uma classe implementando a interface de retorno de chamada e a registra no Excel.

Portanto, vários objetos de retorno de chamada são registrados no Excel em resposta a diferentes solicitações de assinatura do código .NET. Um objeto de retorno de chamada por assinatura de evento.

Uma interface de retorno de chamada para manipulação de eventos significa que, o PIA precisa se inscrever em todos os eventos da interface para cada solicitação de inscrição de evento .NET. Não pode escolher e escolher. Ao receber um retorno de chamada de evento, o objeto de retorno de chamada verifica se o manipulador de eventos .NET associado está interessado no evento atual ou não e, em seguida, invoca o manipulador ou ignora silenciosamente o retorno de chamada.

Efeito nas contagens de referência da instância COM

Todos esses objetos de retorno de chamada não diminuem a contagem de referência de nenhum dos objetos COM que eles recebem (como parâmetros) para qualquer um dos métodos de retorno de chamada (mesmo para os que são ignorados silenciosamente). Eles contam exclusivamente com o coletor de lixo CLR para liberar os objetos COM.

Como a execução do GC é não determinística, isso pode levar à suspensão do processo do Excel por um período maior que o desejado e criar uma impressão de um 'vazamento de memória'.

Solução

A única solução a partir de agora é evitar o provedor de eventos da PIA para a classe COM e escrever seu próprio provedor de eventos, que libera deterministicamente objetos COM.

Para a classe Application, isso pode ser feito implementando a interface AppEvents e registrando a implementação no Excel usando a interface IConnectionPointContainer . A classe Application (e todos os objetos COM que expõem eventos usando o mecanismo de retorno de chamada) implementa a interface IConnectionPointContainer.

Amit Mittal
fonte
4

Como outros já apontaram, você precisa criar uma referência explícita para cada objeto do Excel que você usa e chamar Marshal.ReleaseComObject nessa referência, conforme descrito neste artigo da Base de Dados de Conhecimento . Você também precisa usar o try / finalmente para garantir que o ReleaseComObject seja sempre chamado, mesmo quando uma exceção é lançada. Ou seja, em vez de:

Worksheet sheet = excelApp.Worksheets(1)
... do something with sheet

você precisa fazer algo como:

Worksheets sheets = null;
Worksheet sheet = null
try
{ 
    sheets = excelApp.Worksheets;
    sheet = sheets(1);
    ...
}
finally
{
    if (sheets != null) Marshal.ReleaseComObject(sheets);
    if (sheet != null) Marshal.ReleaseComObject(sheet);
}

Você também precisa chamar Application.Quit antes de liberar o objeto Application se desejar que o Excel feche.

Como você pode ver, isso rapidamente se torna extremamente pesado assim que você tenta fazer algo que seja moderadamente complexo. Desenvolvi aplicativos .NET com êxito com uma classe de wrapper simples que envolve algumas manipulações simples do modelo de objeto do Excel (abra uma pasta de trabalho, grave em um Range, salve / feche a pasta de trabalho etc.). A classe de wrapper implementa IDisposable, implementa cuidadosamente Marshal.ReleaseComObject em todos os objetos que usa e não expõe pubicamente nenhum objeto do Excel para o restante do aplicativo.

Mas essa abordagem não é adequada para requisitos mais complexos.

Essa é uma grande deficiência do .NET COM Interop. Para cenários mais complexos, eu consideraria seriamente escrever uma DLL ActiveX no VB6 ou outra linguagem não gerenciada na qual você possa delegar toda a interação com objetos COM de processo externo, como o Office. Em seguida, você pode fazer referência a esta DLL ActiveX do seu aplicativo .NET e as coisas ficarão muito mais fáceis, pois você precisará liberar apenas essa referência.

Joe
fonte
3

Quando todo o material acima não funcionou, tente dar ao Excel algum tempo para fechar suas folhas:

app.workbooks.Close();
Thread.Sleep(500); // adjust, for me it works at around 300+
app.Quit();

...
FinalReleaseComObject(app);
homem Aranha
fonte
2
Eu odeio esperas arbitrárias. Mas marque +1 porque você tem razão em precisar aguardar o fechamento das pastas de trabalho. Uma alternativa é pesquisar a coleção de pastas de trabalho em um loop e usar a espera arbitrária como o tempo limite do loop.
Dflat
3

Certifique-se de liberar todos os objetos relacionados ao Excel!

Passei algumas horas tentando de várias maneiras. Todas são ótimas idéias, mas finalmente encontrei meu erro: se você não liberar todos os objetos, nenhuma das maneiras acima poderá ajudá-lo no meu caso. Certifique-se de liberar todos os objetos, incluindo o alcance um!

Excel.Range rng = (Excel.Range)worksheet.Cells[1, 1];
worksheet.Paste(rng, false);
releaseObject(rng);

As opções estão juntas aqui .

Ned
fonte
Muito bom ponto! Acho que porque estou usando o Office 2007, há algumas limpezas feitas em meu nome. Eu usei o conselho mais adiante, mas não armazenei variáveis, como você sugeriu aqui e o EXCEL.EXE sai, mas eu poderia ter sorte e, se tiver mais algum problema, definitivamente analisarei esta parte do meu código =)
Coops
3

Um ótimo artigo sobre a liberação de objetos COM é o 2.5 Liberando Objetos COM (MSDN).

O método que eu defenderia é anular as referências do Excel.Interop se elas forem variáveis ​​não locais e, em seguida, chamar GC.Collect()e GC.WaitForPendingFinalizers()duas vezes. As variáveis ​​de interoperabilidade com escopo local serão tratadas automaticamente.

Isso elimina a necessidade de manter uma referência nomeada para cada objeto COM.

Aqui está um exemplo retirado do artigo:

public class Test {

    // These instance variables must be nulled or Excel will not quit
    private Excel.Application xl;
    private Excel.Workbook book;

    public void DoSomething()
    {
        xl = new Excel.Application();
        xl.Visible = true;
        book = xl.Workbooks.Add(Type.Missing);

        // These variables are locally scoped, so we need not worry about them.
        // Notice I don't care about using two dots.
        Excel.Range rng = book.Worksheets[1].UsedRange;
    }

    public void CleanUp()
    {
        book = null;
        xl.Quit();
        xl = null;

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

Estas palavras são diretas do artigo:

Em quase todas as situações, anular a referência RCW e forçar uma coleta de lixo será limpa corretamente. Se você também chamar GC.WaitForPendingFinalizers, a coleta de lixo será tão determinística quanto possível. Ou seja, você terá certeza de exatamente quando o objeto foi limpo - no retorno da segunda chamada para WaitForPendingFinalizers. Como alternativa, você pode usar Marshal.ReleaseComObject. No entanto, observe que é improvável que você precise usar esse método.

Porkbutts
fonte
2

A regra dos dois pontos não funcionou para mim. No meu caso, criei um método para limpar meus recursos da seguinte maneira:

private static void Clean()
{
    workBook.Close();
    Marshall.ReleaseComObject(workBook);
    excel.Quit();
    CG.Collect();
    CG.WaitForPendingFinalizers();
}
Hahnemann
fonte
2

Minha solução

[DllImport("user32.dll")]
static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId);

private void GenerateExcel()
{
    var excel = new Microsoft.Office.Interop.Excel.Application();
    int id;
    // Find the Excel Process Id (ath the end, you kill him
    GetWindowThreadProcessId(excel.Hwnd, out id);
    Process excelProcess = Process.GetProcessById(id);

try
{
    // Your code
}
finally
{
    excel.Quit();

    // Kill him !
    excelProcess.Kill();
}
Loart
fonte
2

Você deve ter muito cuidado ao usar aplicativos de interoperabilidade Word / Excel. Depois de experimentar todas as soluções, ainda tínhamos muito processo "WinWord" aberto no servidor (com mais de 2000 usuários).

Depois de trabalhar no problema por horas, percebi que, se eu abrir mais do que alguns documentos usando Word.ApplicationClass.Document.Open()segmentos diferentes simultaneamente, o processo de trabalho do IIS (w3wp.exe) trava, deixando todos os processos do WinWord abertos!

Portanto, acho que não há solução absoluta para esse problema, mas a mudança para outros métodos, como o desenvolvimento do Office Open XML .

Arvand
fonte
1

A resposta aceita não funcionou para mim. O código a seguir no destruidor fez o trabalho.

if (xlApp != null)
{
    xlApp.Workbooks.Close();
    xlApp.Quit();
}

System.Diagnostics.Process[] processArray = System.Diagnostics.Process.GetProcessesByName("EXCEL");
foreach (System.Diagnostics.Process process in processArray)
{
    if (process.MainWindowTitle.Length == 0) { process.Kill(); }
}
Martin
fonte
1

Atualmente, estou trabalhando na automação do Office e encontrei uma solução para isso que funciona sempre para mim. É simples e não envolve matar nenhum processo.

Parece que apenas percorrendo os processos ativos atuais e de alguma forma 'acessando' um processo aberto do Excel, qualquer instância suspensa perdida do Excel será removida. O código abaixo simplesmente verifica processos onde o nome é 'Excel' e, em seguida, grava a propriedade MainWindowTitle do processo em uma cadeia de caracteres. Essa 'interação' com o processo parece fazer o Windows recuperar e abortar a instância congelada do Excel.

Eu executo o método abaixo pouco antes do encerramento do suplemento que estou desenvolvendo, pois ele dispara o evento de descarregamento. Ele remove todas as instâncias suspensas do Excel sempre. Com toda a honestidade, não sei ao certo por que isso funciona, mas funciona bem para mim e pode ser colocado no final de qualquer aplicativo do Excel sem ter que se preocupar com pontos duplos, Marshal.ReleaseComObject, nem processos de extermínio. Eu ficaria muito interessado em alguma sugestão sobre por que isso é eficaz.

public static void SweepExcelProcesses()
{           
            if (Process.GetProcessesByName("EXCEL").Length != 0)
            {
                Process[] processes = Process.GetProcesses();
                foreach (Process process in processes)
                {
                    if (process.ProcessName.ToString() == "excel")
                    {                           
                        string title = process.MainWindowTitle;
                    }
                }
            }
}
Tom Brearley
fonte
1

Eu acho que isso é apenas o modo como a estrutura lida com aplicativos do Office, mas eu posso estar errado. Em alguns dias, alguns aplicativos limpam os processos imediatamente e outros dias parecem esperar até que o aplicativo seja fechado. Em geral, parei de prestar atenção aos detalhes e apenas certifique-se de que não haja nenhum processo extra flutuando no final do dia.

Além disso, e talvez eu esteja simplificando demais as coisas, mas acho que você pode ...

objExcel = new Excel.Application();
objBook = (Excel.Workbook)(objExcel.Workbooks.Add(Type.Missing));
DoSomeStuff(objBook);
SaveTheBook(objBook);
objBook.Close(false, Type.Missing, Type.Missing);
objExcel.Quit();

Como eu disse anteriormente, não costumo prestar atenção nos detalhes de quando o processo do Excel aparece ou desaparece, mas isso geralmente funciona para mim. Também não gosto de manter os processos do Excel por mais tempo do que o mínimo, mas provavelmente estou paranóico nisso.

bill_the_loser
fonte
1

Como alguns provavelmente já escreveram, não é importante apenas como você fecha o Excel (objeto); também é importante como você o abre e também pelo tipo de projeto.

Em um aplicativo WPF, basicamente o mesmo código está funcionando sem ou com muito poucos problemas.

Eu tenho um projeto no qual o mesmo arquivo do Excel está sendo processado várias vezes para diferentes valores de parâmetros - por exemplo, analisando-os com base em valores dentro de uma lista genérica.

Coloquei todas as funções relacionadas ao Excel na classe base e o analisamos em uma subclasse (diferentes analisadores usam funções comuns do Excel). Eu não queria que o Excel fosse aberto e fechado novamente para cada item em uma lista genérica; portanto, eu o abri apenas uma vez na classe base e o fechei na subclasse. Tive problemas ao mover o código para um aplicativo de desktop. Eu tentei muitas das soluções mencionadas acima.GC.Collect()já foi implementado antes, o dobro do sugerido.

Decidi então mover o código para abrir o Excel em uma subclasse. Em vez de abrir apenas uma vez, agora crio um novo objeto (classe base) e abro o Excel para cada item e o fecho no final. Há alguma penalidade no desempenho, mas com base em vários testes, os processos do Excel estão fechando sem problemas (no modo de depuração), portanto, os arquivos temporários são removidos. Continuarei com os testes e escreverei um pouco mais se receber algumas atualizações.

A linha inferior é: Você também deve verificar o código de inicialização, especialmente se você tiver muitas classes, etc.

Blaz Brencic
fonte