[C#] كيف يمكنني تنظيف كائنات Excel interop بشكل صحيح؟


Answers

يمكنك بالفعل تحرير كائن تطبيق Excel الخاص بك بشكل نظيف ، ولكن عليك أن تأخذ الرعاية.

النصيحة للحفاظ على مرجع مسمى لكل كائن COM على الإطلاق يمكنك الوصول إليه ومن ثم إطلاقه بشكل صريح عبر Marshal.FinalReleaseComObject() صحيح من الناحية النظرية ، ولكن للأسف ، من الصعب جدًا إدارته عمليًا. إذا كان أي واحد ينزلق في أي مكان ويستخدم "نقطتين" ، أو يكرر الخلايا عبر for each حلقة ، أو أي نوع آخر من الأوامر المشابهة ، عندئذ سيكون لديك كائنات COM غير موثوقة ومخاطر تعليق. في هذه الحالة ، لن تكون هناك طريقة للعثور على السبب في التعليمة البرمجية؛ يجب عليك مراجعة جميع التعليمات البرمجية الخاصة بك عن طريق العين ونأمل في العثور على السبب ، وهي مهمة قد تكون شبه مستحيلة لمشروع كبير.

الخبر السار هو أنك لا تحتاج في الواقع إلى الاحتفاظ بمرجع متغير مسمى لكل كائن COM تستخدمه. بدلاً من ذلك ، استدعاء GC.Collect() ثم GC.WaitForPendingFinalizers() لتحرير كافة الكائنات (عادةً ما تكون ثانوية) التي لا تملك مرجع ، ومن ثم تحرير الكائنات التي لديك مرجع متغير مسمى بشكل صريح.

يجب أيضًا تحرير المراجع الخاصة بك في ترتيب عكسي الأهمية: كائنات النطاق أولاً ، ثم أوراق العمل ، والمصنفات ، وأخيرًا كائن تطبيق Excel الخاص بك.

على سبيل المثال ، بافتراض أن لديك متغير كائن نطاق يدعى xlRng ، وهو متغير ورقة عمل مسمى xlSheet ، xlRng مصنف مسمى xlBook Excel Application المسمى xlApp ، فإن كود التنظيف الخاص بك قد يبدو كالتالي:

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

في معظم أمثلة التعليمات البرمجية التي تراها لتنظيف كائنات COM من .NET ، يتم إجراء مكالمات GC.Collect() و GC.WaitForPendingFinalizers() مرتين كما في:

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

هذا لا يجب أن يكون مطلوبًا ، إلا إذا كنت تستخدم أدوات Visual Studio لـ Office (VSTO) ، والتي تستخدم finalizers التي تتسبب في ترقية رسم بياني كامل للكائنات في قائمة الانتظار النهائية. لن يتم تحرير مثل هذه الكائنات حتى جمع القمامة التالي . ومع ذلك ، إذا كنت لا تستخدم VSTO ، فيجب أن تتمكن من الاتصال بـ GC.Collect() و GC.WaitForPendingFinalizers() مرة واحدة فقط.

وأنا أعلم أن استدعاء GC.Collect() بشكل صريح هو بلا - (وبالتأكيد القيام به مرتين يبدو مؤلما للغاية) ، ولكن لا توجد طريقة حوله ، أن نكون صادقين. من خلال العمليات العادية ، ستقوم بإنشاء كائنات مخفية لا تحمل أي مرجع لك ، وبالتالي ، لا يمكن تحريرها من خلال أي وسيلة أخرى غير استدعاء GC.Collect() .

هذا موضوع معقد ، ولكن هذا هو كل ما في الأمر. بمجرد إنشاء هذا القالب لإجراء التنظيف ، يمكنك إجراء الكود بشكل طبيعي ، دون الحاجة إلى أغلفة ، إلخ. :-)

لدي برنامج تعليمي حول هذا هنا:

أتمتة برامج Office مع VB.Net / COM Interop

مكتوب ل VB.NET ، ولكن لا يمكن تأجيل ذلك ، فإن المبادئ هي نفسها تماما كما هو الحال عند استخدام C #.

Question

أنا باستخدام Interop Excel في C # ( ApplicationClass ) وقد وضعت التعليمات البرمجية التالية في بلدي أخيرا فقرة:

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

بالرغم من هذا النوع من الأعمال ، ما زالت عملية Excel.exe في الخلفية حتى بعد إغلاق Excel. يتم تحريرها بمجرد إغلاق التطبيق الخاص بي يدويا.

ماذا أفعل الخطأ ، أو هل هناك بديل لضمان التخلص من الأشياء المتداخلة بشكل صحيح؟




