Bagaimana cara membersihkan objek interop Excel dengan benar?

747

Saya menggunakan interop Excel di C # ( ApplicationClass) dan telah menempatkan kode berikut di klausa saya yang terakhir:

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

Meskipun ini berfungsi, Excel.exeprosesnya masih di latar belakang bahkan setelah saya menutup Excel. Ini hanya dirilis setelah aplikasi saya ditutup secara manual.

Apa yang saya lakukan salah, atau apakah ada alternatif untuk memastikan objek interop dibuang dengan benar?

Neraka
sumber
1
Apakah Anda mencoba untuk mematikan Excel.exe tanpa menutup aplikasi Anda? Tidak yakin saya sepenuhnya memahami pertanyaan Anda.
Bryant
3
Saya mencoba untuk memastikan objek interop yang tidak dikelola dibuang dengan benar. Sehingga tidak ada proses Excel yang berkeliaran bahkan ketika pengguna telah selesai dengan spreadsheet Excel yang kami buat dari aplikasi.
HAdes
3
Jika Anda dapat mencoba melakukannya dengan menghasilkan file XML Excel, jika tidak harap pertimbangkan VSTO un / managed Memory Management: jake.ginnivan.net/vsto-com-interop
Jeremy Thompson
Apakah ini menerjemahkan ke Excel dengan baik?
Coops
2
Lihat (selain jawaban di bawah) artikel dukungan ini dari Microsoft, di mana mereka secara khusus memberikan solusi untuk masalah ini: support.microsoft.com/kb/317109
Arjan

Jawaban:

684

Excel tidak berhenti karena aplikasi Anda masih memegang referensi ke objek COM.

Saya kira Anda memohon setidaknya satu anggota objek COM tanpa menugaskannya ke variabel.

Bagi saya itu adalah objek excelApp.Worksheets yang langsung saya gunakan tanpa menugaskannya ke variabel:

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

Saya tidak tahu bahwa secara internal C # membuat pembungkus untuk objek COM lembar kerja yang tidak dirilis oleh kode saya (karena saya tidak menyadarinya) dan merupakan alasan mengapa Excel tidak diturunkan.

Saya menemukan solusi untuk masalah saya di halaman ini , yang juga memiliki aturan yang bagus untuk penggunaan objek COM di C #:

Jangan pernah menggunakan dua titik dengan objek COM.


Jadi dengan pengetahuan ini cara yang tepat untuk melakukan hal di atas adalah:

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

PEMBARUAN POST MORTEM:

Saya ingin setiap pembaca membaca jawaban ini oleh Hans Passant dengan sangat hati-hati karena ini menjelaskan jebakan yang saya dan banyak pengembang lain temukan. Ketika saya menulis jawaban ini bertahun-tahun yang lalu saya tidak tahu tentang efek debugger terhadap pemulung dan menarik kesimpulan yang salah. Saya menjaga jawaban saya tidak berubah demi sejarah tetapi tolong baca tautan ini dan jangan mengikuti "dua titik": Memahami pengumpulan sampah di .NET dan Bersihkan Objek Excel Interop dengan IDisposable

VVS
sumber
16
Maka saya sarankan untuk tidak menggunakan Excel dari COM dan menyelamatkan diri Anda dari semua masalah. Format Excel 2007 dapat digunakan tanpa membuka Excel, cantik.
user7116
5
Saya tidak mengerti apa artinya "dua titik". Bisakah Anda jelaskan?
A9S6
22
Ini berarti, Anda tidak harus menggunakan pola comObject.Property.PropertiesProperty (Anda melihat dua titik?). Alih-alih, tetapkan comObject.Property ke variabel dan gunakan dan buang variabel itu. Versi yang lebih formal dari aturan di atas bisa berupa sth. seperti "Tetapkan objek com ke variabel sebelum Anda menggunakannya. Ini termasuk objek com yang merupakan properti dari objek com lain."
VVS
5
@Nick: Sebenarnya, Anda tidak perlu pembersihan apa pun, karena pengumpul sampah akan melakukannya untuk Anda. Satu-satunya hal yang perlu Anda lakukan adalah menetapkan setiap objek COM ke variabelnya sendiri sehingga GC mengetahuinya.
VVS
12
@ VSS itu bollocks, GC membersihkan semuanya karena variabel wrapper dibuat oleh .net framework. Mungkin perlu selamanya bagi GC untuk membersihkannya. Memanggil GC.Collect setelah interop berat bukan idear buruk.
CodingBarfield
280

Anda benar-benar dapat melepaskan objek Aplikasi Excel Anda dengan bersih, tetapi Anda harus berhati-hati.

Saran untuk mempertahankan referensi bernama untuk benar-benar setiap objek COM yang Anda akses dan kemudian secara eksplisit melepaskannya melalui Marshal.FinalReleaseComObject()teori, tetapi, sayangnya, sangat sulit untuk dikelola dalam praktiknya. Jika seseorang pernah tergelincir di mana saja dan menggunakan "dua titik", atau mengulangi sel melalui for eachloop, atau perintah sejenis lainnya, maka Anda akan memiliki objek COM yang tidak direferensikan dan berisiko hang. Dalam hal ini, tidak akan ada cara untuk menemukan penyebabnya dalam kode; Anda harus meninjau semua kode Anda dengan mata dan mudah-mudahan menemukan penyebabnya, tugas yang hampir mustahil untuk proyek besar.

Berita baiknya adalah Anda tidak harus mempertahankan referensi variabel bernama untuk setiap objek COM yang Anda gunakan. Sebaliknya, panggil GC.Collect()dan kemudian GC.WaitForPendingFinalizers()untuk melepaskan semua objek (biasanya minor) yang tidak Anda pegang referensi, dan kemudian secara eksplisit melepaskan objek yang Anda pegang referensi variabel bernama.

Anda juga harus merilis referensi nama Anda dalam urutan terbalik kepentingan: objek rentang pertama, lalu lembar kerja, buku kerja, dan akhirnya objek Aplikasi Excel Anda.

