Làm cách nào để dọn sạch các đối tượng interop Excel đúng cách?


747

Tôi đang sử dụng interop Excel trong C # ( ApplicationClass) và đã đặt đoạn mã sau vào mệnh đề cuối cùng của tôi:

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

Mặc dù loại công trình này, Excel.exe quá trình vẫn ở chế độ nền ngay cả sau khi tôi đóng Excel. Nó chỉ được phát hành khi ứng dụng của tôi được đóng bằng tay.

Tôi đang làm gì sai, hoặc có một sự thay thế để đảm bảo các đối tượng interop được xử lý đúng cách?


1
Bạn đang cố gắng tắt Excel.exe mà không đóng ứng dụng của bạn? Không chắc chắn tôi hoàn toàn hiểu câu hỏi của bạn.
Bryant

3
Tôi đang cố gắng đảm bảo các đối tượng interop không được quản lý được xử lý đúng cách. Vì vậy, không có các quy trình Excel tồn tại ngay cả khi người dùng đã hoàn thành với bảng tính Excel mà chúng tôi đã tạo từ ứng dụng.
HAdes

3
Nếu bạn có thể thử làm điều đó bằng cách tạo các tệp XML Excel, nếu không, vui lòng xem xét VSTO un / Managed
Jeremy Thompson

Điều này có dịch sang Excel độc đáo không?
Coops

2
Xem (bên cạnh câu trả lời bên dưới) bài viết hỗ trợ này của Microsoft, nơi họ đưa ra giải pháp cụ thể cho vấn đề này: support.microsoft.com/kb/317109
Arjan

Câu trả lời:


684

Excel không thoát vì ứng dụng của bạn vẫn đang giữ các tham chiếu đến các đối tượng COM.

Tôi đoán bạn đang gọi ít nhất một thành viên của đối tượng COM mà không gán nó cho một biến.

Đối với tôi, đó là đối tượng excelApp.Worksheet mà tôi trực tiếp sử dụng mà không gán nó cho một biến:

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

Tôi không biết rằng C # bên trong đã tạo một trình bao bọc cho đối tượng COM của Worksheet không được phát hành bởi mã của tôi (vì tôi không biết về nó) và là nguyên nhân khiến Excel không được tải.

Tôi đã tìm thấy giải pháp cho vấn đề của mình trên trang này , cũng có một quy tắc hay cho việc sử dụng các đối tượng COM trong C #:

Không bao giờ sử dụng hai dấu chấm với các đối tượng COM.


Vì vậy, với kiến ​​thức này, cách làm đúng ở trên là:

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

CẬP NHẬT BÀI VIẾT:

Tôi muốn mọi độc giả đọc câu trả lời này của Hans Passant rất cẩn thận vì nó giải thích cái bẫy mà tôi và rất nhiều nhà phát triển khác vấp phải. Khi tôi viết câu trả lời này nhiều năm trước, tôi đã không biết về ảnh hưởng của trình gỡ lỗi đối với người thu gom rác và rút ra kết luận sai. Tôi giữ câu trả lời của mình không thay đổi vì lợi ích của lịch sử nhưng xin vui lòng đọc liên kết này và không đi theo cách "hai dấu chấm": Tìm hiểu bộ sưu tập rác trong .NETDọn dẹp các đối tượng tương tác Excel bằng IDis Dùng


16
Sau đó, tôi đề nghị không sử dụng Excel từ COM và tự cứu mình khỏi mọi rắc rối. Các định dạng Excel 2007 có thể được sử dụng mà không cần mở Excel, tuyệt đẹp.
dùng7116

5
Tôi không hiểu "hai chấm" nghĩa là gì. Bạn có thể vui lòng giải thích?
A9S6

22
Điều này có nghĩa là, bạn không nên sử dụng mẫu comObject.Property.ProperIESProperty (bạn có thấy hai dấu chấm không?). Thay vào đó, hãy gán comObject.Property cho một biến và sử dụng và loại bỏ biến đó. Một phiên bản chính thức hơn của quy tắc trên có thể là sth. như "Gán đối tượng com cho các biến trước khi bạn sử dụng chúng. Điều này bao gồm các đối tượng com là thuộc tính của đối tượng com khác."
VVS

5
@Nick: Trên thực tế, bạn không cần bất kỳ loại dọn dẹp nào, vì người dọn rác sẽ làm việc đó cho bạn. Điều duy nhất bạn cần làm là gán mọi đối tượng COM cho biến riêng của nó để GC biết về nó.
VVS

12
@VSS đó là bollocks, GC dọn sạch mọi thứ vì các biến của trình bao bọc được tạo bởi khung .net. Nó chỉ có thể mất mãi mãi cho GC để làm sạch nó. Gọi GC.Collect sau khi interop nặng không phải là một ý tưởng tồi.
CodingBarfield

280

Bạn thực sự có thể phát hành đối tượng Ứng dụng Excel của mình một cách sạch sẽ, nhưng bạn phải cẩn thận.

Lời khuyên để duy trì một tài liệu tham khảo có tên cho tất cả mọi đối tượng COM mà bạn truy cập và sau đó phát hành nó một cách rõ ràng Marshal.FinalReleaseComObject()là đúng về mặt lý thuyết, nhưng thật không may, rất khó quản lý trong thực tế. Nếu một người trượt bất cứ nơi nào và sử dụng "hai dấu chấm" hoặc lặp lại các ô thông qua mộtfor each vòng lặp hoặc bất kỳ loại lệnh tương tự nào khác, thì bạn sẽ có các đối tượng COM không được ước tính và có nguy cơ bị treo. Trong trường hợp này, sẽ không có cách nào để tìm ra nguyên nhân trong mã; bạn sẽ phải xem xét tất cả mã của mình bằng mắt và hy vọng tìm ra nguyên nhân, một nhiệm vụ gần như không thể đối với một dự án lớn.

Tin tốt là bạn thực sự không phải duy trì một tham chiếu biến có tên cho mọi đối tượng COM bạn sử dụng. Thay vào đó, hãy gọiGC.Collect() và sau đó GC.WaitForPendingFinalizers()giải phóng tất cả các đối tượng (thường là nhỏ) mà bạn không giữ tham chiếu, sau đó giải phóng rõ ràng các đối tượng mà bạn giữ tham chiếu biến có tên.

Bạn cũng nên phát hành các tài liệu tham khảo được đặt tên theo thứ tự quan trọng ngược lại: trước tiên là các đối tượng, sau đó là trang tính, sổ làm việc và cuối cùng là đối tượng Ứng dụng Excel của bạn.

