суббота, 11 октября 2014 г.

COM Interop на примере работы с Excel

В предыдущем посте я писал о создании csv-файла и экспорта в него данных  из ASP.NET и при этом, перечисляя разные варианты, не упомянул один совсем уже бредовый для web-приложения, но частенько очень подходящий для приложений десктопных. Это работа с установленным в системе COM-объектом c помощью технологии под названием COM Interop.

COM Interop используется в .Net для предоставления возможности взаимодействия управляемого .Net кода с COM-объектами. Тля того чтобы использовать какой-либо COM-объект из управляемого кода, необходимо создать сборку, содержащую информацию о типах содержащихся в COM-библиотеке, в совместимом с CRL формате.

В процессе работы приложения CLR создает для каждого COM-объекта внутренний объект, называемый Runtime Callable Wrapper (Вызываемая оболочка времени выполнения) или RCW, которая используется для создания  COM-объекта и маршалинга данных между управляемой и неуправляемой средой. Также, RCW используется для мониторинга количества активных ссылок на COM-объект и его уничтожение, когда количество активных ссылок станет равным нулю. Выглядит это примерно так:

Обычно, если вы ходите создать CLR библиотеку для какого-либо компонента самостоятельно, то вам нужно использовать утилиту Tlbimp.exe, но для использования компонентов Office этого делать не нужно. Все необходимые библиотеки уже устанавливаются вместе с продуктом. Нам достаточно только добавить нужную сборку в проект.

Раз

И добавить соответствующую строку using в ваш код

using Excel = Microsoft.Office.Interop.Excel;

В качестве источника данных для примера будем использовать список массивов строк ( List<string[]>). В коде я написал комментарии, которые, думаю, будут вполне понятно объяснять что происходит.

 
            //Missing представляет собой отcутствующий объект, для передачи в качестве не используемого параметра.
            //В принципе, начиная с C# 4.0 можно использовать стандартный синтаксис для
            //необязательных параметров. А можно по старинке, вместо отсутствующего
            // параметра передать Missing.Value
            System.Reflection.Missing missingValue = System.Reflection.Missing.Value;

            //создаем и инициализируем объекты Excel
            Excel.Application App;
            Excel.Workbook xlsWB;
            Excel.Worksheet xlsSheet;

            App = new Microsoft.Office.Interop.Excel.Application();
            //добавляем в файл Excel книгу. Параметр в данной функции - используемый для создания книги шаблон.
            //если нас устраивает вид по умолчанию, то можно спокойно передавать пустой параметр.
            xlsWB = App.Workbooks.Add(missingValue);
            //и использует из нее
            xlsSheet = (Excel.Worksheet)xlsWB.Worksheets.get_Item(1);

            List<string[]> rows = new List<string[]>();
            rows.Add(new string[] {"1","2","3","4"});
            rows.Add(new string[] {"5","6","7","8"});
            rows.Add(new string[] {"9","10","11","12"});
            for (int i = 0; i < rows.Count; i++)
            {
                for (int j = 0; j < rows[i].Length; j++)
                {
                    xlsSheet.Cells[i + 1, j + 1] = rows[i][j];
                }
            }

            //у SaveAs масса  параметров, некоторые из которых могут оказаться для вас полезными. 
            //Сейчас мы используем только тип формата Excel12 (Excel 97) и права доступа.
            //Полное описание что и как смотрите на MSDN: 
            // http://msdn.microsoft.com/ru-ru/library/microsoft.office.tools.excel.workbook.saveas.aspx
            xlsWB.SaveAs(@"C:\excel_text.xls", 
                         Excel.XlFileFormat.xlExcel12, 
                         missingValue, 
                         missingValue, 
                         missingValue, 
                         missingValue, 
                         Excel.XlSaveAsAccessMode.xlNoChange, 
                         missingValue, 
                         missingValue, 
                         missingValue, 
                         missingValue, 
                         missingValue);         
            //закрываем книгу                                                                        
            xlsWB.Close(true, missingValue, missingValue);
            //закрываем приложение
            App.Quit();  
            //уменьшаем счетчики ссылок на COM объекты, что, по идее должно их освободить.
            //почему это не произойдет - читайте ниже ;)
            System.Runtime.InteropServices.Marshal.ReleaseComObject(xlsSheet);
            System.Runtime.InteropServices.Marshal.ReleaseComObject(xlsWB);
            System.Runtime.InteropServices.Marshal.ReleaseComObject(App); 

Ну и пара нюансов с которыми вы можете столкнуться. В случае, если в COM-компоненте происходит исключение, которое вы не обработаете, то, как и положено, весть домен приложения будет аварийно закрыт. Вместе с RCW. В результате чего, COM-объект так и останется висеть в памяти. То есть, вполне реально получить вот такую картинку:

И нюанс второй. Код сверху не совсем правильный. Дело в том, что работая с COM Interop нужно быть предельно внимательным и осторожным, потому, что случайно можно создать COM-объект, совершенно об этом не подозревая. В данном случае, создается даже не один "случайный" объект, а несколько.
Во-первых, в строке  xlsWB = App.Workbooks.Add(missingValue); создается объект App.Workbooks, который не присваевается переменной и, соответственно, не освобожается дл тех пор, пока не выгрузится домен приложения.
Во-вторых, такая же проблема в строке в строке: xlsSheet = (Excel.Worksheet)xlsWB.Worksheets.get_Item(1);

Для того, чтобы эту проблему решить необходимо добавить две новые переменные и немного изменить код инициализации:

 
            //создаем и инициализируем объекты Excel
            Excel.Application App;
            //переменная для Workbooks
            Excel.Workbooks xlsWBs;
            Excel.Workbook xlsWB;
            //переменная для Sheets
            Excel.Sheets xlsSheets;
            Excel.Worksheet xlsSheet;

            App = new Microsoft.Office.Interop.Excel.ApplicationClass();
            //добавляем в файл Excel книгу. Параметр в данной функции - используемый для создания книги шаблон.
            //если нас устраивает вид по умолчанию, то можно спокойно передавать пустой параметр.
            xlsWBs = App.Workbooks;
            xlsWB = xlsWBs.Add(missingValue);
            xlsSheets = xlsWB.Worksheets;
            xlsSheet = (Excel.Worksheet)xlsSheets.get_Item(1);

Ну и добавить освобождение этих переменных в конце с помощью System.Runtime.InteropServices.Marshal.ReleaseComObject.
Вообще же, работая с COM объектами старайтесь придерживаться простого правила:
Никогда не используйте больше одной точки с COM.

Комментариев нет:

Отправить комментарий