Misalnya, dengan asumsi bahwa Anda memiliki variabel objek Range bernama xlRng, variabel Worksheet bernama xlSheet, variabel Workbook bernama xlBookdan variabel Aplikasi Excel bernama xlApp, maka kode pembersihan Anda bisa terlihat seperti berikut:

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

Dalam sebagian besar contoh kode Anda akan melihat untuk membersihkan objek COM dari .NET, GC.Collect()dan GC.WaitForPendingFinalizers()panggilan dibuat DUA KALI seperti pada:

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

Namun, ini tidak diperlukan, kecuali jika Anda menggunakan Visual Studio Tools for Office (VSTO), yang menggunakan finalizers yang menyebabkan seluruh grafik objek dipromosikan dalam antrian finalisasi. Benda-benda seperti itu tidak akan dirilis sampai pengumpulan sampah berikutnya . Namun, jika Anda tidak menggunakan VSTO, Anda harus dapat menelepon GC.Collect()dan GC.WaitForPendingFinalizers()sekali saja.

Saya tahu bahwa menelepon secara eksplisit GC.Collect()adalah tidak-tidak (dan tentu saja melakukannya dua kali terdengar sangat menyakitkan), tetapi tidak ada jalan lain, jujur ​​saja. Melalui operasi normal, Anda akan menghasilkan objek tersembunyi yang tidak Anda pegang referensi sehingga tidak dapat dilepaskan melalui cara lain selain menelepon GC.Collect().

Ini adalah topik yang kompleks, tetapi ini adalah segalanya. Setelah Anda membuat templat ini untuk prosedur pembersihan, Anda dapat membuat kode secara normal, tanpa perlu pembungkus, dll. :-)

Saya punya tutorial tentang ini di sini:

Mengotomatiskan Program Office dengan VB.Net / COM Interop

Ini ditulis untuk VB.NET, tetapi jangan ditunda karena itu, prinsip-prinsipnya persis sama seperti ketika menggunakan C #.

Mike Rosenblum
sumber
3
Diskusi terkait dapat ditemukan di forum ExtremeVBTalk .NET Office Automation, di sini: xtremevbtalk.com/showthread.php?t=303928 .
Mike Rosenblum
2
Dan jika semuanya gagal, maka Process.Kill () dapat digunakan (sebagai upaya terakhir) seperti yang dijelaskan di sini: stackoverflow.com/questions/51462/…
Mike Rosenblum
1
Senang itu berhasil Richard. :-) Dan di sini adalah contoh halus di mana hanya menghindari "dua titik" tidak cukup untuk mencegah masalah: stackoverflow.com/questions/4964663/...
Mike Rosenblum
Inilah pendapat tentang ini dari Misha Shneerson dari Microsoft tentang topik tersebut, dalam tanggal komentar 7 Oktober 2010. ( blogs.msdn.com/b/csharpfaq/archive/2010/09/28/… )
Mike Rosenblum
Saya berharap ini membantu masalah yang kita alami. Apakah aman untuk melakukan pengumpulan sampah dan melepaskan panggilan dalam blok terakhir?
Brett Green
213

Kata pengantar: jawaban saya mengandung dua solusi, jadi berhati-hatilah saat membaca dan jangan lewatkan apa pun.

Ada berbagai cara dan saran tentang cara membuat Excel misalnya diturunkan, seperti:

  • Melepaskan SETIAP objek com secara eksplisit dengan Marshal.FinalReleaseComObject () (tidak lupa tentang objek com yang dibuat secara implisit). Untuk melepaskan setiap objek com yang dibuat, Anda dapat menggunakan aturan 2 titik yang disebutkan di sini:
    Bagaimana cara membersihkan objek interop Excel dengan benar?

  • Memanggil GC.Collect () dan GC.WaitForPendingFinalizers () untuk membuat CLR melepaskan objek-com yang tidak terpakai * (Sebenarnya, itu berfungsi, lihat solusi kedua saya untuk detail)

  • Memeriksa apakah aplikasi-server-com mungkin memperlihatkan kotak pesan menunggu pengguna untuk menjawab (meskipun saya tidak yakin itu dapat mencegah Excel menutup, tetapi saya mendengarnya beberapa kali)

  • Mengirim pesan WM_CLOSE ke jendela Excel utama

  • Menjalankan fungsi yang berfungsi dengan Excel di AppDomain yang terpisah. Beberapa orang percaya contoh Excel akan ditutup, ketika AppDomain diturunkan.

  • Membunuh semua instance excel yang dipakai setelah kode interoping excel kami dimulai.

TAPI! Terkadang semua opsi ini tidak membantu atau tidak sesuai!

Sebagai contoh, kemarin saya menemukan bahwa di salah satu fungsi saya (yang berfungsi dengan excel) Excel terus berjalan setelah fungsi berakhir. Saya mencoba semuanya! Saya benar-benar memeriksa seluruh fungsi 10 kali dan menambahkan Marshal.FinalReleaseComObject () untuk semuanya! Saya juga punya GC.Collect () dan GC.WaitForPendingFinalizers (). Saya memeriksa kotak pesan tersembunyi. Saya mencoba mengirim pesan WM_CLOSE ke jendela Excel utama. Saya menjalankan fungsi saya di AppDomain terpisah dan membongkar domain itu. Tidak ada yang membantu! Opsi dengan menutup semua instance excel tidak tepat, karena jika pengguna memulai instance Excel lain secara manual, selama eksekusi fungsi saya yang juga berfungsi dengan Excel, maka instance itu juga akan ditutup oleh fungsi saya. Saya yakin pengguna tidak akan senang! Jadi, jujur, ini opsi yang lemah (jangan tersinggung kawan).solusi : Bunuh proses excel dengan hWnd dari jendela utamanya (ini solusi pertama).

Ini adalah kode sederhana:

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