Ví dụ: giả sử rằng bạn có một biến đối tượng Phạm vi được đặt tên xlRng, biến Worksheet có tên xlSheet, biến Workbook có tên xlBookvà biến Ứng dụng Excel có tên xlApp, thì mã dọn dẹp của bạn có thể trông giống như sau:

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

Trong hầu hết các ví dụ mã bạn sẽ thấy để dọn dẹp các đối tượng COM khỏi .NET, các cuộc gọi GC.Collect()GC.WaitForPendingFinalizers()cuộc gọi được thực hiện TWICE như trong:

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

Tuy nhiên, điều này không được yêu cầu trừ khi bạn đang sử dụng Visual Studio Tools cho Office (VSTO), sử dụng các công cụ hoàn thiện khiến toàn bộ đồ thị của các đối tượng được quảng bá trong hàng đợi hoàn thiện. Những đối tượng như vậy sẽ không được phát hành cho đến khi thu gom rác tiếp theo . Tuy nhiên, nếu bạn không sử dụng VSTO, bạn sẽ có thể gọi GC.Collect()GC.WaitForPendingFinalizers() chỉ một lần.

Tôi biết rằng gọi một cách rõ ràng GC.Collect()là không (và chắc chắn thực hiện hai lần nghe có vẻ rất đau đớn), nhưng không có cách nào xung quanh nó, phải trung thực. Thông qua các hoạt động bình thường, bạn sẽ tạo ra các đối tượng ẩn mà bạn không có tài liệu tham khảo nào, do đó, không thể giải phóng thông qua bất kỳ phương tiện nào khác ngoài việc gọiGC.Collect() .

Đây là một chủ đề phức tạp, nhưng đây thực sự là tất cả để có nó. Khi bạn thiết lập mẫu này cho quy trình dọn dẹp của mình, bạn có thể viết mã bình thường mà không cần trình bao bọc, v.v .:-)

Tôi có một hướng dẫn về điều này ở đây:

Tự động hóa các chương trình Office với VB.Net / COM Interop

Nó được viết cho VB.NET, nhưng đừng bỏ qua điều đó, các nguyên tắc hoàn toàn giống như khi sử dụng C #.


3
Một cuộc thảo luận liên quan có thể được tìm thấy trên diễn đàn ExtremeVBTalk .NET Office Automatic, tại đây: xtremevbtalk.com/showthread.php?t=303928 .
Mike Rosenblum

2
Và nếu tất cả các cách khác đều thất bại, thì Process.Kill () có thể được sử dụng (như là phương sách cuối cùng) như được mô tả ở đây: stackoverflow.com/questions/51462/ trộm
Mike Rosenblum

1
Vui mừng vì nó đã làm việc Richard. :-) Và đây là một ví dụ tinh tế khi chỉ cần tránh "hai dấu chấm" là không đủ để ngăn chặn sự cố: stackoverflow.com/questions/4964663/ Lỗi
Mike Rosenblum

Đây là ý kiến ​​về vấn đề này từ Misha Shneerson của Microsoft về chủ đề này, trong ngày bình luận ngày 7 tháng 10 năm 2010 ( blog.msdn.com/b/csharpfaq/archive/2010/09/11/ trên )
Mike Rosenblum

Tôi hy vọng điều này sẽ giúp một vấn đề dai dẳng mà chúng ta đang gặp phải. Có an toàn để thực hiện thu thập rác và giải phóng các cuộc gọi trong một khối cuối cùng không?
Brett Green

213

Lời nói đầu: câu trả lời của tôi chứa hai giải pháp, vì vậy hãy cẩn thận khi đọc và đừng bỏ lỡ điều gì.

Có nhiều cách và lời khuyên khác nhau về cách làm cho cá thể Excel không tải, chẳng hạn như:

  • Phát hành rõ ràng MỌI đối tượng com với Marshal.FinalReleaseComObject () (không quên về các đối tượng com được tạo ngầm). Để phát hành mọi đối tượng com đã tạo, bạn có thể sử dụng quy tắc 2 dấu chấm được đề cập ở đây:
    Làm cách nào để dọn sạch các đối tượng interop Excel đúng cách?

  • Gọi GC.Collect () và GC.WaitForPendingFinalulators () để làm cho CLR giải phóng các đối tượng com không sử dụng * (Trên thực tế, nó hoạt động, xem chi tiết giải pháp thứ hai của tôi)

  • Kiểm tra xem ứng dụng com-server có thể hiển thị hộp thông báo đang chờ người dùng trả lời hay không (mặc dù tôi không chắc nó có thể ngăn Excel đóng cửa không, nhưng tôi đã nghe về nó vài lần)

  • Gửi tin nhắn WM_CLOSE đến cửa sổ Excel chính

  • Thực thi chức năng hoạt động với Excel trong một AppDomain riêng. Một số người tin rằng phiên bản Excel sẽ bị đóng, khi AppDomain được tải.

  • Giết tất cả các trường hợp excel đã được khởi tạo sau khi mã xen kẽ excel của chúng tôi bắt đầu.

NHƯNG! Đôi khi tất cả các tùy chọn này chỉ không giúp đỡ hoặc không thể phù hợp!

Ví dụ, hôm qua tôi phát hiện ra rằng trong một trong các hàm của tôi (hoạt động với excel) Excel sẽ tiếp tục chạy sau khi hàm kết thúc. Tôi đã thử mọi cách! Tôi đã kiểm tra kỹ lưỡng toàn bộ hàm 10 lần và thêm Marshal.FinalReleaseComObject () cho mọi thứ! Tôi cũng đã có GC.Collect () và GC.WaitForPendingFinalulators (). Tôi đã kiểm tra các hộp tin nhắn ẩn. Tôi đã cố gắng gửi tin nhắn WM_CLOSE đến cửa sổ Excel chính. Tôi đã thực thi chức năng của mình trong một AppDomain riêng và dỡ tải tên miền đó. Không có gì giúp được! Tùy chọn đóng tất cả các phiên bản excel là không phù hợp, bởi vì nếu người dùng khởi động một phiên bản Excel khác theo cách thủ công, trong khi thực hiện chức năng của tôi cũng hoạt động với Excel, thì phiên bản đó cũng sẽ bị đóng bởi chức năng của tôi. Tôi cá là người dùng sẽ không vui! Vì vậy, thành thật mà nói, đây là một lựa chọn khập khiễng (không có kẻ xúc phạm).Giải pháp : Tiêu diệt quá trình excel bằng hWnd của cửa sổ chính của nó (đó là giải pháp đầu tiên).

Đây là mã đơn giản:

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

