Jak prawidłowo wyczyścić obiekty międzyoperacyjne Excela?


747

Korzystam z interfejsu Excela w C # ( ApplicationClass) i umieściłem następujący kod w mojej klauzuli w końcu:

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

Chociaż ten rodzaj działa, Excel.exeproces jest nadal w tle, nawet po zamknięciu programu Excel. Wydawany jest dopiero po ręcznym zamknięciu mojej aplikacji.

Co robię źle lub czy istnieje alternatywa dla zapewnienia, że ​​obiekty międzyoperacyjne są odpowiednio usuwane?


1
Czy próbujesz zamknąć program Excel.exe bez zamykania aplikacji? Nie jestem pewien, czy w pełni rozumiem twoje pytanie.
Bryant

3
Próbuję się upewnić, że niezarządzane obiekty międzyoperacyjne są odpowiednio usuwane. Aby procesy Excel nie były zawieszone, nawet gdy użytkownik skończył z arkuszem kalkulacyjnym Excel, który utworzyliśmy z aplikacji.
HAdes

3
Jeśli możesz spróbować to zrobić, tworząc pliki XML Excel, w przeciwnym razie rozważ zarządzanie VSTO niezarządzaną
Jeremy Thompson

Czy to ładnie przekłada się na Excela?
Coops

2
Zobacz (oprócz odpowiedzi poniżej) ten artykuł pomocy technicznej firmy Microsoft, w którym konkretnie dają rozwiązania tego problemu: support.microsoft.com/kb/317109
Arjan

Odpowiedzi:


684

Program Excel nie jest zamykany, ponieważ aplikacja nadal przechowuje odwołania do obiektów COM.

Myślę, że wywołujesz przynajmniej jednego członka obiektu COM bez przypisywania go do zmiennej.

Dla mnie był to obiekt excelApp.Worksheets, którego bezpośrednio użyłem bez przypisywania go do zmiennej:

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

Nie wiedziałem, że wewnętrznie C # stworzył opakowanie dla obiektu COM arkusza roboczego, który nie został zwolniony przez mój kod (ponieważ nie byłem tego świadomy) i był przyczyną, dla której Excel nie został zwolniony.

Znalazłem rozwiązanie mojego problemu na tej stronie , która ma również niezłą zasadę używania obiektów COM w C #:

Nigdy nie używaj dwóch kropek do obiektów COM.


Tak więc przy tej wiedzy właściwy sposób na wykonanie powyższych czynności jest następujący:

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

AKTUALIZACJA MORTEMU POST:

Chcę, aby każdy czytelnik bardzo uważnie przeczytał tę odpowiedź Hansa Passanta, ponieważ wyjaśnia ona pułapkę, w którą wpadłem i wielu innych programistów. Kiedy pisałem tę odpowiedź wiele lat temu, nie wiedziałem o wpływie debugera na śmietnik i wyciągnąłem błędne wnioski. Zachowuję swoją odpowiedź bez zmian ze względu na historię, ale proszę przeczytać ten link i nie przejść do „dwóch kropek”: Zrozumienie wyrzucania elementów bezużytecznych w .NET i Oczyszczanie obiektów Excel Interop z IDisposable


16
Następnie sugeruję, aby nie używać Excela z COM i zaoszczędzić sobie wszystkich kłopotów. Formaty Excel 2007 mogą być używane bez otwierania Excela, wspaniale.
user7116,

5
Nie zrozumiałem, co oznaczają „dwie kropki”. Czy możesz wyjaśnić?
A9S6

22
Oznacza to, że nie powinieneś używać wzoru comObject.Property.PropertiesProperty (widzisz dwie kropki?). Zamiast tego przypisz comObject.Property do zmiennej oraz używaj i usuwaj tę zmienną. Bardziej formalną wersją powyższej zasady może być coś. np. „Przypisz obiekt com do zmiennych przed ich użyciem. Obejmuje to obiekty com, które są właściwościami innego obiektu com”.
VVS

5
@Nick: Właściwie nie potrzebujesz żadnego czyszczenia, ponieważ śmieciarz zrobi to za ciebie. Jedyne, co musisz zrobić, to przypisać każdy obiekt COM do własnej zmiennej, aby GC o tym wiedział.
VVS

12
@VSS czyli bollocks, GC czyści wszystko, ponieważ zmienne otoki są tworzone przez platformę .net. GC może to potrwać wieczność. Dzwonienie do GC.Collect po ciężkim interopie nie jest złym idearem.
CodingBarfield

280

Możesz faktycznie zwolnić obiekt aplikacji Excel w czysty sposób, ale musisz uważać.

Porada, aby zachować nazwane odniesienie dla absolutnie każdego obiektu COM, do którego masz dostęp, a następnie jawnie go zwolnić, Marshal.FinalReleaseComObject()jest teoretycznie poprawna, ale niestety bardzo trudna do zarządzania w praktyce. Jeśli ktoś poślizgnie się gdziekolwiek i użyje „dwóch kropek” lub iteruje komórki za pomocą for eachpętli lub innego podobnego polecenia, wówczas będziesz mieć niepowiązane obiekty COM i ryzykujesz zawieszenie. W takim przypadku nie byłoby sposobu na znalezienie przyczyny w kodzie; będziesz musiał przejrzeć cały kod naocznie i, miejmy nadzieję, znaleźć przyczynę, zadanie, które może być prawie niemożliwe w przypadku dużego projektu.

Dobrą wiadomością jest to, że tak naprawdę nie musisz utrzymywać nazwanego odwołania do zmiennej do każdego używanego obiektu COM. Zamiast tego należy wywołać, GC.Collect()a następnie GC.WaitForPendingFinalizers()zwolnić wszystkie (zwykle drobne) obiekty, do których nie ma się odwołania, a następnie jawnie zwolnić obiekty, do których należy nazwane odwołanie do zmiennej.

Powinieneś również zwolnić swoje nazwane odwołania w odwrotnej kolejności ważności: najpierw obiekty zakresu, potem arkusze, skoroszyty, a na końcu obiekt aplikacji Excel.