You should be very careful using Word/Excel interop applications. After trying all the solutions we still had a lot of "WinWord" process left open on server (with more than 2000 users).

After working on the problem for hours, I realized that if I open more than a couple of documents using Word.ApplicationClass.Document.Open() on different threads simultaneously, IIS worker process (w3wp.exe) would crash leaving all WinWord processes open!

So I guess there is no absolute solution to this problem, but switching to other methods such as Office Open XML development.




يجب تحرير أي شيء موجود في مساحة اسم Excel. فترة

لا يمكنك أن تفعل:

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

عليك أن تفعل

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

تليها الافراج عن الأشياء.




استعمال:

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

Declare it, add code in the finally block:

finally
{
    GC.Collect();
    GC.WaitForPendingFinalizers();
    if (excelApp != null)
    {
        excelApp.Quit();
        int hWnd = excelApp.Application.Hwnd;
        uint processID;
        GetWindowThreadProcessId((IntPtr)hWnd, out processID);
        Process[] procs = Process.GetProcessesByName("EXCEL");
        foreach (Process p in procs)
        {
            if (p.Id == processID)
                p.Kill();
        }
        Marshal.FinalReleaseComObject(excelApp);
    }
}



¨°º¤ø„¸ Shoot Excel proc and chew bubble gum ¸„ø¤º°¨

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



Make sure that you release all objects related to Excel!

I spent a few hours by trying several ways. All are great ideas but I finally found my mistake: If you don't release all objects, none of the ways above can help you like in my case. Make sure you release all objects including range one!

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

The options are together here .




استكمال : إضافة رمز C # ، والارتباط بوظائف ويندوز

قضيت بعض الوقت في محاولة لمعرفة هذه المشكلة ، وفي ذلك الوقت كان XtremeVBTalk الأكثر نشاطا واستجابة. فيما يلي ارتباط إلى مشاركتي الأصلية ، إغلاق عملية Excel Interop نظيفة ، حتى إذا تعطل التطبيق الخاص بك . في ما يلي ملخص للمقالة ، ونسخ الرمز إلى هذه المشاركة.

  • إغلاق عملية Interop مع Application.Quit() و Process.Kill() يعمل للجزء الأكبر ولكن يفشل إذا تعطل التطبيقات catastrophically. أي إذا تعطل التطبيق ، ستظل عملية Excel قيد التشغيل.
  • الحل هو السماح لنظام التشغيل بمعالجة تنظيف العمليات الخاصة بك من خلال كائنات عمل Windows باستخدام مكالمات Win32. عندما يموت التطبيق الرئيسي الخاص بك ، سيتم إنهاء العمليات المرتبطة (أي إكسل) كذلك.

لقد وجدت أن هذا هو حل نظيف لأن نظام التشغيل يقوم بعمل حقيقي للتنظيف. كل ما عليك القيام به هو تسجيل عملية Excel.

رمز عمل ويندوز

يلتف على مكالمات Win32 API لتسجيل العمليات 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);
    }

}

ملاحظة حول رمز منشئ

  • في منشئ ، info.LimitFlags = 0x2000; يسمى. 0x2000 هي قيمة التعداد JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE ، ويتم تعريف هذه القيمة بواسطة MSDN النحو التالي:

يتسبب في إنهاء جميع العمليات المرتبطة بالمهمة عند إغلاق المؤشر الأخير للمهمة.

استدعاء Win32 API إضافي للحصول على معرف العملية (PID)

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

باستخدام الكود

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



I think that some of that is just the way that the framework handles Office applications, but I could be wrong. On some days, some applications clean up the processes immediately, and other days it seems to wait until the application closes. In general, I quit paying attention to the details and just make sure that there aren't any extra processes floating around at the end of the day.

Also, and maybe I'm over simplifying things, but I think you can just...

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

Like I said earlier, I don't tend to pay attention to the details of when the Excel process appears or disappears, but that usually works for me. I also don't like to keep Excel processes around for anything other than the minimal amount of time, but I'm probably just being paranoid on that.




لا أستطيع أن أصدق أن هذه المشكلة قد تطارد العالم لمدة 5 سنوات .... إذا قمت بإنشاء تطبيق ، فأنت بحاجة إلى إيقافه أولاً قبل إزالة الارتباط.

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

عند الاغلاق

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

عندما تقوم بتطبيق Excel جديد ، فإنه يفتح برنامج excel في الخلفية. أنت بحاجة إلى قيادة برنامج اكسل للإقلاع قبل أن تطلق الرابط لأن برنامج إكسل هذا ليس جزءًا من التحكم المباشر. لذلك ، ستبقى مفتوحة إذا تم إصدار الرابط!