Như bạn có thể thấy tôi đã cung cấp hai phương thức, theo mẫu Try-Parse (tôi nghĩ rằng nó phù hợp ở đây): một phương thức không ném ngoại lệ nếu Quy trình không thể bị giết (ví dụ quy trình không còn tồn tại nữa) và một phương thức khác đưa ra ngoại lệ nếu Quá trình không bị giết. Điểm yếu duy nhất trong mã này là quyền bảo mật. Về mặt lý thuyết, người dùng có thể không có quyền để giết quá trình, nhưng trong 99,99% tất cả các trường hợp, người dùng có quyền như vậy. Tôi cũng đã thử nghiệm nó với một tài khoản khách - nó hoạt động hoàn hảo.

Vì vậy, mã của bạn, hoạt động với Excel, có thể trông như thế này:

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 bị chấm dứt! :)

Ok, chúng ta hãy quay trở lại giải pháp thứ hai, như tôi đã hứa ở đầu bài. Giải pháp thứ hai là gọi GC.Collect () và GC.WaitForPendingFinalulators (). Vâng, họ thực sự làm việc, nhưng bạn cần phải cẩn thận ở đây!
Nhiều người nói (và tôi đã nói) rằng gọi GC.Collect () không giúp được gì. Nhưng lý do nó không có ích là nếu vẫn còn các tham chiếu đến các đối tượng COM! Một trong những lý do phổ biến nhất khiến GC.Collect () không hữu ích là chạy dự án ở chế độ Gỡ lỗi. Trong các đối tượng chế độ gỡ lỗi không thực sự được tham chiếu nữa sẽ không được thu gom rác cho đến khi kết thúc phương thức.
Vì vậy, nếu bạn đã thử dùng GC.Collect () và GC.WaitForPendingFinalulators () và nó không có ích, hãy thử làm như sau:

1) Thử chạy dự án của bạn ở chế độ Phát hành và kiểm tra xem Excel có đóng đúng không

2) Gói phương thức làm việc với Excel theo một phương pháp riêng. Vì vậy, thay vì một cái gì đó như thế này:

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

bạn viết:

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

Bây giờ, Excel sẽ đóng =)


19
thật đáng buồn khi chủ đề đã quá cũ đến nỗi câu trả lời xuất sắc của bạn xuất hiện ở phía dưới, mà tôi nghĩ là lý do duy nhất khiến nó không được nâng cấp nhiều lần hơn ...
chiccodoro

15
Tôi phải thừa nhận, khi lần đầu tiên đọc câu trả lời của bạn, tôi đã nghĩ rằng đó là một loại bùn khổng lồ. Sau khoảng 6 giờ vật lộn với điều này (mọi thứ được phát hành, tôi không có dấu chấm kép, v.v.), bây giờ tôi nghĩ câu trả lời của bạn là thiên tài.
Đánh dấu

3
Cám ơn vì cái này. Đã vật lộn với một Excel sẽ không đóng bất kể điều gì trong một vài ngày trước khi tôi tìm thấy nó. Thông minh.
BBlake

2
@nightcoder: câu trả lời tuyệt vời, thực sự chi tiết. Nhận xét của bạn liên quan đến chế độ gỡ lỗi là rất đúng và quan trọng là bạn đã chỉ ra điều này. Mã tương tự trong chế độ phát hành thường có thể tốt. Tuy nhiên, Process.Kill chỉ tốt khi sử dụng tự động hóa, không phải khi chương trình của bạn đang chạy trong Excel, ví dụ như là một bổ trợ COM được quản lý.
Mike Rosenblum

2
@DanM: Cách tiếp cận của bạn là chính xác 100% và sẽ không bao giờ thất bại. Bằng cách giữ tất cả các biến của bạn cục bộ cho phương thức, tất cả các tham chiếu đều không thể truy cập được bằng mã .NET. Điều này có nghĩa là khi phần cuối cùng của bạn gọi GC.Collect (), bộ hoàn thiện cho tất cả các đối tượng COM của bạn sẽ được gọi một cách chắc chắn. Mỗi bộ hoàn thiện sau đó gọi Marshal.FinalReleaseComObject trên đối tượng được hoàn thiện. Cách tiếp cận của bạn là do đó đơn giản và bằng chứng ngu ngốc. Không có sợ sử dụng này. (Chỉ báo trước: nếu sử dụng VSTO, điều mà tôi nghi ngờ là bạn, bạn sẽ cần gọi cho GC.Collect () & GC.WaitForPendingFinalulators TWICE.)
Mike Rosenblum

49

CẬP NHẬT : Đã thêm mã C # và liên kết đến Windows Jobs

Đôi khi tôi đã cố gắng tìm ra vấn đề này và tại thời điểm đó, XtremeVBTalk là hoạt động tích cực và nhạy bén nhất. Đây là một liên kết đến bài viết gốc của tôi, Đóng quy trình Tương tác Excel một cách sạch sẽ, ngay cả khi ứng dụng của bạn gặp sự cố . Dưới đây là một bản tóm tắt của bài viết, và mã được sao chép vào bài viết này.

  • Đóng quá trình Interop với Application.Quit()Process.Kill()hoạt động với hầu hết các phần, nhưng không thành công nếu các ứng dụng gặp sự cố nghiêm trọng. Tức là nếu ứng dụng gặp sự cố, quy trình Excel sẽ vẫn hoạt động lỏng lẻo.
  • Giải pháp là để cho HĐH xử lý việc dọn dẹp các quy trình của bạn thông qua Windows Job Object bằng các cuộc gọi Win32. Khi ứng dụng chính của bạn chết, các quy trình liên quan (ví dụ Excel) cũng sẽ bị chấm dứt.

Tôi thấy đây là một giải pháp sạch vì HĐH đang thực hiện công việc dọn dẹp thực sự. Tất cả bạn phải làm là đăng ký quá trình Excel.

Mã công việc Windows

Kết thúc các lệnh gọi API Win32 để đăng ký các quy trình 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);
    }

}

Lưu ý về mã xây dựng

  • Trong constructor, info.LimitFlags = 0x2000;được gọi là. 0x2000JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSEgiá trị enum và giá trị này được xác định bởi MSDN là:

Làm cho tất cả các quy trình liên quan đến công việc chấm dứt khi xử lý cuối cùng cho công việc được đóng lại.

Gọi API Win32 bổ sung để nhận ID tiến trình (PID)

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

Sử dụng mã

    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
Hoàn toàn giết chết một máy chủ COM ngoài quy trình (có thể đang phục vụ các máy khách COM khác) là một ý tưởng khủng khiếp. Nếu bạn đang dùng đến điều này thì đó là vì giao thức COM của bạn bị hỏng. Máy chủ COM không nên được coi là một ứng dụng cửa sổ thông thường
MickyD