Na przykład, zakładając, że masz nazwaną zmienną obiektu Range, nazwaną zmienną xlRngarkusza xlSheetroboczego, nazwaną zmienną skoroszytu xlBooki nazwaną zmienną aplikacji Excel xlApp, wówczas kod czyszczenia może wyglądać następująco:

// 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);

W większości przykładów kodu zobaczysz do czyszczenia obiektów COM z .NET, GC.Collect()a GC.WaitForPendingFinalizers()wywołania i są wykonywane DWUKROTNIE jak w:

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

Nie powinno to być jednak wymagane, chyba że używasz programu Visual Studio Tools for Office (VSTO), który korzysta z finalizatorów, które powodują awans całego wykresu obiektów w kolejce finalizacji. Takie obiekty nie zostaną zwolnione do następnego wyrzucania elementów bezużytecznych. Jeśli jednak nie używasz VSTO, możesz zadzwonić GC.Collect()i GC.WaitForPendingFinalizers()tylko raz.

Wiem, że bezpośrednie dzwonienie GC.Collect()jest nie-nie (a na pewno robienie tego dwukrotnie brzmi bardzo bolesnie), ale szczerze mówiąc, nie można tego obejść. Poprzez normalne operacje wygenerujesz ukryte obiekty, do których nie masz żadnego odniesienia, dlatego nie możesz ich uwolnić w żaden inny sposób niż wywołanie GC.Collect().

To złożony temat, ale tak naprawdę to wszystko. Po ustaleniu tego szablonu dla procedury czyszczenia możesz normalnie kodować, bez potrzeby owijania itp .:-)

Mam tutaj poradnik na ten temat:

Automatyzacja programów biurowych za pomocą VB.Net / COM Interop

Jest napisany dla VB.NET, ale nie zniechęcaj się, zasady są dokładnie takie same jak przy użyciu C #.


3
Powiązaną dyskusję można znaleźć na forum ExtremeVBTalk .NET Office Automation, tutaj: xtremevbtalk.com/showthread.php?t=303928 .
Mike Rosenblum

2
A jeśli wszystko inne zawiedzie, wówczas można użyć Process.Kill () (w ostateczności), jak opisano tutaj: stackoverflow.com/questions/51462/…
Mike Rosenblum

1
Cieszę się, że zadziałało Richard. :-) A oto subtelny przykład, w którym po prostu uniknięcie „dwóch kropek” nie jest wystarczające, aby zapobiec problemowi: stackoverflow.com/questions/4964663/…
Mike Rosenblum

Oto opinia na ten temat od Misha Shneerson z Microsoftu na ten temat, w terminie do komentarza 7 października 2010. ( blogs.msdn.com/b/csharpfaq/archive/2010/09/28/… )
Mike Rosenblum

Mam nadzieję, że pomoże to w utrzymaniu stałego problemu. Czy bezpieczne jest wykonywanie operacji wyrzucania elementów bezużytecznych i zwalnianie w obrębie bloku wreszcie?
Brett Green

213

Przedmowa: moja odpowiedź zawiera dwa rozwiązania, więc uważaj podczas czytania i niczego nie przegap.

Istnieją różne sposoby i porady dotyczące rozładowywania wystąpienia programu Excel, takie jak:

  • Zwolnienie KAŻDEGO obiektu com jawnie za pomocą Marshal.FinalReleaseComObject () (nie zapominając o niejawnie utworzonych obiektach com). Aby zwolnić każdy utworzony obiekt com, możesz użyć reguły 2 kropek wymienionych tutaj:
    Jak prawidłowo wyczyścić obiekty programu Excel interop?

  • Wywoływanie GC.Collect () i GC.WaitForPendingFinalizers () w celu uwolnienia CLR nieużywanych obiektów com * (Właściwie to działa, zobacz szczegóły w moim drugim rozwiązaniu)

  • Sprawdzanie, czy aplikacja na serwer może wyświetlać okno komunikatu czekające na odpowiedź użytkownika (chociaż nie jestem pewien, czy może to uniemożliwić zamknięcie programu Excel, ale słyszałem o tym kilka razy)

  • Wysyłanie wiadomości WM_CLOSE do głównego okna programu Excel

  • Wykonywanie funkcji współpracującej z programem Excel w osobnej AppDomain. Niektóre osoby uważają, że wystąpienie programu Excel zostanie zamknięte, gdy aplikacja AppDomain zostanie zwolniona.

  • Zabijanie wszystkich instancji programu Excel, które zostały utworzone po uruchomieniu naszego kodu współpracy programu Excel.

ALE! Czasami wszystkie te opcje po prostu nie pomagają lub nie mogą być odpowiednie!

Na przykład wczoraj dowiedziałem się, że w jednej z moich funkcji (która działa z programem Excel) program Excel działa po zakończeniu funkcji. Próbowałem wszystkiego! Dokładnie sprawdziłem całą funkcję 10 razy i do wszystkiego dodałem Marshal.FinalReleaseComObject ()! Miałem także GC.Collect () i GC.WaitForPendingFinalizers (). Sprawdziłem ukryte skrzynki wiadomości. Próbowałem wysłać wiadomość WM_CLOSE do głównego okna programu Excel. Wykonałem swoją funkcję w oddzielnej domenie aplikacji i zwolniłem tę domenę. Nic nie pomogło! Opcja zamykania wszystkich instancji programu Excel jest nieodpowiednia, ponieważ jeśli użytkownik uruchomi inną instancję programu Excel ręcznie, podczas wykonywania mojej funkcji, która działa również z programem Excel, ta instancja zostanie również zamknięta przez moją funkcję. Założę się, że użytkownik nie będzie szczęśliwy! Więc, szczerze mówiąc, jest to kiepska opcja (bez obrazy).rozwiązanie : Zabij proces Excel przez hWnd jego głównego okna (jest to pierwsze rozwiązanie).

Oto prosty kod:

[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();
}