Seperti yang Anda lihat saya menyediakan dua metode, sesuai dengan pola Try-Parse (saya pikir itu tepat di sini): satu metode tidak membuang pengecualian jika Proses tidak dapat dibunuh (misalnya proses tidak ada lagi) , dan metode lain melempar pengecualian jika Proses tidak terbunuh. Satu-satunya tempat yang lemah dalam kode ini adalah izin keamanan. Secara teoritis, pengguna mungkin tidak memiliki izin untuk mematikan proses, tetapi dalam 99,99% dari semua kasus, pengguna memiliki izin tersebut. Saya juga mengujinya dengan akun tamu - itu berfungsi dengan baik.

Jadi, kode Anda, bekerja dengan Excel, dapat terlihat seperti ini:

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 dihentikan! :)

Ok, mari kita kembali ke solusi kedua, seperti yang saya janjikan di awal posting. Solusi kedua adalah dengan memanggil GC.Collect () dan GC.WaitForPendingFinalizers (). Ya, mereka benar-benar berfungsi, tetapi Anda harus berhati-hati di sini!
Banyak orang mengatakan (dan saya katakan) bahwa memanggil GC.Collect () tidak membantu. Tetapi alasan itu tidak membantu adalah jika masih ada referensi ke objek COM! Salah satu alasan paling populer untuk GC.Collect () tidak membantu adalah menjalankan proyek dalam mode Debug. Dalam mode debug-objek yang tidak benar-benar direferensikan lagi tidak akan menjadi sampah yang dikumpulkan sampai akhir metode.
Jadi, jika Anda mencoba GC.Collect () dan GC.WaitForPendingFinalizers () dan itu tidak membantu, coba lakukan hal berikut:

1) Cobalah untuk menjalankan proyek Anda dalam mode Rilis dan periksa apakah Excel ditutup dengan benar

2) Bungkus metode bekerja dengan Excel dalam metode terpisah. Jadi, bukannya sesuatu seperti ini:

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

Anda menulis:

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

Sekarang, Excel akan menutup =)

nightcoder
sumber
19
sedih karena utasnya sudah sangat tua sehingga jawaban Anda yang sangat bagus muncul jauh di bawah, yang saya pikir adalah satu-satunya alasan untuk tidak diputuskan lebih banyak kali ...
chiccodoro
15
Saya harus mengakui, ketika saya pertama kali membaca jawaban Anda, saya pikir itu adalah lumpur raksasa. Setelah sekitar 6 jam bergulat dengan ini (semuanya dilepaskan, saya tidak punya titik ganda, dll.), Sekarang saya pikir jawaban Anda jenius.
Tandai
3
Terima kasih untuk ini. Telah bergulat dengan Excel yang tidak akan menutup apa pun selama beberapa hari sebelum saya menemukan ini. Luar biasa.
BBlake
2
@ nightcoder: jawaban yang luar biasa, sangat detail. Komentar Anda sehubungan dengan mode debug sangat benar dan penting yang Anda tunjukkan ini. Kode yang sama dalam mode rilis seringkali bisa baik-baik saja. Proses. Namun, hanya baik ketika menggunakan otomatisasi, bukan ketika program Anda berjalan di dalam Excel, misalnya, sebagai tambahan COM yang dikelola.
Mike Rosenblum
2
@DANM: Pendekatan Anda 100% benar dan tidak akan pernah gagal. Dengan menjaga semua variabel Anda tetap pada metode ini, semua referensi secara efektif tidak dapat dijangkau oleh kode .NET. Ini berarti bahwa ketika bagian terakhir Anda memanggil GC.Collect (), finalizer untuk semua objek COM Anda akan dipanggil dengan pasti. Setiap finalizer kemudian memanggil Marshal.FinalReleaseComObject pada objek yang sedang difinalisasi. Karenanya, pendekatan Anda sederhana dan tanpa bukti. Jangan takut menggunakan ini. (Hanya peringatan: jika menggunakan VSTO, yang saya ragu Anda adalah, Anda perlu menelepon GC.Collect () & GC.WaitForPendingFinalizers TWICE.)
Mike Rosenblum
49

UPDATE : Menambahkan kode C #, dan menautkan ke Windows Jobs

Saya menghabiskan beberapa waktu untuk mencari tahu masalah ini, dan pada saat itu XtremeVBTalk adalah yang paling aktif dan responsif. Berikut ini tautan ke posting asli saya, Menutup proses Excel Interop dengan bersih, bahkan jika aplikasi Anda macet . Di bawah ini adalah ringkasan posting, dan kode yang disalin ke posting ini.

  • Menutup proses Interop dengan Application.Quit()dan Process.Kill()berfungsi sebagian besar, tetapi gagal jika aplikasi crash secara katastropis. Yaitu jika aplikasi macet, proses Excel masih akan berjalan longgar.
  • Solusinya adalah membiarkan OS menangani pembersihan proses Anda melalui Windows Job Objects menggunakan panggilan Win32. Ketika aplikasi utama Anda mati, proses terkait (yaitu Excel) juga akan dihentikan.

Saya menemukan ini menjadi solusi bersih karena OS melakukan pekerjaan nyata membersihkan. Yang harus Anda lakukan adalah mendaftarkan proses Excel.

Kode Pekerjaan Windows

Membungkus Panggilan API Win32 untuk mendaftarkan proses 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);
    }

}

Catatan tentang kode konstruktor

  • Dalam konstruktor, info.LimitFlags = 0x2000;disebut. 0x2000adalah nilai JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSEenum, dan nilai ini didefinisikan oleh MSDN sebagai:

Menyebabkan semua proses yang terkait dengan pekerjaan berakhir saat pegangan terakhir ke pekerjaan ditutup.

Extra Win32 API Call untuk mendapatkan ID Proses (PID)

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

Menggunakan kodenya

    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);
Astaga
sumber
2
Secara eksplisit membunuh server COM yang tidak dalam proses (yang mungkin melayani klien COM lainnya) adalah ide yang mengerikan. Jika Anda menggunakan ini karena protokol COM Anda rusak. Server COM seharusnya tidak diperlakukan sebagai aplikasi windowed biasa
MickyD
39