38

Điều này làm việc cho một dự án tôi đang làm việc:

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

Chúng tôi đã học được rằng điều quan trọng là đặt mọi tham chiếu đến một đối tượng COM của Excel thành null khi bạn hoàn thành nó. Điều này bao gồm các tế bào, tấm và tất cả mọi thứ.


30

Bất cứ điều gì trong không gian tên Excel cần phải được phát hành. Giai đoạn = Stage

Bạn không thể làm:

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

Bạn phải làm

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

tiếp theo là việc phát hành các đối tượng.


3
Làm thế nào aobut xlRange.Interior.Color chẳng hạn.
HAdes

Nội thất cần được phát hành (mặt khác trong không gian tên) ... Mặt khác, màu sắc không có (vì nó từ System.Drawing.Color, iirc)
MagicKat

2
Trên thực tế Màu là màu Excel không phải là màu .Net. bạn chỉ cần vượt qua một Long. Ngoài ra sách bài tập def. cần phải được phát hành, bảng tính ... ít như vậy.
Loại ẩn danh

Điều đó không đúng, quy tắc hai chấm, xấu một chấm là vô nghĩa. Ngoài ra, bạn không phải phát hành Workbook, Worksheet, Ranges, đó là công việc của GC. Điều duy nhất mà bạn không bao giờ được quên là gọi Quit trên một và chỉ đối tượng Excel.Application. Sau khi gọi Quit, null đối tượng Excel.Application và gọi GC.Collect (); GC.WaitForPendingFinalulators (); hai lần.
Dietrich Baumgarten

29