Jak widać, podałem dwie metody, zgodnie ze wzorem Try-Parse (myślę, że jest to właściwe tutaj): jedna metoda nie zgłasza wyjątku, jeśli Proces nie może zostać zabity (na przykład proces już nie istnieje) , a inna metoda zgłasza wyjątek, jeśli proces nie został zabity. Jedynym słabym miejscem w tym kodzie są uprawnienia bezpieczeństwa. Teoretycznie użytkownik może nie mieć uprawnień do zabicia procesu, ale w 99,99% wszystkich przypadków użytkownik ma takie uprawnienia. Przetestowałem to również na koncie gościa - działa idealnie.

Twój kod działający z programem Excel może wyglądać następująco:

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 został zakończony! :)

Ok, wróćmy do drugiego rozwiązania, jak obiecałem na początku postu. Drugim rozwiązaniem jest wywołanie GC.Collect () i GC.WaitForPendingFinalizers (). Tak, faktycznie działają, ale musisz uważać tutaj!
Wiele osób twierdzi (i powiedziałem), że wywołanie GC.Collect () nie pomaga. Ale to nie pomoże, jeśli nadal istnieją odniesienia do obiektów COM! Jednym z najbardziej popularnych powodów, dla których GC.Collect () nie jest pomocny, jest uruchomienie projektu w trybie debugowania. W obiektach w trybie debugowania, do których tak naprawdę nie ma już odniesienia, nie będą wyrzucane śmieci do końca metody.
Jeśli więc wypróbowałeś GC.Collect () i GC.WaitForPendingFinalizers () i to nie pomogło, spróbuj wykonać następujące czynności:

1) Spróbuj uruchomić projekt w trybie Release i sprawdź, czy program Excel został poprawnie zamknięty

2) Zawiń metodę pracy z programem Excel osobną metodą. Zamiast czegoś takiego:

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

ty piszesz:

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)
    ...
  }
}

Teraz program Excel zostanie zamknięty =)


19
smutne, że wątek jest już tak stary, że twoja doskonała odpowiedź pojawia się tak daleko poniżej, co, jak sądzę, jest jedynym powodem, dla którego nie był już więcej oceniany ...
chiccodoro

15
Muszę przyznać, że kiedy po raz pierwszy przeczytałem twoją odpowiedź, pomyślałem, że to gigantyczna kludge. Po około 6 godzinach zmagań z tym (wszystko jest wypuszczone, nie mam podwójnych kropek itp.), Myślę, że twoja odpowiedź jest genialna.
Mark

3
Dzięki za to. Walczyłem z Excelem, który nie zamknąłby się bez względu na wszystko przez kilka dni, zanim to znalazłem. Świetny.
BBlake,

2
@nightcoder: niesamowita odpowiedź, naprawdę szczegółowa. Twoje uwagi dotyczące trybu debugowania są bardzo prawdziwe i ważne, że o tym wspomniałeś. Ten sam kod w trybie zwolnienia często może być w porządku. Process.Kill jest jednak dobry tylko w przypadku korzystania z automatyzacji, a nie wtedy, gdy program działa w programie Excel, np. Jako zarządzany dodatek COM.
Mike Rosenblum,

2
@DanM: Twoje podejście jest w 100% prawidłowe i nigdy nie zawiedzie. Dzięki zachowaniu wszystkich zmiennych lokalnych dla metody, wszystkie odwołania są skutecznie nieosiągalne przez kod .NET. Oznacza to, że kiedy twoja sekcja wreszcie wywoła GC.Collect (), finalizatory dla wszystkich twoich obiektów COM zostaną wywołane z pewnością. Każdy finalizator wywołuje następnie Marshal.FinalReleaseComObject na finalizowanym obiekcie. Twoje podejście jest zatem proste i głupie. Nie obawiaj się tego. (Tylko zastrzeżenie: jeśli używasz VSTO, w co wątpię, że tak, musisz wywołać GC.Collect () i GC.WaitForPendingFinalizers DWUKROTNIE.)
Mike Rosenblum

49

AKTUALIZACJA : Dodano kod C # i link do zadań systemu Windows

Spędziłem trochę czasu próbując rozwiązać ten problem, a wtedy XtremeVBTalk był najbardziej aktywny i responsywny. Oto link do mojego oryginalnego postu, „ Zamykanie procesu Excel Interop”, nawet jeśli aplikacja ulegnie awarii . Poniżej znajduje się streszczenie posta, a kod skopiowany do tego postu.

  • Zamykanie procesu Interop Application.Quit()i Process.Kill()działa w przeważającej części, ale kończy się niepowodzeniem, jeśli aplikacja ulegnie awarii. Tzn. Jeśli aplikacja ulegnie awarii, proces programu Excel nadal będzie działał luźno.
  • Rozwiązaniem jest umożliwienie systemowi operacyjnemu czyszczenia procesów przez Windows Job Objects przy użyciu wywołań Win32. Kiedy Twoja główna aplikacja umrze, powiązane procesy (np. Excel) również zostaną zakończone.

Uważam, że jest to czyste rozwiązanie, ponieważ system operacyjny wykonuje prawdziwą pracę w zakresie czyszczenia. Wszystko, co musisz zrobić, to zarejestrować proces Excel.

Kod pracy systemu Windows

Zawija wywołania API Win32, aby zarejestrować procesy Interop.

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);
    }

}

Uwaga na temat kodu konstruktora

  • W konstruktorze info.LimitFlags = 0x2000;wywoływana jest funkcja . 0x2000jest JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSEwartością wyliczania, a ta wartość jest zdefiniowana przez MSDN jako:

Powoduje zakończenie wszystkich procesów powiązanych z zadaniem, gdy ostatni uchwyt zadania jest zamknięty.

Dodatkowe wywołanie API Win32, aby uzyskać identyfikator procesu (PID)

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

Korzystanie z kodu

    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);