Ini berhasil untuk proyek yang saya kerjakan:

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

Kami belajar bahwa penting untuk menetapkan setiap referensi ke objek Excel COM ke nol ketika Anda selesai menggunakannya. Ini termasuk Sel, Lembar, dan semuanya.

Philip Fourie
sumber
30

Apa pun yang ada di dalam namespace Excel perlu dirilis. Titik

Anda tidak dapat melakukan:

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

Anda harus melakukannya

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

diikuti oleh pelepasan benda-benda.

MagicKat
sumber
3
Bagaimana aobut xlRange.Interior.Color misalnya.
HAdes
Interior perlu dirilis (di namespace) ... Warna di sisi lain tidak (karena dari System.Drawing.Color, iirc)
MagicKat
2
sebenarnya Warna adalah warna Excel, bukan warna .Net. Anda baru saja lulus Long. Juga buku kerja def. perlu dirilis, lembar kerja ... kurang begitu.
Anonymous Type
Itu tidak benar, aturan dua-titik-buruk, satu-titik-baik adalah omong kosong. Juga, Anda tidak harus merilis Buku Kerja, Lembar Kerja, Rentang, itu adalah pekerjaan GC. Satu-satunya hal yang tidak boleh Anda lupakan adalah memanggil Berhenti pada satu-satunya objek Excel. Aplikasi. Setelah memanggil Berhenti, null objek Excel.Application dan panggil GC.Collect (); GC.WaitForPendingFinalizers (); dua kali.
Dietrich Baumgarten
29