Đầu tiên - bạn không bao giờ phải gọi Marshal.ReleaseComObject(...)hoặc Marshal.FinalReleaseComObject(...)khi thực hiện xen kẽ Excel. Đây là một kiểu chống khó hiểu, nhưng bất kỳ thông tin nào về điều này, bao gồm từ Microsoft, cho thấy bạn phải phát hành thủ công các tham chiếu COM từ .NET là không chính xác. Thực tế là bộ thực thi .NET và trình thu gom rác theo dõi chính xác và dọn sạch các tham chiếu COM. Đối với mã của bạn, điều này có nghĩa là bạn có thể loại bỏ toàn bộ vòng lặp `while (...) ở trên cùng.

Thứ hai, nếu bạn muốn đảm bảo rằng các tham chiếu COM đến một đối tượng COM ngoài quy trình sẽ được dọn sạch khi quá trình của bạn kết thúc (để quy trình Excel sẽ đóng), bạn cần đảm bảo rằng trình thu gom rác chạy. Bạn làm điều này một cách chính xác với các cuộc gọi đến GC.Collect()GC.WaitForPendingFinalizers(). Gọi hai lần này là an toàn và đảm bảo rằng các chu kỳ chắc chắn cũng được dọn sạch (mặc dù tôi không chắc là cần thiết và sẽ đánh giá cao một ví dụ cho thấy điều này).

Thứ ba, khi chạy theo trình gỡ lỗi, các tham chiếu cục bộ sẽ được duy trì một cách giả tạo cho đến khi kết thúc phương thức (để kiểm tra biến cục bộ hoạt động). Vì vậy, GC.Collect()các cuộc gọi không hiệu quả để làm sạch đối tượng như rng.Cellstừ cùng một phương thức. Bạn nên tách mã thực hiện xen kẽ COM từ dọn dẹp GC thành các phương thức riêng biệt. (Đây là một khám phá quan trọng đối với tôi, từ một phần của câu trả lời được đăng ở đây bởi @nightcoder.)

Do đó, mô hình chung sẽ là:

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

Có rất nhiều thông tin sai lệch và nhầm lẫn về vấn đề này, bao gồm nhiều bài đăng trên MSDN và trên Stack Overflow (và đặc biệt là câu hỏi này!).

Điều cuối cùng đã thuyết phục tôi để có cái nhìn gần hơn và tìm ra lời khuyên đúng đắn là bài đăng trên blog Marshal.ReleaseComObject Được coi là Nguy hiểm cùng với việc tìm ra vấn đề với các tài liệu tham khảo còn tồn tại dưới trình gỡ lỗi gây nhầm lẫn cho thử nghiệm trước đây của tôi.


5
Hầu như TẤT CẢ CÁC TRẢ LỜI TRÊN TRANG NÀY ĐANG TUYỆT VỜI MỘT LẦN NÀY. Họ làm việc nhưng cách làm việc quá nhiều. VƯỢT QUA quy tắc "2 DOTS". Hãy để GC làm việc cho bạn. Bằng chứng bổ sung từ .Net GURU Hans Passant: stackoverflow.com/a/25135685/3852958
Alan Baljeu

1
Chính phủ, thật là nhẹ nhõm để tìm ra cách chính xác để làm điều đó. Cảm ơn bạn.
Dietrich Baumgarten

18

Tôi đã tìm thấy một mẫu chung hữu ích có thể giúp triển khai mẫu xử lý chính xác cho các đối tượng COM, cần Marshal.ReleaseComObject được gọi khi chúng đi ra khỏi phạm vi:

Sử dụng:

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

Bản mẫu:

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

Tài liệu tham khảo:

http://www.deez.info/sengelha/2005/02/11/usiously-idis Dùng- class-3-autoreleasecomobject /


3
yup cái này là tốt Tôi nghĩ rằng mã này có thể được cập nhật mặc dù bây giờ FinalReleaseCOMObject đã có sẵn.
Loại ẩn danh

Vẫn còn khó chịu khi có một khối sử dụng cho từng đối tượng. Xem tại đây stackoverflow.com/questions/2191361/ khăn để thay thế.
Henrik

16

Tôi không thể tin rằng vấn đề này đã ám ảnh thế giới trong 5 năm .... Nếu bạn đã tạo một ứng dụng, bạn cần tắt nó trước khi xóa liên kết.

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

khi đóng cửa

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

Khi bạn mới tạo một ứng dụng excel, nó sẽ mở một chương trình excel trong nền. Bạn cần ra lệnh cho chương trình excel đó thoát ra trước khi bạn phát hành liên kết vì chương trình excel đó không nằm trong sự kiểm soát trực tiếp của bạn. Do đó, nó sẽ vẫn mở nếu liên kết được phát hành!

Mọi người lập trình tốt ~ ~


Không, bạn không, đặc biệt là nếu bạn muốn cho phép người dùng tương tác với ứng dụng COM (Excel trong trường hợp này - OP không chỉ định liệu có tương tác với người dùng hay không, nhưng không tham chiếu đến việc tắt nó). Bạn chỉ cần yêu cầu người gọi cho phép người dùng có thể thoát Excel khi đóng, giả sử, một tệp duy nhất.
downwitch

điều này hoạt động hoàn hảo với tôi - đã bị sập khi tôi chỉ có objExcel.Quit (), nhưng khi tôi cũng đã làm objExcel.Application.Quit (), đóng lại sạch sẽ.
mcmillab

13

Các nhà phát triển thông thường, không có giải pháp nào của bạn làm việc cho tôi, vì vậy tôi quyết định thực hiện một thủ thuật mới .

Trước tiên hãy xác định "Mục tiêu của chúng tôi là gì?" => "Không thấy đối tượng excel sau công việc của chúng tôi trong trình quản lý tác vụ"

Đồng ý. Không được thách thức và bắt đầu phá hủy nó, nhưng xem xét không phá hủy phiên bản os Excel khác đang chạy song song.

Vì vậy, hãy lấy danh sách các bộ xử lý hiện tại và tìm nạp PID của các quy trình EXCEL, sau khi hoàn thành công việc của bạn, chúng tôi có một vị khách mới trong danh sách các quy trình với một PID duy nhất, tìm và hủy chỉ một quy trình đó.

<hãy ghi nhớ bất kỳ quy trình excel mới nào trong công việc excel của bạn sẽ được phát hiện là mới và bị phá hủy> <Một giải pháp tốt hơn là bắt giữ PID của đối tượng excel mới được tạo và chỉ cần phá hủy>

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

Điều này giải quyết vấn đề của tôi, hy vọng bạn cũng vậy.


1
.... đây là một chút của một cách tiếp cận sledghammer? - nó tương tự như những gì tôi cũng đang sử dụng :( nhưng cần thay đổi. Vấn đề với việc sử dụng phương pháp này là thường khi mở excel, trên máy chạy cái này, nó có cảnh báo ở thanh bên trái, nói điều gì đó như "Excel đã bị đóng không chính xác": không thanh lịch lắm. Tôi nghĩ một trong những gợi ý trước đó trong chủ đề này là được ưu tiên.
whytheq

11

Điều này có vẻ như nó đã quá phức tạp. Theo kinh nghiệm của tôi, chỉ có ba điều quan trọng để khiến Excel đóng đúng cách:

1: đảm bảo không còn tài liệu tham khảo nào cho ứng dụng excel mà bạn đã tạo (dù sao bạn cũng chỉ nên có một tài liệu tham khảo; đặt nó thành null)

2: gọi GC.Collect()

3: Excel phải được đóng, do người dùng đóng chương trình theo cách thủ công hoặc bằng cách bạn gọi Quittrên đối tượng Excel. (Lưu ý rằng nó Quitsẽ hoạt động giống như khi người dùng cố đóng chương trình và sẽ đưa ra hộp thoại xác nhận nếu có thay đổi chưa được lưu, ngay cả khi Excel không hiển thị. Người dùng có thể nhấn hủy và sau đó Excel sẽ không bị đóng. )

1 cần phải xảy ra trước 2, nhưng 3 có thể xảy ra bất cứ lúc nào.

Một cách để thực hiện điều này là bọc đối tượng Excel xen kẽ với lớp của riêng bạn, tạo cá thể interop trong hàm tạo và triển khai IDis Dùng với Vứt bỏ trông giống như

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

Điều đó sẽ dọn sạch excel từ phía chương trình của bạn. Khi Excel được đóng (do người dùng hoặc bạn gọi bằng tay Quit), quy trình sẽ biến mất. Nếu chương trình đã bị đóng, thì quá trình sẽ biến mất trong GC.Collect()cuộc gọi.

(Tôi không chắc tầm quan trọng của nó, nhưng bạn có thể muốn một GC.WaitForPendingFinalizers()cuộc gọi sau GC.Collect()cuộc gọi nhưng không nhất thiết phải loại bỏ quy trình Excel.)

Điều này đã làm việc cho tôi mà không có vấn đề trong nhiều năm. Hãy nhớ rằng mặc dù điều này hoạt động, bạn thực sự phải đóng một cách duyên dáng để nó hoạt động. Bạn vẫn sẽ tích lũy các quy trình excel.exe nếu bạn làm gián đoạn chương trình của mình trước khi Excel được dọn sạch (thường bằng cách nhấn "dừng" trong khi chương trình của bạn đang được gỡ lỗi).


1
Câu trả lời này hoạt động và vô cùng thuận tiện hơn câu trả lời được chấp nhận. Không phải lo lắng về "hai dấu chấm" với các đối tượng COM, không sử dụng Marshal.
andrew.cuthbert

9

Để thêm vào lý do tại sao Excel không đóng, ngay cả khi bạn tạo các điều chỉnh trực tiếp cho từng đối tượng khi đọc, tạo, là vòng lặp '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

Và một lý do khác, sử dụng lại một tài liệu tham khảo mà không phát hành giá trị trước đó.
Grimfort

Cảm ơn bạn. Tôi cũng đang sử dụng "cho mỗi" giải pháp của bạn làm việc cho tôi.
Chad Braun-Duin

+1 Cảm ơn. Ngoài ra, lưu ý rằng nếu bạn có một đối tượng cho phạm vi Excel và bạn muốn thay đổi phạm vi trong suốt thời gian tồn tại của đối tượng, tôi thấy rằng tôi phải phát hànhComCombbject trước khi gán lại nó, điều này làm cho mã không gọn gàng!
AjV Jsy

9

Theo truyền thống, tôi đã làm theo lời khuyên trong câu trả lời của VVS . Tuy nhiên, trong nỗ lực để giữ cho câu trả lời này được cập nhật với các tùy chọn mới nhất, tôi nghĩ rằng tất cả các dự án trong tương lai của tôi sẽ sử dụng thư viện "NetScript".

NetScript là một sự thay thế hoàn toàn cho các PIA Office và hoàn toàn không theo phiên bản. Đó là một tập hợp các trình bao bọc COM được quản lý có thể xử lý việc dọn dẹp thường gây ra những cơn đau đầu như vậy khi làm việc với Microsoft Office trong .NET.

Một số tính năng chính là:

  • Chủ yếu là độc lập với phiên bản (và các tính năng phụ thuộc phiên bản được ghi lại)
  • Không phụ thuộc
  • Không có PIA
  • Không đăng ký
  • Không có VSTO

Tôi không có cách nào liên kết với dự án; Tôi thực sự đánh giá cao sự giảm đau đầu rõ rệt.


2
Điều này nên được đánh dấu là câu trả lời, thực sự. NetScript trừu tượng hóa tất cả sự phức tạp này đi.
C. Augusto Proiete

1
Tôi đã sử dụng NetScript trong một khoảng thời gian đáng kể để viết một bổ trợ excel và nó hoạt động hoàn hảo. Điều duy nhất cần xem xét là nếu bạn không loại bỏ các đối tượng đã sử dụng một cách rõ ràng, nó sẽ thực hiện khi bạn thoát khỏi ứng dụng (vì dù sao nó vẫn theo dõi chúng). Vì vậy, quy tắc chung với NetScript là luôn sử dụng mẫu "sử dụng" với mọi đối tượng Excel như ô, phạm vi hoặc trang tính, v.v.
Stas Ivanov

8

Câu trả lời được chấp nhận ở đây là chính xác, nhưng cũng lưu ý rằng không chỉ cần tránh các tham chiếu "hai dấu chấm", mà cả các đối tượng được truy xuất thông qua chỉ mục. Bạn cũng không cần đợi cho đến khi kết thúc chương trình để dọn sạch các đối tượng này, tốt nhất là tạo các chức năng sẽ dọn sạch chúng ngay khi bạn hoàn thành với chúng, khi có thể. Đây là một hàm tôi đã tạo để gán một số thuộc tính của đối tượng Style được gọi là 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 { } 
}

Lưu ý rằng tôi phải đặt xlBorders[Excel.XlBordersIndex.xlEdgeBottom]thành một biến để dọn sạch nó (Không phải vì hai dấu chấm, đề cập đến một phép liệt kê không cần phải phát hành, nhưng vì đối tượng tôi đang đề cập thực sự là một đối tượng Border điều đó không cần phải được phát hành).

Loại điều này không thực sự cần thiết trong các ứng dụng tiêu chuẩn, chúng thực hiện rất tốt việc tự dọn dẹp, nhưng trong các ứng dụng ASP.NET, nếu bạn bỏ lỡ một trong số chúng, bất kể bạn có thường xuyên gọi trình thu gom rác, Excel sẽ vẫn đang chạy trên máy chủ của bạn

Nó đòi hỏi rất nhiều sự chú ý đến chi tiết và nhiều lần thực hiện kiểm tra trong khi giám sát Trình quản lý tác vụ khi viết mã này, nhưng làm như vậy sẽ giúp bạn tránh được rắc rối khi tìm kiếm trong các trang mã để tìm ra một ví dụ mà bạn đã bỏ lỡ. Điều này đặc biệt quan trọng khi làm việc trong các vòng lặp, trong đó bạn cần giải phóng MACHI TỨC THÌ của một đối tượng, mặc dù nó sử dụng cùng một tên biến mỗi lần nó lặp.


Inside Release, kiểm tra Null (câu trả lời của Joe). Điều đó sẽ tránh các ngoại lệ null không cần thiết. Tôi đã thử nghiệm rất nhiều phương pháp. Đây là cách duy nhất để phát hành Excel hiệu quả.
Gerhard Powell

8

Sau khi thử

  1. Phát hành các đối tượng COM theo thứ tự ngược lại
  2. Thêm GC.Collect()GC.WaitForPendingFinalizers()hai lần vào cuối
  3. Không quá hai chấm
  4. Đóng sổ làm việc và bỏ ứng dụng
  5. Chạy trong chế độ phát hành

giải pháp cuối cùng phù hợp với tôi là di chuyển một bộ

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

mà chúng ta đã thêm vào cuối hàm vào một trình bao bọc, như sau:

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

Tôi thấy rằng tôi không cần phải vô hiệu hóa ComObjects, chỉ Quit () - Đóng () và FinalReleaseComObject. Tôi không thể tin rằng đây là tất cả những gì còn thiếu của tôi để làm cho nó hoạt động. Tuyệt quá!
Carol

7

Tôi đã làm theo chính xác điều này ... Nhưng tôi vẫn gặp vấn đề 1 trên 1000 lần. Ai biết tại sao. Đã đến lúc mang búa ra ...

Ngay sau khi lớp Ứng dụng Excel được khởi tạo, tôi sẽ nắm được quy trình Excel vừa được tạo.

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

Sau đó, khi tôi đã thực hiện tất cả các thao tác dọn dẹp COM ở trên, tôi chắc chắn rằng quy trình đó không chạy. Nếu nó vẫn chạy, giết nó!

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

7

¨ ° º¤ø „Bắn Excel Proc và nhai kẹo cao su bong bóng ¸„ ø¤º °

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

Bạn cần lưu ý rằng Excel cũng rất nhạy cảm với văn hóa bạn đang chạy.

Bạn có thể thấy rằng bạn cần đặt văn hóa thành EN-US trước khi gọi các hàm Excel. Điều này không áp dụng cho tất cả các chức năng - nhưng một số trong số họ.

    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;

Điều này áp dụng ngay cả khi bạn đang sử dụng VSTO.

Để biết chi tiết: http://support.microsoft.com/default.aspx?scid=kb;en-us;Q320369


6

"Không bao giờ sử dụng hai dấu chấm với các đối tượng COM" là một quy tắc tuyệt vời để tránh rò rỉ các tham chiếu COM, nhưng Excel PIA có thể dẫn đến rò rỉ theo nhiều cách hơn là rõ ràng ngay từ cái nhìn đầu tiên.

Một trong những cách này là đăng ký vào bất kỳ sự kiện nào được hiển thị bởi bất kỳ đối tượng COM nào của mô hình đối tượng Excel.

Ví dụ: đăng ký sự kiện WorkbookOpen của lớp Ứng dụng.

Một số lý thuyết về các sự kiện COM

Các lớp COM hiển thị một nhóm các sự kiện thông qua các giao diện gọi lại. Để đăng ký các sự kiện, mã máy khách có thể chỉ cần đăng ký một đối tượng thực hiện giao diện gọi lại và lớp COM sẽ gọi các phương thức của nó để đáp ứng với các sự kiện cụ thể. Vì giao diện gọi lại là giao diện COM, nên nhiệm vụ của đối tượng triển khai là giảm số tham chiếu của bất kỳ đối tượng COM nào mà nó nhận được (dưới dạng tham số) cho bất kỳ trình xử lý sự kiện nào.

Cách Excel PIA trưng bày các sự kiện COM

Excel PIA trưng bày các sự kiện COM của lớp Ứng dụng Excel như các sự kiện .NET thông thường. Bất cứ khi nào mã khách hàng đăng ký một sự kiện .NET (nhấn mạnh vào 'a'), PIA sẽ tạo một thể hiện của một lớp thực hiện giao diện gọi lại và đăng ký nó với Excel.

Do đó, một số đối tượng gọi lại được đăng ký với Excel để đáp ứng các yêu cầu đăng ký khác nhau từ mã .NET. Một đối tượng gọi lại cho mỗi thuê bao sự kiện.

Giao diện gọi lại để xử lý sự kiện có nghĩa là, PIA phải đăng ký tất cả các sự kiện giao diện cho mọi yêu cầu đăng ký sự kiện .NET. Nó không thể chọn và chọn. Khi nhận được một cuộc gọi lại sự kiện, đối tượng gọi lại kiểm tra xem trình xử lý sự kiện .NET có liên quan có quan tâm đến sự kiện hiện tại hay không và sau đó gọi trình xử lý hoặc âm thầm bỏ qua cuộc gọi lại.

Ảnh hưởng đến số tham chiếu COM

Tất cả các đối tượng gọi lại này không làm giảm số tham chiếu của bất kỳ đối tượng COM nào mà chúng nhận được (dưới dạng tham số) cho bất kỳ phương thức gọi lại nào (ngay cả đối với các đối tượng âm thầm bị bỏ qua). Họ chỉ dựa vào CLR thu gom rác để giải phóng các đối tượng COM.

Do việc chạy GC không mang tính quyết định, điều này có thể dẫn đến việc giữ quá trình Excel trong thời gian dài hơn mong muốn và tạo ra ấn tượng về 'rò rỉ bộ nhớ'.

Giải pháp

Giải pháp duy nhất cho đến thời điểm hiện tại là tránh nhà cung cấp sự kiện của PIA cho lớp COM và viết nhà cung cấp sự kiện của riêng bạn để phát hành các đối tượng COM một cách xác định.

Đối với lớp Ứng dụng, điều này có thể được thực hiện bằng cách triển khai giao diện AppEvents và sau đó đăng ký triển khai với Excel bằng cách sử dụng giao diện IConnectionPointContainer . Lớp Ứng dụng (và đối với vấn đề đó, tất cả các đối tượng COM phơi bày các sự kiện bằng cơ chế gọi lại) thực hiện giao diện IConnectionPointContainer.


4

Như những người khác đã chỉ ra, bạn cần tạo một tham chiếu rõ ràng cho mọi đối tượng Excel bạn sử dụng và gọi Marshal.ReleaseComObject trên tham chiếu đó, như được mô tả trong bài viết KB này . Bạn cũng cần sử dụng thử / cuối cùng để đảm bảo ReleaseComObject luôn được gọi, ngay cả khi ném ngoại lệ. Tức là thay vì:

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

bạn cần làm một cái gì đó như:

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

Bạn cũng cần gọi Application.Quit trước khi phát hành đối tượng Ứng dụng nếu bạn muốn đóng Excel.

Như bạn có thể thấy, điều này nhanh chóng trở nên cực kỳ khó sử dụng ngay khi bạn cố gắng làm bất cứ điều gì thậm chí phức tạp vừa phải. Tôi đã phát triển thành công các ứng dụng .NET với lớp trình bao bọc đơn giản bao gồm một vài thao tác đơn giản của mô hình đối tượng Excel (mở sổ làm việc, ghi vào Phạm vi, lưu / đóng sổ làm việc, v.v.). Lớp trình bao bọc thực hiện IDis Dùng một cách cẩn thận, thực hiện Marshal.ReleaseComObject trên mọi đối tượng mà nó sử dụng và không hiển thị rõ ràng bất kỳ đối tượng Excel nào cho phần còn lại của ứng dụng.

Nhưng phương pháp này không mở rộng tốt cho các yêu cầu phức tạp hơn.

Đây là một thiếu sót lớn của .NET COM Interop. Đối với các kịch bản phức tạp hơn, tôi sẽ xem xét nghiêm túc việc viết một ActiveX ActiveX bằng VB6 hoặc ngôn ngữ không được quản lý khác mà bạn có thể ủy quyền tất cả các tương tác với các đối tượng COM ngoài luồng như Office. Sau đó, bạn có thể tham chiếu ActiveX ActiveX này từ ứng dụng .NET của mình và mọi thứ sẽ dễ dàng hơn nhiều vì bạn chỉ cần phát hành một tham chiếu này.


3

Khi tất cả những thứ ở trên không hoạt động, hãy thử cho Excel một chút thời gian để đóng trang tính của nó:

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

...
FinalReleaseComObject(app);

2
Tôi ghét sự chờ đợi tùy tiện. Nhưng +1 vì bạn đúng về việc cần phải đợi sổ làm việc đóng lại. Một cách khác là thăm dò bộ sưu tập sổ làm việc trong một vòng lặp và sử dụng sự chờ đợi tùy ý làm thời gian chờ của vòng lặp.
dFlat

3

Đảm bảo rằng bạn phát hành tất cả các đối tượng liên quan đến Excel!

Tôi đã dành một vài giờ bằng cách thử một vài cách. Tất cả đều là những ý tưởng tuyệt vời nhưng cuối cùng tôi đã tìm thấy sai lầm của mình: Nếu bạn không giải phóng tất cả các đối tượng, không có cách nào ở trên có thể giúp bạn thích trong trường hợp của tôi. Hãy chắc chắn rằng bạn phát hành tất cả các đối tượng bao gồm phạm vi một!

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

Các tùy chọn là cùng nhau ở đây .


Điểm rất tốt! Tôi nghĩ bởi vì tôi đang sử dụng Office 2007 nên tôi đã thực hiện một số thao tác dọn dẹp. Tôi đã sử dụng lời khuyên thêm nhưng không lưu trữ các biến như bạn đã đề xuất ở đây và EXCEL.EXE không thoát nhưng tôi có thể may mắn và nếu tôi gặp bất kỳ vấn đề nào nữa, tôi chắc chắn sẽ xem xét phần này của mã =)
Coops

3

Một bài viết tuyệt vời về việc phát hành các đối tượng COM là 2.5 Phát hành Đối tượng COM (MSDN).

Phương pháp mà tôi muốn ủng hộ là vô hiệu hóa các tham chiếu Excel.Interop của bạn nếu chúng là các biến không cục bộ, sau đó gọi GC.Collect()GC.WaitForPendingFinalizers()hai lần. Các biến Interop có phạm vi cục bộ sẽ được xử lý tự động.

Điều này loại bỏ sự cần thiết phải giữ một tham chiếu có tên cho mọi đối tượng COM.

Dưới đây là một ví dụ được lấy từ bài báo:

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

Những từ này là thẳng từ bài viết:

Trong hầu hết mọi tình huống, vô hiệu hóa tham chiếu RCW và buộc bộ sưu tập rác sẽ dọn sạch đúng cách. Nếu bạn cũng gọi cho GC.WaitForPendingFinalulators, bộ sưu tập rác sẽ mang tính quyết định như bạn có thể thực hiện. Đó là, bạn sẽ khá chắc chắn chính xác khi nào đối tượng đã được dọn sạch trên mạng khi trở về từ cuộc gọi thứ hai đến WaitForPendingFinalulators. Để thay thế, bạn có thể sử dụng Marshal.ReleaseComObject. Tuy nhiên, lưu ý rằng bạn rất khó có thể sử dụng phương pháp này.


2

Quy tắc hai dấu chấm không làm việc cho tôi. Trong trường hợp của tôi, tôi đã tạo một phương thức để làm sạch tài nguyên của mình như sau:

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

2

Giải pháp của tôi

[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

Bạn nên rất cẩn thận khi sử dụng các ứng dụng xen kẽ Word / Excel. Sau khi thử tất cả các giải pháp, chúng tôi vẫn còn rất nhiều quá trình "WinWord" bị bỏ ngỏ trên máy chủ (với hơn 2000 người dùng).

Sau khi giải quyết vấn đề trong nhiều giờ, tôi nhận ra rằng nếu tôi mở nhiều hơn một vài tài liệu sử dụng Word.ApplicationClass.Document.Open()trên các luồng khác nhau, quy trình công nhân IIS (w3wp.exe) sẽ bị sập khiến tất cả các quy trình WinWord mở!

Vì vậy, tôi đoán không có giải pháp tuyệt đối cho vấn đề này, mà chuyển sang các phương pháp khác như phát triển Office Open XML .


1

Câu trả lời được chấp nhận không làm việc cho tôi. Các mã sau đây trong hàm hủy đã thực hiện công việc.

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

Tôi hiện đang làm việc về tự động hóa Office và đã tình cờ tìm ra giải pháp cho việc này hiệu quả mọi lúc đối với tôi. Nó là đơn giản và không liên quan đến việc giết bất kỳ quá trình.

Dường như chỉ bằng cách lặp qua các quy trình hoạt động hiện tại và bằng bất kỳ cách nào 'truy cập' một quy trình Excel đang mở, mọi trường hợp treo Excel đi lạc sẽ bị xóa. Đoạn mã dưới đây chỉ đơn giản kiểm tra các quy trình có tên là 'Excel', sau đó ghi thuộc tính MainWindowTitle của quy trình vào một chuỗi. 'Tương tác' với quy trình này dường như làm cho Windows bắt kịp và hủy bỏ phiên bản đóng băng của Excel.

Tôi chạy phương thức dưới đây ngay trước khi bổ trợ mà tôi đang phát triển thoát, vì nó kích hoạt sự kiện dỡ tải. Nó loại bỏ mọi trường hợp treo Excel mỗi lần. Thành thật mà nói, tôi không hoàn toàn chắc chắn tại sao nó hoạt động, nhưng nó hoạt động tốt với tôi và có thể được đặt ở cuối bất kỳ ứng dụng Excel nào mà không phải lo lắng về các dấu chấm kép, Marshal.ReleaseComObject, cũng như không giết chết các quy trình. Tôi sẽ rất quan tâm đến bất kỳ đề xuất nào về lý do tại sao điều này có hiệu quả.

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

Tôi nghĩ rằng một số trong đó chỉ là cách mà khung xử lý các ứng dụng Office, nhưng tôi có thể sai. Vào một số ngày, một số ứng dụng dọn sạch các quy trình ngay lập tức và những ngày khác dường như phải đợi cho đến khi ứng dụng đóng lại. Nói chung, tôi không chú ý đến các chi tiết và chỉ đảm bảo rằng không có bất kỳ quy trình bổ sung nào trôi nổi vào cuối ngày.

Ngoài ra, và có lẽ tôi đang đơn giản hóa mọi thứ, nhưng tôi nghĩ bạn chỉ có thể ...

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

Như tôi đã nói trước đây, tôi không có xu hướng chú ý đến các chi tiết khi quá trình Excel xuất hiện hoặc biến mất, nhưng điều đó thường làm việc với tôi. Tôi cũng không muốn giữ các quy trình Excel xung quanh cho bất cứ điều gì ngoài khoảng thời gian tối thiểu, nhưng có lẽ tôi chỉ bị hoang tưởng về điều đó.


1

Như một số người có thể đã viết, việc bạn đóng Excel (đối tượng) không chỉ quan trọng ; Nó cũng quan trọng như thế nào bạn mở nó và cũng theo loại dự án.

Trong một ứng dụng WPF, về cơ bản cùng một mã đang hoạt động mà không có hoặc có rất ít vấn đề.

Tôi có một dự án trong đó cùng một tệp Excel đang được xử lý nhiều lần cho các giá trị tham số khác nhau - ví dụ: phân tích cú pháp dựa trên các giá trị trong danh sách chung.

Tôi đặt tất cả các hàm liên quan đến Excel vào lớp cơ sở và trình phân tích cú pháp vào một lớp con (các trình phân tích cú pháp khác nhau sử dụng các hàm Excel phổ biến). Tôi không muốn Excel được mở và đóng lại cho từng mục trong danh sách chung, vì vậy tôi chỉ mở nó một lần trong lớp cơ sở và đóng nó trong lớp con. Tôi gặp vấn đề khi chuyển mã vào ứng dụng máy tính để bàn. Tôi đã thử nhiều giải pháp được đề cập ở trên.GC.Collect()đã được thực hiện trước đó, hai lần như đề xuất.

Sau đó, tôi đã quyết định rằng tôi sẽ chuyển mã để mở Excel sang một lớp con. Thay vì chỉ mở một lần, bây giờ tôi tạo một đối tượng mới (lớp cơ sở) và mở Excel cho mọi mục và đóng nó ở cuối. Có một số hình phạt về hiệu năng, nhưng dựa trên một số thử nghiệm, các quy trình Excel đang đóng mà không gặp sự cố (trong chế độ gỡ lỗi), do đó, các tệp tạm thời cũng bị xóa. Tôi sẽ tiếp tục với thử nghiệm và viết thêm một số nếu tôi sẽ nhận được một số cập nhật.

Điểm mấu chốt là: Bạn cũng phải kiểm tra mã khởi tạo, đặc biệt nếu bạn có nhiều lớp, v.v.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.