2
Jawne zabicie pozaprocesowego serwera COM (który może obsługiwać innych klientów COM) to straszny pomysł. Jeśli uciekasz się do tego, to dlatego, że Twój protokół COM jest uszkodzony. Serwerów COM nie należy traktować jak zwykłej aplikacji okienkowej
MickyD

38

To zadziałało dla projektu, nad którym pracowałem:

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

Dowiedzieliśmy się, że po zakończeniu korzystania z każdego odwołania do obiektu COM programu Excel należy wyzerować. Obejmowało to komórki, arkusze i wszystko.


30

Wszystko, co znajduje się w przestrzeni nazw Excela, musi zostać zwolnione. Kropka

Nie możesz robić:

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

Musisz to robić

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

następnie następuje zwolnienie obiektów.


3
Jak na przykład aobut xlRange.Interior.Color.
HAdes

Wnętrze musi zostać wydane (jest w przestrzeni nazw) ... Kolor z drugiej strony nie (bo pochodzi z System.Drawing.Color, iirc)
MagicKat

2
faktycznie Kolor to kolor Excela, a nie kolor .Net. po prostu zdasz Długi. Również skoroszyty def. trzeba zwolnić, arkusze robocze ... mniej.
Anonimowy typ

To nieprawda, zasada dwóch kropek, dobra kropka, to nonsens. Ponadto nie musisz wydawać skoroszytów, arkuszy, zakresów, to jest zadanie GC. Jedyną rzeczą, której nigdy nie wolno zapomnieć, jest wywołanie Quit na jednym i jedynym obiekcie Excel.Application. Po wywołaniu Quit zeruj obiekt Excel.Application i wywołaj GC.Collect (); GC.WaitForPendingFinalizers (); dwa razy.
Dietrich Baumgarten

29