Pertama - Anda tidak perlu menelepon Marshal.ReleaseComObject(...)atau Marshal.FinalReleaseComObject(...)ketika melakukan interop Excel. Ini adalah anti-pola yang membingungkan, tetapi informasi apa pun tentang ini, termasuk dari Microsoft, yang menunjukkan Anda harus melepaskan referensi COM secara manual dari .NET tidak benar. Faktanya adalah bahwa .NET runtime dan pengumpul sampah dengan benar melacak dan membersihkan referensi COM. Untuk kode Anda, ini berarti Anda dapat menghapus seluruh loop `while (...) di bagian atas.

Kedua, jika Anda ingin memastikan bahwa referensi COM ke objek COM out-of-proses dibersihkan ketika proses Anda berakhir (sehingga proses Excel akan menutup), Anda perlu memastikan bahwa pengumpul sampah berjalan. Anda melakukan ini dengan benar dengan panggilan ke GC.Collect()dan GC.WaitForPendingFinalizers(). Memanggil ini dua kali aman, dan memastikan bahwa siklus pasti dibersihkan juga (meskipun saya tidak yakin itu diperlukan, dan akan menghargai contoh yang menunjukkan ini).

Ketiga, ketika berjalan di bawah debugger, referensi lokal akan tetap hidup sampai akhir metode (sehingga pemeriksaan variabel lokal bekerja). Jadi GC.Collect()panggilan tidak efektif untuk membersihkan objek seperti rng.Cellsdari metode yang sama. Anda harus membagi kode melakukan COM interop dari pembersihan GC menjadi metode terpisah. (Ini adalah penemuan kunci bagi saya, dari satu bagian dari jawaban yang diposting di sini oleh @nightcoder.)

Jadi, pola umumnya adalah:

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

Ada banyak informasi palsu dan kebingungan tentang masalah ini, termasuk banyak posting di MSDN dan Stack Overflow (dan terutama pertanyaan ini!).

Apa yang akhirnya meyakinkan saya untuk melihat lebih dekat dan mencari tahu saran yang tepat adalah posting blog Marshal.ReleaseComObject Dianggap Berbahaya bersama-sama dengan menemukan masalah dengan referensi tetap hidup di bawah debugger yang membingungkan pengujian saya sebelumnya.

Pemerintah
sumber
5
Hampir SEMUA JAWABAN PADA HALAMAN INI SALAH KECUALI SATU INI. Mereka bekerja tetapi terlalu banyak bekerja. MENGABAIKAN aturan "2 DOTS". Biarkan GC melakukan pekerjaan Anda untuk Anda. Bukti tambahan dari .Net GURU Hans Passant: stackoverflow.com/a/25135685/3852958
Alan Baljeu
1
Govert, sungguh melegakan menemukan cara yang benar untuk melakukannya. Terima kasih.
Dietrich Baumgarten
18

Saya menemukan templat generik yang berguna yang dapat membantu menerapkan pola pembuangan yang benar untuk objek COM, yang membutuhkan Marshal.ReleaseComObject dipanggil ketika mereka keluar dari ruang lingkup:

Pemakaian:

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

Templat:

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

Referensi:

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

Edward Wilde
sumber
3
yup yang ini bagus. Saya pikir kode ini dapat diperbarui meskipun sekarang FinalReleaseCOMObject tersedia.
Tipe Anonim
Masih menjengkelkan memiliki blok penggunaan untuk setiap objek. Lihat di sini stackoverflow.com/questions/2191489/... untuk alternatif.
Henrik
16

Saya tidak percaya masalah ini telah menghantui dunia selama 5 tahun .... Jika Anda telah membuat aplikasi, Anda harus mematikannya terlebih dahulu sebelum menghapus tautan.

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

saat menutup

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

Ketika Anda baru aplikasi excel, itu membuka program excel di latar belakang. Anda perlu memerintahkan program excel untuk berhenti sebelum Anda melepaskan tautan karena program excel itu bukan bagian dari kontrol langsung Anda. Karena itu, ia akan tetap terbuka jika tautannya dirilis!

Pemrograman yang bagus, semuanya ~~

Colin
sumber
Tidak, Anda tidak, terutama jika Anda ingin mengizinkan interaksi pengguna dengan aplikasi COM (Excel dalam kasus ini - OP tidak menentukan apakah interaksi pengguna dimaksudkan, tetapi tidak membuat referensi untuk mematikannya). Anda hanya perlu membuat penelepon melepaskan pengguna agar dapat keluar dari Excel saat menutup, katakanlah, satu file.
downwitch
ini bekerja dengan baik untuk saya - crash ketika saya hanya punya objExcel.Quit (), tetapi ketika saya juga melakukan objExcel.Application.Quit () sebelumnya, ditutup dengan bersih.
mcmillab
13

Pengembang umum, tidak ada solusi yang bekerja untuk saya, jadi saya memutuskan untuk menerapkan trik baru .

Pertama mari kita tentukan "Apa tujuan kita?" => "Tidak melihat objek excel setelah pekerjaan kami di task manager"

Baik. Biarkan tidak untuk menantang dan mulai menghancurkannya, tetapi pertimbangkan untuk tidak menghancurkan contoh lain os Excel yang berjalan secara paralel.

Jadi, dapatkan daftar prosesor saat ini dan ambil PID dari proses EXCEL, maka setelah pekerjaan Anda selesai, kami memiliki tamu baru dalam daftar proses dengan PID unik, temukan dan hancurkan yang itu.

<perlu diingat setiap proses excel baru selama pekerjaan excel Anda akan terdeteksi sebagai baru dan dihancurkan> <Solusi yang lebih baik adalah dengan menangkap PID objek excel yang baru dibuat dan menghancurkannya>

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

Ini menyelesaikan masalah saya, harap Anda juga.

Mohsen Afshin
sumber
1
.... ini sedikit pendekatan sledghammer? - Ini mirip dengan apa yang saya juga gunakan :( tetapi perlu diubah. Masalah dengan menggunakan pendekatan ini adalah bahwa sering ketika membuka excel, pada mesin tempat ini berjalan, ia memiliki peringatan di bar sebelah kiri, mengatakan sesuatu seperti "Excel ditutup dengan tidak benar": tidak terlalu elegan. Saya pikir salah satu saran sebelumnya di utas ini lebih disukai.
whytheq
11

Ini sepertinya terlalu rumit. Dari pengalaman saya, hanya ada tiga hal utama untuk membuat Excel menutup dengan benar:

1: pastikan tidak ada referensi yang tersisa untuk aplikasi excel yang Anda buat (Anda seharusnya hanya punya satu; setel ke null)

2: panggilan GC.Collect()

3: Excel harus ditutup, baik oleh pengguna secara manual menutup program, atau dengan Anda memanggil Quitobjek Excel. (Catatan yang Quitakan berfungsi sama seperti jika pengguna mencoba untuk menutup program, dan akan menyajikan dialog konfirmasi jika ada perubahan yang belum disimpan, bahkan jika Excel tidak terlihat. Pengguna dapat menekan membatalkan, dan kemudian Excel tidak akan ditutup. )

1 harus terjadi sebelum 2, tetapi 3 bisa terjadi kapan saja.

Salah satu cara untuk mengimplementasikan ini adalah dengan membungkus objek Excel interop dengan kelas Anda sendiri, membuat instance interop di konstruktor, dan mengimplementasikan IDisposable dengan Buang terlihat seperti

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

Itu akan membersihkan excel dari sisi program Anda. Setelah Excel ditutup (secara manual oleh pengguna atau dengan Anda menelepon Quit) proses akan hilang. Jika program sudah ditutup, maka prosesnya akan hilang pada GC.Collect()panggilan.

(Saya tidak yakin seberapa penting hal itu, tetapi Anda mungkin ingin GC.WaitForPendingFinalizers()panggilan setelah GC.Collect()panggilan tetapi tidak sepenuhnya diperlukan untuk menyingkirkan proses Excel.)

Ini berhasil bagi saya tanpa masalah selama bertahun-tahun. Perlu diingat bahwa meskipun ini berfungsi, Anda benar-benar harus menutup dengan anggun agar bisa berfungsi. Anda masih akan mendapatkan akumulasi proses excel.exe jika Anda menghentikan program Anda sebelum Excel dibersihkan (biasanya dengan menekan "stop" saat program Anda sedang di-debug).

Dave Cousineau
sumber
1
Jawaban ini berfungsi dan jauh lebih nyaman daripada jawaban yang diterima. Jangan khawatir tentang "dua titik" dengan objek COM, tidak ada gunanya Marshal.
andrew.cuthbert
9

Untuk menambah alasan mengapa Excel tidak menutup, bahkan ketika Anda membuat refrences langsung ke setiap objek setelah dibaca, kreasi, adalah loop 'Untuk'.

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
sumber
Dan alasan lain, menggunakan kembali referensi tanpa melepaskan nilai sebelumnya terlebih dahulu.
Grimfort
Terima kasih. Saya juga menggunakan "untuk setiap" solusi Anda bekerja untuk saya.
Chad Braun-Duin
+1 Terima kasih. Plus, perhatikan bahwa jika Anda memiliki objek untuk rentang Excel, dan Anda ingin mengubah rentang selama masa pakai objek, saya menemukan bahwa saya harus melepaskan ReleaseComObject sebelum menetapkan ulang, yang membuat kode sedikit berantakan!
AjV Jsy
9

Saya secara tradisional mengikuti saran yang ditemukan dalam jawaban VVS . Namun, dalam upaya untuk menjaga jawaban ini tetap terkini dengan opsi terbaru, saya pikir semua proyek masa depan saya akan menggunakan perpustakaan "NetOffice".

NetOffice adalah pengganti lengkap untuk PIA Office dan sepenuhnya versi-agnostik. Ini adalah kumpulan pembungkus Managed COM yang dapat menangani pembersihan yang sering menyebabkan sakit kepala saat bekerja dengan Microsoft Office di .NET.

Beberapa fitur utama adalah:

  • Sebagian besar versi-independen (dan fitur tergantung versi didokumentasikan)
  • Tidak ada ketergantungan
  • Tidak ada PIA
  • Tidak ada pendaftaran
  • Tidak ada VSTO

Saya sama sekali tidak berafiliasi dengan proyek; Saya benar-benar menghargai pengurangan tajam pada sakit kepala.

BTownTKD
sumber
2
Ini harus ditandai sebagai jawabannya, sungguh. NetOffice memisahkan semua kompleksitas ini.
C. Augusto Proiete
1
Saya telah menggunakan NetOffice untuk waktu yang cukup lama menulis add-in excel dan bekerja dengan sempurna. Satu-satunya hal yang perlu dipertimbangkan adalah bahwa jika Anda tidak membuang objek yang digunakan secara eksplisit, itu akan melakukannya ketika Anda keluar dari aplikasi (karena itu tetap melacaknya). Jadi aturan praktis dengan NetOffice selalu menggunakan pola "menggunakan" dengan setiap objek Excel seperti sel, rentang, atau sheet, dll.
Stas Ivanov
8

Jawaban yang diterima di sini benar, tetapi juga perhatikan bahwa tidak hanya referensi "dua titik" perlu dihindari, tetapi juga objek yang diambil melalui indeks. Anda juga tidak perlu menunggu sampai Anda selesai dengan program untuk membersihkan objek-objek ini, yang terbaik adalah membuat fungsi yang akan membersihkannya segera setelah Anda selesai menggunakannya, jika memungkinkan. Berikut adalah fungsi yang saya buat yang memberikan beberapa properti objek Style yang disebut 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 { } 
}

Perhatikan bahwa saya harus mengatur xlBorders[Excel.XlBordersIndex.xlEdgeBottom]ke variabel untuk membersihkannya (Bukan karena dua titik, yang merujuk pada enumerasi yang tidak perlu dilepaskan, tetapi karena objek yang saya maksud sebenarnya adalah objek Border yang perlu dirilis).

Hal semacam ini tidak benar-benar diperlukan dalam aplikasi standar, yang melakukan pekerjaan besar membersihkan setelah diri mereka sendiri, tetapi dalam aplikasi ASP.NET, jika Anda melewatkan salah satu dari ini, tidak peduli seberapa sering Anda memanggil pengumpul sampah, Excel akan masih berjalan di server Anda.

Dibutuhkan banyak perhatian terhadap detail dan banyak pelaksanaan pengujian saat memantau Manajer Tugas saat menulis kode ini, tetapi hal itu menghemat kerumitan mencari halaman kode untuk menemukan satu contoh yang Anda lewatkan. Ini sangat penting saat bekerja dalam loop, di mana Anda perlu melepaskan SETIAP INSTAN objek, meskipun menggunakan nama variabel yang sama setiap kali loop.

Chris McGrath
sumber
Inside Release, periksa Null (jawaban Joe). Itu akan menghindari pengecualian nol yang tidak perlu. Saya telah menguji BANYAK metode. Ini adalah satu-satunya cara untuk melepaskan Excel secara efektif.
Gerhard Powell
8

Setelah mencoba

  1. Lepaskan objek COM dalam urutan terbalik
  2. Tambahkan GC.Collect()dan GC.WaitForPendingFinalizers()dua kali di akhir
  3. Tidak lebih dari dua titik
  4. Tutup buku kerja dan keluar dari aplikasi
  5. Jalankan dalam mode rilis

solusi terakhir yang bekerja untuk saya adalah memindahkan satu set

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

yang kami tambahkan di akhir fungsi ke pembungkus, sebagai berikut:

private void FunctionWrapper(string sourcePath, string targetPath)
{
    try
    {
        FunctionThatCallsExcel(sourcePath, targetPath);
    }
    finally
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}
Ditjen
sumber
Saya menemukan saya tidak perlu membatalkan ComObjects, hanya Berhenti () - Tutup () dan FinalReleaseComObject. Saya tidak percaya ini semua yang hilang pada saya untuk membuatnya bekerja. Bagus!
Carol
7

Saya mengikuti ini persis ... Tapi saya masih mengalami masalah 1 dari 1000 kali. Siapa yang tahu kenapa. Saatnya mengeluarkan palu ...

Tepat setelah kelas Aplikasi Excel yang dipakai saya mendapatkan proses Excel yang baru saja dibuat.

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

Kemudian setelah saya melakukan semua pembersihan COM di atas, saya memastikan bahwa prosesnya tidak berjalan. Jika masih berjalan, bunuh saja!

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

¨ ° º¤ø „¸ Tembak Excel proc dan kunyah permen karet ¸„ ø¤º ° ¨

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
sumber
6

Anda harus menyadari bahwa Excel sangat sensitif terhadap budaya yang Anda jalankan juga.

Anda mungkin menemukan bahwa Anda perlu mengatur budaya ke EN-US sebelum memanggil fungsi Excel. Ini tidak berlaku untuk semua fungsi - tetapi beberapa di antaranya.

    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;

Ini berlaku bahkan jika Anda menggunakan VSTO.

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


sumber
6

"Jangan pernah menggunakan dua titik dengan objek COM" adalah aturan praktis yang bagus untuk menghindari kebocoran referensi COM, tetapi Excel PIA dapat menyebabkan kebocoran dalam lebih banyak cara daripada yang terlihat pada pandangan pertama.

Salah satu cara ini adalah berlangganan ke setiap peristiwa yang diekspos oleh salah satu objek COM model objek Excel.

Misalnya, berlangganan acara WorkbookOpen kelas Aplikasi.

Beberapa teori tentang peristiwa COM

Kelas COM mengekspos sekelompok acara melalui antarmuka panggilan balik. Untuk berlangganan acara, kode klien dapat dengan mudah mendaftarkan objek yang mengimplementasikan antarmuka panggilan balik dan kelas COM akan memanggil metode-metodenya dalam menanggapi peristiwa tertentu. Karena antarmuka panggilan balik adalah antarmuka COM, itu adalah tugas dari objek pelaksana untuk mengurangi jumlah referensi dari objek COM yang diterimanya (sebagai parameter) untuk salah satu penangan acara.

Bagaimana Excel PIA mengekspos Acara COM

Excel PIA memperlihatkan peristiwa COM dari kelas Aplikasi Excel sebagai peristiwa .NET konvensional. Setiap kali berlangganan kode klien untuk acara NET (penekanan pada 'a'), PIA menciptakan sebuah instance dari kelas yang mengimplementasikan interface panggilan kembali dan register dengan Excel.

Oleh karena itu, sejumlah objek panggilan kembali didaftarkan dengan Excel sebagai tanggapan atas permintaan berlangganan yang berbeda dari kode .NET. Satu objek panggilan balik per langganan acara.

Antarmuka panggilan balik untuk penanganan acara berarti bahwa, PIA harus berlangganan ke semua acara antarmuka untuk setiap permintaan berlangganan .NET event. Itu tidak bisa memilih. Saat menerima panggilan balik acara, objek panggilan balik akan memeriksa apakah penangan peristiwa .NET terkait tertarik dengan peristiwa saat ini atau tidak dan kemudian memanggil penangan atau secara diam-diam mengabaikan panggilan balik.

Efek pada jumlah referensi instance COM

Semua objek panggilan balik ini tidak mengurangi jumlah referensi dari objek COM yang mereka terima (sebagai parameter) untuk salah satu metode panggilan balik (bahkan untuk yang diabaikan secara diam-diam). Mereka hanya mengandalkan pengumpul sampah CLR untuk membebaskan objek COM.

Karena GC run bersifat non-deterministik, ini dapat menyebabkan penangguhan proses Excel untuk durasi yang lebih lama dari yang diinginkan dan menciptakan kesan 'kebocoran memori'.

Larutan

Satu-satunya solusi pada saat ini adalah dengan menghindari penyedia acara PIA untuk kelas COM dan menulis penyedia acara Anda sendiri yang secara pasti melepaskan objek COM.

Untuk kelas Aplikasi, ini bisa dilakukan dengan mengimplementasikan antarmuka AppEvents dan kemudian mendaftarkan implementasi dengan Excel dengan menggunakan antarmuka IConnectionPointContainer . Kelas Aplikasi (dan dalam hal ini semua objek COM yang mengekspos peristiwa menggunakan mekanisme panggilan balik) mengimplementasikan antarmuka IConnectionPointContainer.

Amit Mittal
sumber
4

Seperti yang telah ditunjukkan orang lain, Anda perlu membuat referensi eksplisit untuk setiap objek Excel yang Anda gunakan, dan memanggil Marshal.ReleaseComObject pada referensi itu, seperti yang dijelaskan dalam artikel KB ini . Anda juga perlu menggunakan try / akhirnya untuk memastikan ReleaseComObject selalu dipanggil, bahkan ketika ada pengecualian. Yaitu alih-alih:

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

Anda perlu melakukan sesuatu seperti:

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

Anda juga perlu memanggil Application.Quit sebelum melepaskan objek Aplikasi jika Anda ingin Excel ditutup.

Seperti yang Anda lihat, ini dengan cepat menjadi sangat sulit segera setelah Anda mencoba melakukan sesuatu yang bahkan cukup kompleks. Saya telah berhasil mengembangkan aplikasi .NET dengan kelas pembungkus sederhana yang membungkus beberapa manipulasi sederhana dari model objek Excel (buka buku kerja, tulis ke Range, simpan / tutup buku kerja dll). Kelas wrapper mengimplementasikan IDisposable, mengimplementasikan Marshal dengan hati-hati. ReleaseComObject pada setiap objek yang digunakannya, dan tidak secara terbuka mengekspos objek Excel apa pun ke seluruh aplikasi.

Tetapi pendekatan ini tidak sesuai untuk persyaratan yang lebih kompleks.

Ini adalah kekurangan besar .NET COM Interop. Untuk skenario yang lebih kompleks, saya serius mempertimbangkan untuk menulis ActiveX DLL di VB6 atau bahasa tidak terkelola lainnya di mana Anda dapat mendelegasikan semua interaksi dengan objek COM out-proc seperti Office. Anda kemudian dapat mereferensikan ActiveX DLL ini dari aplikasi .NET Anda, dan segalanya akan jauh lebih mudah karena Anda hanya perlu merilis referensi yang satu ini.

Joe
sumber
3

Ketika semua hal di atas tidak berhasil, coba beri Excel waktu untuk menutup lembarannya:

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

...
FinalReleaseComObject(app);
manusia laba-laba
sumber
2
Aku benci menunggu sembarangan. Tapi +1 karena Anda benar tentang keinginan untuk menunggu buku kerja ditutup. Alternatifnya adalah polling koleksi workbook dalam satu loop dan menggunakan tunggu sembarang sebagai loop timeout.
dFlat
3

Pastikan Anda melepaskan semua objek yang terkait dengan Excel!

Saya menghabiskan beberapa jam dengan mencoba beberapa cara. Semua adalah ide-ide bagus tetapi saya akhirnya menemukan kesalahan saya: Jika Anda tidak melepaskan semua objek, tidak ada cara di atas yang dapat membantu Anda seperti dalam kasus saya. Pastikan Anda melepaskan semua objek termasuk rentang satu!

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

Pilihannya adalah bersama di sini .

Ned
sumber
Poin yang sangat bagus! Saya pikir karena saya menggunakan Office 2007 ada beberapa pembersihan dilakukan atas nama saya. Saya telah menggunakan saran lebih lanjut tetapi belum menyimpan variabel seperti yang Anda sarankan di sini dan EXCEL.EXE tidak keluar tetapi saya hanya bisa beruntung dan jika saya memiliki masalah lebih lanjut saya pasti akan melihat bagian ini dari kode saya =)
Coops
3

Artikel hebat tentang pelepasan objek COM adalah 2.5 Melepaskan Objek COM (MSDN).

Metode yang saya sarankan adalah untuk membatalkan referensi Excel.Interop Anda jika mereka adalah variabel non-lokal, dan kemudian memanggil GC.Collect()dan GC.WaitForPendingFinalizers()dua kali. Variabel Interop yang dicakup secara lokal akan dijaga secara otomatis.

Ini menghilangkan kebutuhan untuk menyimpan referensi bernama untuk setiap objek COM.

Berikut ini contoh yang diambil dari artikel:

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

Kata-kata ini langsung dari artikel:

Di hampir semua situasi, membatalkan referensi RCW dan memaksa pengumpulan sampah akan membersihkan dengan benar. Jika Anda juga memanggil GC.WaitForPendingFinalizers, pengumpulan sampah akan sama deterministiknya dengan Anda. Artinya, Anda akan cukup yakin kapan objek telah dibersihkan — saat kembali dari panggilan kedua ke WaitForPendingFinalizers. Sebagai alternatif, Anda dapat menggunakan Marshal.ReleaseComObject. Namun, perhatikan bahwa Anda sangat tidak mungkin perlu menggunakan metode ini.

Porkbutts
sumber
2

Aturan dua titik tidak bekerja untuk saya. Dalam kasus saya, saya membuat metode untuk membersihkan sumber daya saya sebagai berikut:

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

Solusi saya

[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
sumber
2

Anda harus sangat berhati-hati menggunakan aplikasi interop Word / Excel. Setelah mencoba semua solusi, kami masih memiliki banyak proses "WinWord" yang dibiarkan terbuka di server (dengan lebih dari 2000 pengguna).

Setelah mengatasi masalah selama berjam-jam, saya menyadari bahwa jika saya membuka lebih dari beberapa dokumen menggunakan Word.ApplicationClass.Document.Open() pada utas yang berbeda secara bersamaan, proses pekerja IIS (w3wp.exe) akan macet meninggalkan semua proses WinWord terbuka!

Jadi saya kira tidak ada solusi absolut untuk masalah ini, tetapi beralih ke metode lain seperti pengembangan Office Open XML .

Arvand
sumber
1

Jawaban yang diterima tidak berhasil untuk saya. Kode berikut dalam destructor melakukan pekerjaan.

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
sumber
1

Saat ini saya sedang mengerjakan otomatisasi Office dan menemukan solusi untuk ini yang berfungsi setiap saat untuk saya. Ini sederhana dan tidak melibatkan pembunuhan proses apa pun.

Tampaknya dengan hanya mengulang-ulang proses aktif saat ini, dan dengan cara apa pun 'mengakses' proses Excel terbuka, setiap instance Excel yang tersesat akan dihapus. Kode di bawah ini hanya memeriksa proses di mana namanya 'Excel', lalu menulis properti MainWindowTitle dari proses tersebut ke string. 'Interaksi' ini dengan proses tampaknya membuat Windows mengejar dan membatalkan contoh Excel yang beku.

Saya menjalankan metode di bawah ini tepat sebelum add-in di mana saya sedang mengembangkan berhenti, karena itu menyalakannya acara bongkar. Ini menghapus setiap contoh Excel yang menggantung setiap saat. Dalam semua kejujuran saya tidak sepenuhnya yakin mengapa ini bekerja, tetapi itu bekerja dengan baik bagi saya dan dapat ditempatkan di akhir setiap aplikasi Excel tanpa harus khawatir tentang titik ganda, Marshal.ReleaseComObject, atau proses pembunuhan. Saya akan sangat tertarik dengan saran mengapa ini efektif.

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
sumber
1

Saya berpikir bahwa beberapa di antaranya adalah cara framework menangani aplikasi Office, tapi saya bisa saja salah. Pada beberapa hari, beberapa aplikasi segera membersihkan prosesnya, dan beberapa hari lainnya sepertinya menunggu sampai aplikasi ditutup. Secara umum, saya berhenti memperhatikan detail dan hanya memastikan bahwa tidak ada proses tambahan yang beredar di akhir hari.

Juga, dan mungkin saya terlalu menyederhanakan hal, tapi saya pikir Anda bisa ...

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

Seperti yang saya katakan sebelumnya, saya tidak cenderung memperhatikan detail kapan proses Excel muncul atau menghilang, tetapi itu biasanya bekerja untuk saya. Saya juga tidak suka menjaga proses Excel sekitar untuk apa pun selain jumlah waktu minimal, tapi saya mungkin hanya paranoid karenanya.

bill_the_loser
sumber
1

Seperti beberapa mungkin sudah ditulis, bukan hanya penting bagaimana Anda menutup Excel (objek); itu juga penting bagaimana Anda membuka dan juga berdasarkan jenis proyeknya.

Dalam aplikasi WPF, pada dasarnya kode yang sama berfungsi tanpa atau dengan sedikit masalah.

Saya punya proyek di mana file Excel yang sama sedang diproses beberapa kali untuk nilai parameter yang berbeda - misalnya menguraikannya berdasarkan nilai-nilai di dalam daftar generik.

Saya meletakkan semua fungsi yang berhubungan dengan Excel ke dalam kelas dasar, dan parser ke dalam subkelas (parser yang berbeda menggunakan fungsi Excel yang umum). Saya tidak ingin Excel dibuka dan ditutup lagi untuk setiap item dalam daftar generik, jadi saya hanya membukanya sekali di kelas dasar dan menutupnya di subkelas. Saya mengalami masalah saat memindahkan kode ke aplikasi desktop. Saya sudah mencoba banyak solusi yang disebutkan di atas.GC.Collect()sudah diterapkan sebelumnya, dua kali seperti yang disarankan.

Kemudian saya telah memutuskan bahwa saya akan memindahkan kode untuk membuka Excel ke subclass. Alih-alih membuka hanya sekali, sekarang saya membuat objek baru (kelas dasar) dan membuka Excel untuk setiap item dan menutupnya di akhir. Ada beberapa penalti kinerja, tetapi berdasarkan beberapa tes, proses Excel ditutup tanpa masalah (dalam mode debug), demikian juga file sementara dihapus. Saya akan melanjutkan pengujian dan menulis lebih banyak jika saya akan mendapatkan beberapa pembaruan.

Intinya adalah: Anda juga harus memeriksa kode inisialisasi, terutama jika Anda memiliki banyak kelas, dll.

Blaz Brencic
sumber