جيد البرمجة الجميع ~~




I followed this exactly... But I still ran into issues 1 out of 1000 times. Who knows why. Time to bring out the hammer...

Right after the Excel Application class is instantiated I get a hold of the Excel process that was just created.

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

Then once I've done all the above COM clean-up, I make sure that process isn't running. If it is still running, kill it!

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



بلدي الحل

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



أولاً - لن تحتاج مطلقًا إلى الاتصال بـ Marshal.ReleaseComObject(...) أو Marshal.FinalReleaseComObject(...) عند إجراء Excel interop. وهو نمط مكافحة محيرة ، ولكن أي معلومات حول هذا الأمر ، بما في ذلك من Microsoft ، تشير إلى ضرورة تحرير مراجع COM يدوياً من .NET غير صحيحة. الحقيقة أن .NET runtime و garbage collector بشكل صحيح تعقب وتعقب مراجع COM. بالنسبة إلى شفرتك ، هذا يعني أنه يمكنك إزالة الحلقة الكاملة أثناء (...) في الجزء العلوي.

ثانيًا ، إذا كنت تريد التأكد من أنه يتم تنظيف COM المراجع إلى كائن COM خارج العملية عند انتهاء العملية (بحيث يتم إغلاق عملية Excel) ، تحتاج إلى التأكد من تشغيل أداة تجميع مجمعي البيانات المهملة. يمكنك القيام بذلك بشكل صحيح مع المكالمات إلى GC.Collect() و GC.WaitForPendingFinalizers() . إن الاتصال بهذا مرتين آمن ، ويضمن تنظيف الدورات بالتأكيد أيضًا (على الرغم من أنني لست متأكدًا من أنها ضرورية ، وسوف نقدر مثالاً يوضح ذلك).

ثالثًا ، عند التشغيل تحت مصحح الأخطاء ، سيتم الاحتفاظ بالمراجع المحلية بشكل مصطنع حتى نهاية الطريقة (بحيث يعمل الفحص المحلي المتغير). لذلك لا تكون مكالمات GC.Collect() فعالة لتنظيف الكائن مثل rng.Cells من نفس الطريقة. يجب عليك تقسيم التعليمات البرمجية القيام COM interop من تنظيف GC إلى أساليب منفصلة. (كان هذا اكتشافًا أساسيًا بالنسبة لي ، من جزء واحد من الإجابة المنشورة هنا بواسطةnightcoder.)

النمط العام سيكون بالتالي:

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

هناك الكثير من المعلومات الخاطئة والارتباك حول هذه المشكلة ، بما في ذلك العديد من المنشورات على MSDN وعلى (وخاصة هذا السؤال!).

ما أقنعني أخيراً بأن يكون لدي نظرة عن قرب ، وأن أحصل على النصيحة الصحيحة كانت مشاركة المدونة Marshal.ReleaseComObject نظرنا إلى الخطورة مع إيجاد المشكلة مع المراجع التي بقيت على قيد الحياة تحت المصحح الذي كان يربك الاختبار السابق.




After trying

  1. Release COM objects in reverse order
  2. Add GC.Collect() and GC.WaitForPendingFinalizers() twice at the end
  3. No more than two dots
  4. Close workbook and quit application
  5. Run in release mode

the final solution that works for me is to move one set of

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

that we added to the end of the function to a wrapper, as follows:

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



As others have pointed out, you need to create an explicit reference for every Excel object you use, and call Marshal.ReleaseComObject on that reference, as described in this KB article . You also need to use try/finally to ensure ReleaseComObject is always called, even when an exception is thrown. Ie instead of:

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

you need to do something like:

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

You also need to call Application.Quit before releasing the Application object if you want Excel to close.

As you can see, this quickly becomes extremely unwieldy as soon as you try to do anything even moderately complex. I have successfully developed .NET applications with a simple wrapper class that wraps a few simple manipulations of the Excel object model (open a workbook, write to a Range, save/close the workbook etc). The wrapper class implements IDisposable, carefully implements Marshal.ReleaseComObject on every object it uses, and does not pubicly expose any Excel objects to the rest of the app.

But this approach doesn't scale well for more complex requirements.

This is a big deficiency of .NET COM Interop. For more complex scenarios, I would seriously consider writing an ActiveX DLL in VB6 or other unmanaged language to which you can delegate all interaction with out-proc COM objects such as Office. You can then reference this ActiveX DLL from your .NET application, and things will be much easier as you will only need to release this one reference.




To add to reasons why Excel does not close, even when you create direct refrences to each object upon read, creation, is the 'For' loop.

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