Po pierwsze - nigdy nie musisz dzwonić Marshal.ReleaseComObject(...)ani Marshal.FinalReleaseComObject(...)wykonywać operacji w programie Excel. Jest to mylący anty-wzorzec, ale wszelkie informacje na ten temat, w tym od Microsoft, wskazujące, że musisz ręcznie zwolnić odwołania COM z .NET, są nieprawidłowe. Faktem jest, że środowisko uruchomieniowe .NET i moduł czyszczenia pamięci poprawnie śledzą i usuwają odwołania COM. W przypadku kodu oznacza to, że możesz usunąć całą pętlę `while (...) u góry.

Po drugie, jeśli chcesz się upewnić, że odwołania COM do obiektu COM poza procesem zostaną wyczyszczone po zakończeniu procesu (aby proces Excel się zamknął), musisz upewnić się, że moduł czyszczenia pamięci działa. Robisz to poprawnie z połączeniami do GC.Collect()i GC.WaitForPendingFinalizers(). Dwukrotne wywołanie tego jest bezpieczne i zapewnia, że ​​cykle są zdecydowanie wyczyszczone (choć nie jestem pewien, czy jest to potrzebne i doceniłbym przykład, który to pokazuje).

Po trzecie, podczas działania w debuggerze lokalne odniesienia będą sztucznie utrzymywane przy życiu aż do końca metody (tak, aby lokalna kontrola zmiennych działała). Tak więc GC.Collect()wywołania nie są skuteczne w czyszczeniu obiektów jak rng.Cellsprzy użyciu tej samej metody. Powinieneś podzielić kod wykonując interakcję COM z czyszczenia GC na osobne metody. (To było dla mnie kluczowe odkrycie, z jednej części odpowiedzi opublikowanej tutaj przez @nightcoder.)

Ogólny wzór byłby zatem następujący:

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

Istnieje wiele fałszywych informacji i zamieszania na ten temat, w tym wiele postów na MSDN i na Stack Overflow (a szczególnie na to pytanie!).

To, co ostatecznie przekonało mnie do dokładniejszego przyjrzenia się i znalezienia właściwej porady, to post na blogu Marshal.ReleaseComObject uznany za niebezpieczny wraz ze znalezieniem problemu z referencjami utrzymywanymi przy debuggerze, który mylił moje wcześniejsze testy.


5
Praktycznie WSZYSTKIE ODPOWIEDZI NA TEJ STRONIE SĄ ŹLE Z WYŁĄCZENIEM JEDNEJ. Działają, ale są o wiele za dużo pracy. Zignoruj ​​zasadę „2 kropki”. Pozwól, aby GC wykonał za ciebie pracę. Dodatkowe dowody z .Net GURU Hans Passant: stackoverflow.com/a/25135685/3852958
Alan Baljeu

1
Govert, co za ulga w znalezieniu właściwego sposobu na zrobienie tego. Dziękuję Ci.
Dietrich Baumgarten

18

I znalazł użyteczne ogólny szablon, który może pomóc wdrożenie prawidłowego wzorca utylizacji dla obiektów COM, tę potrzebę Marshal.ReleaseComObject nazwie kiedy wychodzi z zakresu:

Stosowanie:

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();
    } 
}

Szablon:

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;
        }
    }
}

Odniesienie:

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


3
tak, ten jest dobry. Myślę, że ten kod można zaktualizować teraz, gdy dostępny jest FinalReleaseCOMObject.
Anonimowy typ

Nadal denerwujące jest posiadanie bloku używanego dla każdego obiektu. Zobacz tutaj stackoverflow.com/questions/2191489/ ... jako alternatywę.
Henrik

16

Nie mogę uwierzyć, że ten problem nawiedza świat od 5 lat ... Jeśli utworzyłeś aplikację, musisz ją najpierw zamknąć przed usunięciem linku.

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

podczas zamykania

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

Gdy nowa aplikacja Excela, otwiera program Excel w tle. Musisz wydać polecenie, aby ten program Excel zakończył działanie przed zwolnieniem łącza, ponieważ ten program Excel nie jest częścią twojej bezpośredniej kontroli. Dlatego pozostanie otwarty, jeśli link zostanie zwolniony!

Dobre programowanie dla wszystkich ~~


Nie, nie robisz tego, zwłaszcza jeśli chcesz zezwolić na interakcję użytkownika z aplikacją COM (w tym przypadku Excel - OP nie określa, czy interakcja użytkownika jest zamierzona, ale nie wspomina o jej wyłączeniu). Musisz tylko wywołać program wywołujący, aby użytkownik mógł wyjść z programu Excel podczas zamykania, powiedzmy, jednego pliku.
downwitch

działało to dla mnie idealnie - zawieszone, gdy miałem tylko obiekt objExcel.Quit (), ale gdy wcześniej również zrobiłem objExcel.Application.Quit (), zamknięte.
mcmillab

13

Zwykli programiści, żadne z twoich rozwiązań nie działało dla mnie, więc decyduję się na wdrożenie nowej sztuczki .

Najpierw określmy „Jaki jest nasz cel?” => „Nie widać obiektu Excela po naszej pracy w menedżerze zadań”

Ok. Nie rzucaj wyzwania i zacznij go niszczyć, ale rozważ, aby nie zniszczyć innych wystąpień programu Excel, które działają równolegle.

Pobierz listę bieżących procesorów i pobierz PID procesów EXCEL, a następnie, gdy twoje zadanie zostanie wykonane, mamy nowego gościa na liście procesów z unikalnym PID, znajdź i zniszcz tylko ten.

<pamiętaj, że każdy nowy proces programu Excel podczas pracy w programie Excel zostanie wykryty jako nowy i zniszczony> <Lepszym rozwiązaniem jest przechwycenie PID nowo utworzonego obiektu programu Excel i po prostu jego zniszczenie>

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();

To rozwiązuje mój problem, mam nadzieję, że również twój.


1
.... to jest trochę podejście do sanek? - jest podobny do tego, którego używam również :( ale muszę go zmienić. Problem z korzystaniem z tego podejścia jest taki, że często podczas otwierania programu Excel na komputerze, na którym to działa, ma alert na pasku po lewej stronie, mówiąc coś w stylu „Program Excel został nieprawidłowo zamknięty”: niezbyt elegancki. Myślę, że należy preferować jedną z wcześniejszych sugestii w tym wątku.
whytheq

11

To z pewnością wydaje się zbyt skomplikowane. Z mojego doświadczenia wynika, że ​​prawidłowe zamknięcie programu Excel to tylko trzy kluczowe rzeczy:

1: upewnij się, że nie ma żadnych odwołań do utworzonej aplikacji Excel (powinieneś mieć tylko jedną; ustaw ją na null)

2: połączenie GC.Collect()

3: Excel musi zostać zamknięty przez użytkownika ręcznie zamykającego program lub przez wywołanie Quitobiektu Excel. (Należy pamiętać, że Quitbędzie działać tak, jakby użytkownik próbował zamknąć program i wyświetli okno dialogowe potwierdzenia, jeśli wystąpią niezapisane zmiany, nawet jeśli program Excel nie jest widoczny. Użytkownik może nacisnąć przycisk Anuluj, a następnie program Excel nie zostanie zamknięty. )

1 musi zdarzyć się przed 2, ale 3 mogą się zdarzyć w dowolnym momencie.

Jednym ze sposobów realizacji tego jest zawinięcie obiektu Excel interop własną klasą, utworzenie instancji interop w konstruktorze i implementacja IDisposable z Dispose wyglądającym jak

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

To wyczyści program Excel ze strony Twojego programu. Po zamknięciu programu Excel (ręcznie przez użytkownika lub przez telefon Quit) proces zniknie. Jeśli program został już zamknięty, proces zniknie podczas GC.Collect()połączenia.

(Nie jestem pewien, jak ważne to jest, ale możesz chcieć GC.WaitForPendingFinalizers()połączenia po nim, GC.Collect()ale nie jest to absolutnie konieczne, aby pozbyć się procesu Excel).

To działało dla mnie bez problemu od lat. Pamiętaj jednak, że podczas gdy to działa, w rzeczywistości musisz zamknąć się z gracją, aby zadziałało. Nadal będziesz gromadzić procesy excel.exe, jeśli przerwiesz program przed wyczyszczeniem programu Excel (zwykle poprzez naciśnięcie „stop” podczas debugowania programu).


1
Ta odpowiedź działa i jest nieskończenie wygodniejsza niż odpowiedź zaakceptowana. Bez obaw o „dwie kropki” z obiektami COM, bez użycia Marshal.
andrew.cuthbert

9

Aby dodać do powodów, dla których program Excel nie zamyka się, nawet gdy tworzysz bezpośrednie odwołania do każdego obiektu podczas odczytu, tworzenie, jest pętlą „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

I kolejny powód, ponowne użycie odwołania bez wcześniejszego zwolnienia poprzedniej wartości.
Grimfort,

Dziękuję Ci. Używałem również „dla każdego”, które zadziałało dla mnie.
Chad Braun-Duin

+1 dzięki. Dodatkowo zauważ, że jeśli masz obiekt dla zakresu Excela i chcesz zmienić zakres w czasie jego istnienia, zauważyłem, że musiałem ReleaseComObject przed ponownym przypisaniem, co czyni kod trochę nieporządnym!
AjV Jsy,

9

Tradycyjnie stosuję się do rad zawartych w odpowiedzi VVS . Jednak starając się aktualizować tę odpowiedź zgodnie z najnowszymi opcjami, myślę, że wszystkie moje przyszłe projekty będą korzystać z biblioteki „NetOffice”.

NetOffice jest kompletnym zamiennikiem Office PIA i jest całkowicie niezależny od wersji. Jest to kolekcja opakowań zarządzanych COM, które mogą obsługiwać czyszczenie, które często powoduje takie problemy podczas pracy z pakietem Microsoft Office w .NET.

Niektóre kluczowe funkcje to:

  • Głównie niezależne od wersji (i funkcje zależne od wersji są udokumentowane)
  • Bez zależności
  • Bez PIA
  • Bez rejestracji
  • Brak VSTO

Nie jestem w żaden sposób związany z projektem; Naprawdę doceniam wyraźne zmniejszenie bólu głowy.


2
To naprawdę powinno być oznaczone jako odpowiedź. NetOffice odciąga całą tę złożoność.
C. Augusto Proiete,

1
Używam NetOffice od dłuższego czasu, pisząc dodatek do programu Excel i działało to doskonale. Jedyną rzeczą do rozważenia jest to, że jeśli nie usuniesz używanych obiektów jawnie, zrobi to po wyjściu z aplikacji (ponieważ i tak je śledzi). Zatem w NetOffice zasadą jest, aby zawsze używać wzorca „za pomocą” z każdym obiektem Excela, takim jak komórka, zakres lub arkusz itp.
Staś Iwanow

8

Przyjęta tutaj odpowiedź jest poprawna, ale należy również pamiętać, że należy unikać nie tylko odniesień do „dwóch kropek”, ale także obiektów, które są pobierane przez indeks. Nie musisz też czekać, aż program skończy wyczyścić te obiekty, najlepiej jest utworzyć funkcje, które wyczyszczą je, gdy tylko skończysz, jeśli to możliwe. Oto utworzona przeze mnie funkcja, która przypisuje niektóre właściwości obiektu Style o nazwie 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 { } 
}

Zauważ, że musiałem ustawić xlBorders[Excel.XlBordersIndex.xlEdgeBottom]zmienną, aby to wyczyścić (Nie z powodu dwóch kropek, które odnoszą się do wyliczenia, które nie muszą być zwolnione, ale dlatego, że obiekt, o którym mówię, jest w rzeczywistości obiektem Border który musi zostać wydany).

Tego rodzaju rzeczy nie są tak naprawdę konieczne w standardowych aplikacjach, które wykonują świetną robotę po sobie, ale w aplikacjach ASP.NET, jeśli przegapisz choć jedną z nich, bez względu na to, jak często wywołujesz moduł wyrzucania elementów bezużytecznych, Excel nadal działa na twoim serwerze.

Wymaga to dużej dbałości o szczegóły i wykonywania wielu testów podczas monitorowania Menedżera zadań podczas pisania tego kodu, ale pozwala to uniknąć kłopotów z desperackim przeszukiwaniem stron kodu w celu znalezienia jednego pominiętego wystąpienia. Jest to szczególnie ważne podczas pracy w pętlach, w których należy zwolnić KAŻDĄ INSTANCJĘ obiektu, mimo że używa tej samej nazwy zmiennej za każdym razem, gdy pętla jest używana.


Wewnątrz wydania sprawdź Null (odpowiedź Joe). Pozwoli to uniknąć niepotrzebnych wyjątków zerowych. Przetestowałem wiele metod. Jest to jedyny sposób na skuteczne zwolnienie programu Excel.
Gerhard Powell

8

Po spróbowaniu

  1. Zwolnij obiekty COM w odwrotnej kolejności
  2. Dodaj GC.Collect()i GC.WaitForPendingFinalizers()dwukrotnie na końcu
  3. Nie więcej niż dwie kropki
  4. Zamknij skoroszyt i zamknij aplikację
  5. Uruchom w trybie zwolnienia

ostatecznym rozwiązaniem, które działa dla mnie, jest przeniesienie jednego zestawu

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

które dodaliśmy na końcu funkcji do opakowania, w następujący sposób:

private void FunctionWrapper(string sourcePath, string targetPath)
{
    try
    {
        FunctionThatCallsExcel(sourcePath, targetPath);
    }
    finally
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

Stwierdziłem, że nie muszę zerować obiektów ComObject, tylko Quit () - Close () i FinalReleaseComObject. Nie mogę uwierzyć, że to wszystko, czego mi brakowało, żeby to zadziałało. Świetny!
Carol

7

Śledziłem dokładnie to ... Ale wciąż napotykałem problemy na 1 z 1000 razy. Kto wie dlaczego? Czas wydobyć młotek ...

Zaraz po utworzeniu instancji klasy aplikacji Excel otrzymuję właśnie utworzony proces Excel.

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

Potem, kiedy wykonam wszystkie powyższe operacje czyszczenia COM, upewniam się, że proces nie działa. Jeśli nadal działa, zabij go!

if (!process.HasExited)
   process.Kill();

7

¸ ° º¤ø „¸ Shoot Excel proc i żuć gumę balonową ¸„ ø¤º ° ¨

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);
}

6

Musisz pamiętać, że program Excel jest bardzo wrażliwy na kulturę, w której prowadzisz.

Może się okazać, że musisz ustawić kulturę na EN-US przed wywołaniem funkcji Excela. Nie dotyczy to wszystkich funkcji - ale niektórych z nich.

    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;

Dotyczy to nawet jeśli korzystasz z VSTO.

Aby uzyskać szczegółowe informacje: http://support.microsoft.com/default.aspx?scid=kb;en-us;Q320369


6

„Nigdy nie używaj dwóch kropek z obiektami COM” to wielka ogólna zasada, aby uniknąć wycieku referencji COM, ale Excel PIA może prowadzić do wycieku na więcej sposobów niż pozornie na pierwszy rzut oka.

Jednym z tych sposobów jest zasubskrybowanie dowolnego zdarzenia ujawnionego przez dowolny obiekt COM modelu obiektowego Excel.

Na przykład subskrybowanie zdarzenia WorkbookOpen klasy aplikacji.

Trochę teorii na temat zdarzeń COM

Klasy COM ujawniają grupę zdarzeń poprzez interfejsy zwrotne. Aby subskrybować zdarzenia, kod klienta może po prostu zarejestrować obiekt implementujący interfejs zwrotny, a klasa COM wywoła swoje metody w odpowiedzi na określone zdarzenia. Ponieważ interfejs oddzwaniania jest interfejsem COM, obowiązkiem obiektu implementującego jest zmniejszenie liczby referencji dowolnego otrzymanego obiektu COM (jako parametru) dla dowolnej procedury obsługi zdarzeń.

Jak Excel PIA eksponuje zdarzenia COM

Excel PIA przedstawia zdarzenia COM klasy aplikacji Excel jako konwencjonalne zdarzenia .NET. Ilekroć subskrybuje kod klienta do zdarzenia .NET (nacisk na „a”), PIA tworzy się instancji klasy implementującej interfejs oddzwonienia i rejestry to z Excela.

W związku z tym wiele obiektów oddzwaniania jest rejestrowanych w programie Excel w odpowiedzi na różne żądania subskrypcji z kodu .NET. Jeden obiekt oddzwaniania na subskrypcję zdarzenia.

Interfejs oddzwaniania do obsługi zdarzeń oznacza, że ​​PIA musi subskrybować wszystkie zdarzenia interfejsu dla każdego żądania subskrypcji zdarzeń .NET. Nie można wybrać. Po otrzymaniu wywołania zwrotnego zdarzenia obiekt zwrotny sprawdza, czy powiązana procedura obsługi zdarzeń .NET jest zainteresowana bieżącym zdarzeniem, czy nie, a następnie albo wywołuje procedurę obsługi, albo dyskretnie ignoruje oddzwonienie.

Wpływ na liczbę odwołań do instancji COM

Wszystkie te obiekty oddzwaniania nie zmniejszają liczby referencyjnej żadnego z otrzymywanych obiektów COM (jako parametrów) dla żadnej z metod oddzwaniania (nawet tych, które są dyskretnie ignorowane). Opierają się wyłącznie na śmieciarzu CLR, aby uwolnić obiekty COM.

Ponieważ przebieg GC jest niedeterministyczny, może to prowadzić do wstrzymania procesu Excela na dłużej niż jest to pożądane i stworzyć wrażenie „wycieku pamięci”.

Rozwiązanie

Jedynym rozwiązaniem na razie jest uniknięcie dostawcy zdarzeń PIA dla klasy COM i napisanie własnego dostawcy zdarzeń, który deterministycznie uwalnia obiekty COM.

W przypadku klasy aplikacji można to zrobić, implementując interfejs AppEvents, a następnie rejestrując implementację w programie Excel za pomocą interfejsu IConnectionPointContainer . Klasa aplikacji (i w tym przypadku wszystkie obiekty COM eksponujące zdarzenia za pomocą mechanizmu zwrotnego) implementuje interfejs IConnectionPointContainer.


4

Jak zauważyli inni, musisz utworzyć jawne odwołanie dla każdego używanego obiektu Excel i wywołać Marshal.ReleaseComObject w tym odwołaniu, jak opisano w tym artykule z bazy wiedzy . Musisz także użyć try / wreszcie, aby upewnić się, że ReleaseComObject jest zawsze wywoływany, nawet gdy zostanie zgłoszony wyjątek. Tj. Zamiast:

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

musisz zrobić coś takiego:

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);
}

Musisz także wywołać aplikację. Wyjdź przed zwolnieniem obiektu aplikacji, jeśli chcesz zamknąć program Excel.

Jak widać, szybko staje się to wyjątkowo nieporęczne, gdy tylko spróbujesz zrobić coś nawet umiarkowanie złożonego. Z powodzeniem opracowałem aplikacje .NET z prostą klasą opakowania, która otacza kilka prostych manipulacji modelem obiektowym Excel (otwórz skoroszyt, napisz do zakresu, zapisz / zamknij skoroszyt itp.). Klasa otoki implementuje IDisposable, dokładnie implementuje Marshal.ReleaseComObject na każdym używanym obiekcie i nie ujawnia publicznie żadnych obiektów Excela pozostałej części aplikacji.

Ale to podejście nie daje się dobrze skalować w przypadku bardziej złożonych wymagań.

Jest to duży niedobór .NET COM Interop. W przypadku bardziej skomplikowanych scenariuszy poważnie zastanowiłbym się nad napisaniem biblioteki DLL ActiveX w VB6 lub innym niezarządzanym języku, do którego można delegować całą interakcję z obiektami COM poza procesem, takimi jak Office. Następnie możesz odwołać się do tej biblioteki ActiveX DLL z aplikacji .NET, a rzeczy będą znacznie łatwiejsze, ponieważ będziesz musiał tylko opublikować to jedno odwołanie.


3

Gdy wszystkie powyższe rzeczy nie działają, spróbuj dać Excelowi trochę czasu na zamknięcie swoich arkuszy:

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

...
FinalReleaseComObject(app);

2
Nienawidzę arbitralnych oczekiwań. Ale +1, ponieważ masz rację, że musisz czekać na zamknięcie skoroszytów. Alternatywą jest odpytanie kolekcji skoroszytów w pętli i użycie arbitralnego oczekiwania jako limitu czasu pętli.
dFlat

3

Pamiętaj, aby zwolnić wszystkie obiekty związane z programem Excel!

Spędziłem kilka godzin, próbując na kilka sposobów. Wszystkie są świetnymi pomysłami, ale w końcu znalazłem swój błąd: jeśli nie uwolnisz wszystkich obiektów, żadna z powyższych metod nie pomoże ci w moim przypadku. Upewnij się, że wypuściłeś wszystkie obiekty, w tym pierwszy!

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

Opcje są tutaj razem .


Bardzo dobry punkt! Wydaje mi się, że ponieważ korzystam z pakietu Office 2007, w moim imieniu dokonano pewnych porządków. Skorzystałem z porady wyżej, ale nie zapisałem zmiennych, jak tu zasugerowałeś, i EXCEL.EXE kończy działanie, ale mogę mieć szczęście i jeśli będę miał dalsze problemy, na pewno spojrzę na tę część mojego kodu =)
Coops

3

Świetny artykuł na temat zwalniania obiektów COM to 2.5 Zwalnianie obiektów COM (MSDN).

Metodą, którą zalecałbym, jest zerowanie odwołań do programu Excel.Interop, jeśli są to zmienne nielokalne, a następnie wywołanie GC.Collect()i GC.WaitForPendingFinalizers()dwukrotne. Zmienne lokalne Interop będą obsługiwane automatycznie.

Eliminuje to potrzebę zachowania nazwanego odwołania dla każdego obiektu COM.

Oto przykład zaczerpnięty z artykułu:

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();
    }
}

Te słowa pochodzą bezpośrednio z artykułu:

W prawie wszystkich sytuacjach zerowanie odwołania RCW i wymuszenie wyrzucania elementów bezużytecznych spowoduje prawidłowe wyczyszczenie. Jeśli wywołasz także GC.WaitForPendingFinalizers, odśmiecanie będzie tak deterministyczne, jak tylko możesz. Oznacza to, że będziesz dokładnie pewien, kiedy obiekt zostanie wyczyszczony - po powrocie z drugiego wywołania WaitForPendingFinalizers. Alternatywnie możesz użyć Marshal.ReleaseComObject. Należy jednak pamiętać, że jest mało prawdopodobne, aby kiedykolwiek trzeba było używać tej metody.


2

Reguła dwóch kropek nie działała dla mnie. W moim przypadku stworzyłem metodę czyszczenia moich zasobów w następujący sposób:

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

2

Moje rozwiązanie

[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();
}

2

Należy bardzo ostrożnie korzystać z aplikacji do współpracy Word / Excel. Po wypróbowaniu wszystkich rozwiązań nadal wiele procesów „WinWord” pozostawało otwartych na serwerze (z ponad 2000 użytkowników).

Po wielu godzinach pracy nad problemem zdałem sobie sprawę, że jeśli otworzę więcej niż kilka dokumentów Word.ApplicationClass.Document.Open()jednocześnie z różnymi wątkami, proces roboczy IIS (w3wp.exe) ulegnie awarii, pozostawiając otwarte wszystkie procesy WinWord!

Sądzę więc, że nie ma absolutnego rozwiązania tego problemu, ale przejście na inne metody, takie jak tworzenie pakietu Office Open XML .


1

Przyjęta odpowiedź nie działała dla mnie. Poniższy kod w destruktorze wykonał zadanie.

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(); }
}


1

Obecnie pracuję nad automatyzacją pakietu Office i natknąłem się na rozwiązanie, które działa za każdym razem dla mnie. Jest to proste i nie wymaga zabijania żadnych procesów.

Wydaje się, że po prostu przechodząc przez bieżące aktywne procesy i w jakikolwiek sposób „uzyskując dostęp” do otwartego procesu programu Excel, wszelkie zbłąkane wiszące wystąpienia programu Excel zostaną usunięte. Poniższy kod po prostu sprawdza procesy o nazwie „Excel”, a następnie zapisuje właściwość MainWindowTitle procesu do łańcucha. Ta „interakcja” z procesem wydaje się powodować, że Windows łapie i przerywa zawieszone wystąpienie programu Excel.

Korzystam z poniższej metody tuż przed tym, jak dodatek, który rozwijam, kończy pracę, ponieważ uruchamia to zdarzenie zwalniające. Usuwa za każdym razem wszelkie wiszące wystąpienia programu Excel. Szczerze mówiąc, nie jestem do końca pewien, dlaczego to działa, ale działa dobrze dla mnie i może zostać umieszczone na końcu dowolnej aplikacji Excel bez martwienia się o podwójne kropki, Marshal.ReleaseComObject ani procesy zabijania. Byłbym bardzo zainteresowany wszelkimi sugestiami, dlaczego jest to skuteczne.

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;
                    }
                }
            }
}

1

Myślę, że niektóre z nich to tylko sposób, w jaki środowisko obsługuje aplikacje Office, ale mogę się mylić. W niektóre dni niektóre aplikacje natychmiast czyszczą procesy, a w inne dni wydaje się czekać na zamknięcie aplikacji. Ogólnie rzecz biorąc, przestałem zwracać uwagę na szczegóły i po prostu upewniłem się, że pod koniec dnia nie ma żadnych dodatkowych procesów.

Poza tym, a może już upraszczam rzeczy, ale myślę, że możesz po prostu ...

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();

Jak powiedziałem wcześniej, nie zwracam uwagi na szczegóły, kiedy proces Excel pojawia się lub znika, ale to zwykle działa dla mnie. Nie lubię też utrzymywać procesów Excela poza minimalnym czasem, ale prawdopodobnie jestem po prostu paranoikiem.


1

Jak niektórzy zapewne już napisali, nie jest ważne tylko, jak zamknąć Excela (obiekt); ważne jest także to, jak go otworzysz , a także rodzaj projektu.

W aplikacji WPF zasadniczo ten sam kod działa bez problemów lub z niewielkimi problemami.

Mam projekt, w którym ten sam plik Excel jest przetwarzany kilka razy dla różnych wartości parametrów - np. Parsowanie go na podstawie wartości z ogólnej listy.

Umieszczam wszystkie funkcje związane z programem Excel w klasie bazowej, a analizator składni w podklasę (różne parsery używają typowych funkcji programu Excel). Nie chciałem, aby Excel był otwierany i zamykany ponownie dla każdego elementu z ogólnej listy, więc otworzyłem go tylko raz w klasie podstawowej i zamknąłem w podklasie. Miałem problemy podczas przenoszenia kodu do aplikacji komputerowej. Wypróbowałem wiele wyżej wymienionych rozwiązań. GC.Collect()został już wdrożony wcześniej, dwa razy jak sugerowano.

Potem zdecydowałem, że przeniesie kod do otwierania programu Excel do podklasy. Zamiast otwierać tylko raz, teraz tworzę nowy obiekt (klasa podstawowa) i otwieram Excel dla każdego elementu i zamykam go na końcu. Występuje pewien spadek wydajności, ale na podstawie kilku testów procesy Excela zamykają się bez problemów (w trybie debugowania), więc usuwane są również pliki tymczasowe. Będę kontynuować testowanie i napiszę więcej, jeśli otrzymam jakieś aktualizacje.

Najważniejsze jest to: Musisz również sprawdzić kod inicjujący, szczególnie jeśli masz wiele klas itp.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.