среда, 20 мая 2009 г.

Работа с Excel из C#.

Столкнулся с необходимостью поработать с файлами Excel из C#. Задачи простые - считать данные из книги Excel, создать новую книгу Excel, внести в нее данные. Предполагается, что Excel на компьютере имеется, но какой версии, не известно. Файлы по размерам маленькие, скорость работы не критична.

Существует несколько вариантов работы с Excel из C#: автоматизация Excel, подключение через OleDB/ODBC, дополнительные библиотеки (Aspose Excel), работа через XML, через Open XML и т.п.

Наиболее простой вариант - воспользоваться автоматизацией Excel. Да, скорость работы - не блеск. Зато удобно использовать, код пишется быстро, объемы кода не велики. Из .NET автоматизация подключается парой кликов мыши - добавил в References сборку Microsoft.Office.Interop.Excel - и работай с привычными com-объектами Application,Workbook, Worksheet и т.п.

Проблема одна - сборки "Microsoft.Office.Interop.Excel" для каждой версии Excel разные. У меня установлен office 2003 и, соответственно, interop-сборка версии 11. А что делать, если мне нужно разработать приложение, которое может работать с Excel 97? Или с любыми версиями Excel? Для того, чтобы считать или записать пару значений в ячейки листа Excel сложного api не требуется - любой Excel сгодится. А мне приходится привязываться к конкретной версии.

Можно воспользоваться поздним связыванием. Но если его использовать "в лоб", то ни о никаком удобстве работы речи уже не идет - код станет сложным, а вызовы методов - косвенными, нетипизированными. Ошибок будет - вагон.

Выход нашелся. На СodeProject обнаружил замечательную статью "SafeCOMWrapper - Managed Disposable Strongly Typed safe wrapper to late bound COM". В ней изложена элегантная методика использования позднего связывания, устраняющая все проблемы: связывание становится поздним (привязки к конкретной версии Excel нет), для объектов автоматизации автоматически реализуется шаблон IDisposable (отработанные объекты уничтожаются автоматически), все вызовы методов явные.

Идея реализации, вкратце, следующая. Для каждого COM-объекта автоматизации вы прописываете отдельный интерфейс. В интерфейс включаете все методы и свойства, которые вам необходимо использовать у этого COM-объекта. Обратите внимание - только те, которые необходимо использовать, а вовсе не все, реализуемые COM-объектом.

Каждый COM-объект автоматизации "заворачивается" в класс COMWrapper, унаследованный от RealProxy. RealProxy - это стандартный класс, позволяющий организовать перехват вызовов методов. При создании COM-объекта вы создаете экземпляр COMWrapper и указываете требуемый вам интерфейс. Среда динамически генерирует прокси-объект, реализующий этот интерфейс. С этим прокси-объектом вы в дальнейшем и работаете как с объектом автоматизации. Вызов любого метода прокси-объетка перехватывается и транслируется в вызов метода Invoke класса RealProxy, перекрытый в классе COMWrapper. Здесь его можно обработать как душе угодно. В реализации по умолчанию, вызовы свойств транслируются в вызовы соответствующих методов get_ и set_, создаваемых .NET, возвращаемые объекты автоматизации автоматически заворачиваются в COMWrapper и т.п.

В оригинально статье приведен пример использования данной методики для Microsoft.Outlook. Я, на базе приведенных исходных кодов, адаптировал методику для работы с Microsoft.Excel. Естественно, реализовав интерфейсы только для тех объектов автоматизации, которые потребовались мне для работы. Дополнить интерфейсы недостающими методами или добавить интерфейсы для других объектов автоматизации не составляет труда. Достаточно посмотреть через Object Browser метаданные сборки "Microsoft.Office.Interop.Excel" и скопировать оттуда необходимые объявления методов и интерфейсов (с минимальной адаптацией).

Пример кода для работы с Excel:
using (Application app = ExcelApplication.Create()) {
String s = app.Version;
app.Visible = true; // make excel visible
using (Workbook wb = app.Workbooks.Add(Type.Missing)) {
wb.Title = "new workbook";
using (Worksheets worksheets = wb.Sheets) {
using (Worksheet ws = worksheets[1]) {
//try to assign some values to some cells
using (Range cells = ws.Cells) {
for (int i = 1; i < 10; ++i) {
using (Range r = cells[i, i]) {
r.Value = i * i;
}
}
}
}
}
wb.Saved = true;
wb.Close();
}
app.Quit();
}
Оригинальные исходные коды лежат на codeproject вместе со статьей. Исходные коды, адаптированные для Microsoft Excel, можно взять здесь.Update. Практика использования выявила небольшую проблему - забыть вызвать Dispose у оберток объектов Excel очень легко. После чего Excel висит в памяти до тех пор, пока не закроешь приложение (или до следующей сборки мусора). Пример:
using (Workbook wb = app.Workbooks.Add(Type.Missing)) {
using (Worksheet ws = wb.Sheets[1]) 
/*Здесь потерян Dispose у объекта Worksheets, который создается при 
вызове wb.Sheets*/
{ .... }}
вместо
using (Workbook wb = app.Workbooks.Add(Type.Missing)) {
using (Worksheets worksheets = wb.Sheets) {
using (Worksheet ws = worksheets[1]) {
.... }}

Просто надо быть внимательнее.

34 комментария:

  1. Круто! Можешь ответить на вопрос. В статье на codeproject в исходниках автор включает файлы:
    ComEventProvider.cs
    ComEventsAttribute.cs
    ComEventSink.cs
    ComProgIdAttribute.cs

    Вопрос: это сгенерированные файлы (типа AssemblyInfo.cs) или написаны вручную. Если вручную, то основываясь на чем. Тем более, что в твоей версии, как я понял, они не используются

    ОтветитьУдалить
  2. Огромное спасибо! Все работает.

    ОтветитьУдалить
  3. Всем доброго врмени суток!

    Пытаюсь ипользовать этот подход в своей программе. Все работает, кром того что если считывать свойства объекта в цикле, допустим пробегать по ячейкам листа, то появляется исключение. Вот код:

    Workbook oBook;
    Sheets xlsheets = oBook.sheets;
    Worksheet curWS = xlsheets[2];

    for (int i = 0; i < Count; i++)
    {
    Range curRange = curWS.Cells[4 + i, counter];

    // при i > 80 вылетает исключение. i примерное, не точное число, иногда до 116 дотягивает, у меня в логике нет проблем)

    curRange.Value2 = (object)sendValue;
    curRange = null;
    }

    Исключение: COM object that has been separated from its underlying RCW can not be used

    Спасибо всем кто ответит.

    ОтветитьУдалить
  4. Чтобы точно убедиться, что ошибка не в логике, нужно сделать отдельное маленькое тестовое приложение. С одной функцией - считывание свойств объектов в цикле. Попробуй. Если ошибка воспроизведется - выкладывай текст функции, я постараюсь помочь с поиском ошибки.

    P.s. Имхо все же лучше писать так:
    using (Range curRange ... ) {
    ...
    curRange.Value2 = (object)sendValue;
    /*сurRange = null; */
    }

    Если просто присваивать curRange = null, то вызов Dipsose у curRange будет откладываться до сборки мусора на "неизвестно когда".

    ОтветитьУдалить
  5. Спасибо большое за совет. Действительно using помог. Хотя не очень понимаю, как это влиять может. Видимо тонокости неподвластные пониманию :)

    Но вот вопрос, если у меня есть класс, а в нем два метода которые вызываются в разное время, в одной создается например Sheet, а в другой через какое-то время закрывается или производятся манипуляции с яйками. То как в этом случе using использовать? Его же на два метода не растянешь.

    У меня просто есть wrap класс позволяющий манипулироватьс экзелем. Так чтобы пользователи класса не знали что там внутри вообще происхдит. Типа метод "Создать лист", или "создать лист и згрузить в него некую структур данных" и т.д. Я хочу сказать о том что за пределы класа объявление книг и листов не получится вынести.

    Такие дела. Спасибо большое еще раз.

    ОтветитьУдалить
  6. Код:

    using (A a = new A()) {
    ...
    }

    по большому счету, эквивалентен

    A a = new A(); //создание объекта Excel
    try {
    ...
    } finally {
    a.Dispose(); //уничтожение объекта Excel
    }

    Так что можно обойтись без using - в одном методе вызывать A a = new A(), а в другом a.Dispose().

    Все классы объектов Excel, используемые в описываемом подходе, реализуют интерфейс IDisposable. Для уничтожения экземпляров таких объектов нужно явно вызывать у них метод Dispose. Если этого не сделать, то Dispose будет вызван автоматически, при сборке мусора. Но когда это произойдет - не известно. А до той поры все объекты будут накапливаться в памяти неуничтоженными. И если их максимально возможное количество ограничено - полезут ошибки.

    P.s. на rsdn есть хорошая статья на тему IDisposable и using. После ее прочтения вся эта суета с Dispose станет понятнее :)

    ОтветитьУдалить
  7. А тогда все понятно!!! Спасибо, Вы мне очень помогли. Удачи.

    ОтветитьУдалить
  8. Тут еще вопрос сопутствующий, если я вызываю
    a.Dispose(); но мне по какой-то причине нужно чтобы объект превратился в null. Могу ли я записать так:

    object a = new a();
    ...

    a.Dispose();
    a.Null;

    Прочитал статью которую вы рекомендовали. Некторые вещи стали теперь понятнее. Спасибо за сслыку. Не могли бы вы посоветовать программу или способ поискать утечки памяти. Спасибо.

    ОтветитьУдалить
  9. Конечно, объект можно "занулить" стандартным способом:
    a.Dispose();
    a = null;
    Если ссылка на объект "a" у нас была единственной, то после a = null объект станет неиспользуемым и вся занимаемая им (управляемая) память будет освобождена при сборке мусора.

    Могу посоветовать посмотреть в сторону утилит
    Redgate ANTS Memory Profiler и ProfileSharp. Первая коммерческая, вторая бесплатная.

    ОтветитьУдалить
  10. Просто чисто логически не очень понятно. Я так понял что Dispose и должен был сразу дaвать сигнал на чистку памяти этого объекта, и когда мы пишем a=null, то не противоречит ли это друг другу. Т.е. как можно поставить флаг (=null) чтобы GC очистил занмаимую им память, если мы уже вызвали a.Dispose() для той же самой цели, но и еще более аккуратно, судя из того чтоя понял, при этом очистка будет выполнена как раз в момент вызова метода Dispose, а не когда вздумается, как это было бы с чистым a=null.

    ОтветитьУдалить
  11. Вызовы a.Dipose() и a = null служат для разных целей.

    Вызов Dispose нужен для очистки неуправляемых ресурсов. При этом сам объект "a" остается живым и здоровым, мы все так же имеем на него ссылку в коде и можем вызывать его методы. Но все неуправляемые ресурсы в объекте "a" после вызова Dipose уничтожены.

    Выражение a = null говорит о том, что ссылка на a нам больше не нужна.. а значит, объект надо пометить как неиспользуемый. Все управляемые ресурсы, занятые объектом, будут уничтожены при сборке мусора.

    Пример. Пусть у нас есть класс A, содержащий как управляемый, так неуправляемый объекты:

    class A {
    //управляемый объект
    public String m_S;
    //неуправляемый ресурс
    private UnmanagedObject m_U;
    public A() {
    //создаем объект в неуправляемой памяти
    allocate_unmanaged(m_U);
    }

    void Dipose() {
    //уничтожаем объект в неуправляемой памяти
    deallocate_unmanaged(m_U);
    }
    }

    Создаем экземпляр:
    A a = new A();

    Уничтожаем объект:
    a.Dispose();
    //m_U уничтожен

    a = null;
    //a и m_S помечены как более неиспользуемые; память, занимаемая ими, будет освобождена при сборке мусора.

    Только класс A знает, как создавать неуправляемый объект. И только он знает, как его уничтожать. C# самостоятельно сделать это не может, его надо научить.. Поэтому придумали IDisposable.

    Интерфейс IDisposable - это соглашение. Соглашение о том, что в классе A будет реализована функция Dispose, которая корректно уничтожает все неуправляемые ресурсы, выделенные в этом классе. В нашем примере функция Dipose должна содержать вызов deallocate_unmanaged, обратный вызову allocate_unmanaged.

    Однако реализация IDisposable несколько нетривиальна (см. статью на RSDN). Нетривиальность связана с вопросом: кто и когда должен вызывать Dispose?

    По идее, ее должен вызвать программист, когда уничтожает объект. Но он может забыть это сделать и утечки останутся. Поэтому в класс добавляют финализатор и вызывают Dispose в финализаторе. Финализаторы автоматически вызываются во время сборки мусора. Так что если программист забыл вызвать Dispose, то Dispose будет вызвана во время сборки мусора.

    Но здесь другая проблема. Объекты с финализаторами обрабатываются сборщиком мусора не так эффективно, как объекты без финализаторов. Добавляются накладные расходы.. В том случае, если программист культурный и Dipose вызывать не забывает (например, использует using), таких накладных расходов нужно суметь избежать.

    В результате, корректная реализация IDisposable включает две функции Dispose, финализатор и вызов GC.SuppressFinalize в финализаторе.

    Надеюсь, стало понятнее. Вот здесь все подробно изложено in english.

    ОтветитьУдалить
  12. Спасибо за ответы. Буду разбираться. Удачи.

    ОтветитьУдалить
  13. Спасибо за статью.
    Только я немного запутался в определениях. Автоматизация Excel это вот это: http://ru.wikipedia.org/wiki/Microsoft_OLE_Automation ?

    ОтветитьУдалить
  14. Огромное спасибо за статью!

    Есть задача: открыть книгу, подгрузить в неё vba-скрипт из внешнего файла и запустить из него определённую функцию на выполнение. Сейчас пытаюсь добавить в интерфейс Application доступ к скриптам. Где можно почитать или может кто уже расширял этот код для работы со скриптами?

    ОтветитьУдалить
  15. Нашёл, что загрузку vba-скрипта из файла можно провести так: ActiveWorkBook.VBProject.VBComponents.Import(lcFile), Осталось сообразить как определить VBProject, сидящий аж в Microsoft.Vbe.Interop.VBProject... Мозги слегка расплавились. :(

    ОтветитьУдалить
  16. Привет, отличная статья! Огромное спасибо =)

    Мне нужно работать с чартами в Excel. Так что я стал добавлять нужные мне сущности в качестве оберток из Object Explorer. Добавил в интерфейс Worksheet метод ChartObjects ChartObjects(object Index); А в ChartObjects добавил object Item(object Index); Сам интерфейс ChartObject тоже добавил.

    Теперь хочу получить ссылку на какой-нибудь чарт. Далее вызываю все как положено:

    ManagedExcel.ChartObject chartObject = (ManagedExcel.ChartObject)charts.Item(ChartName);

    Тут и сваливается исключение: Unable to cast COM object of type 'System.__ComObject' to interface type 'ManagedExcel.ChartObject'...(Exception from HRESULT: 0x80004002 (E_NOINTERFACE)

    ChartName - это строковая переменная, имя чартра. Если заменить на int'овый индекс, то опять исключение. Все остальные интерфейсы подцепились без проблем. Может кто сталкивался с этим?

    ОтветитьУдалить
  17. Трудно что-либо сказать, не видя код целиком (например, не ясно как инициализируется charts). Но вот что заметил. В MSDN метод Worksheet.ChartObjects описывается так:

    public virtual Object ChartObjects (
    [OptionalAttribute] Object Index
    )

    Return Value: an object that represents either a single embedded chart (a Microsoft.Office.Interop.Excel.ChartObject) or a collection of all the embedded charts (a Microsoft.Office.Interop.Excel.ChartObjects) on the worksheet.

    http://msdn.microsoft.com/en-us/library/microsoft.office.tools.excel.worksheet.chartobjects(v=vs.80).aspx

    Т.е. Worksheet.ChartObjects может возвращать как ChartObjects, так и CharObject. Я бы попробовал определить в Worksheet функцию Object ChartObjects(object Index); и вызвать ее так:

    ManagedExcel.ChartObject chartObject = (ManagedExcel.ChartObject)worksheet.ChartObjects(ChartName);

    ОтветитьУдалить
  18. Привет!
    Да, моя вина, забыл указать, как инициализирую переменную charts:

    ManagedExcel.ChartObjects charts = (ManagedExcel.ChartObjects)worksheet.ChartObjects(Type.Missing);

    Ваш совет о приведении сразу к ManagedExcel.ChartObject не помог =( Ошибка все та же.

    Зато обнаружил следующее. Даже если worksheet типа ManagedExcel.Worksheet (т.е. объявлен и описан нами), то метод ChartObjects(object index) не хочет возвращать тип ManagedExcel.ChartObject, зато прекрасно кастится к обычному Excel.ChartObject из набора типов VSTO (Microsoft.Office.Interop.Excel)

    ОтветитьУдалить
  19. Добрый день!
    Спасибо за статью, очень актуально.
    Есть вопрос: подскажите, где отыскать описание всех интефейсов - например Range для диапазона ячеек; в MSDN найти не получается, видимо искать не умею :((

    ОтветитьУдалить
  20. Добрый день,

    Самая полная справка конечно в MSDN
    http://msdn.microsoft.com/en-us/library/microsoft.office.interop.excel.range_members.aspx

    Можно еще из Excel вызвать редактор Visual Basic и воспользоваться его справкой. Порой ее оказывается достаточно.

    ОтветитьУдалить
    Ответы
    1. Спасибо за быстрый ответ, всё равно в данный момент не могу разобраться с описанием

      public interface Range : Common {
      Range this[Object rowIndex, Object columnIndex] { get; set; }
      int Row { get; }
      int Column { get; }
      Range Columns { get; }
      Range Rows { get; }
      String Text { get; }

      object Value { get; set; }
      object Value2 { get; set; }

      В качестве параметров вроде должен получать Object (диапазон ячеек), но передать удаётся только Int (одна ячейка). Пробовал и по простому ("A1","B3") и ячейки передавать, в первом случае ошибка всегда, во втором отрабатывает, если только ячейки содержат числовые значения, но опять получаю одну ячейку.
      P.S. Прошу простить невежество, но раньше использовал Automation

      Удалить
    2. Добрый день.
      А всё-таки выбирать range - диапазон, а не ячейку так и не получилось... А жаль... :-(

      Удалить
  21. Доброго времени суток.

    Добавлять классы и методы научился, к примеру:
    1. в EcxelWrappers.cs добавляем в Range такие строки:
    Interior Interior { get; }
    Font Font { get; }

    2. описываем их чуть ниже:
    public interface Interior : Common
    {
    Interior Interior { get; }
    Object Color { get; set; }
    }

    public interface Font : Common
    {
    Font Font { get; }
    Object Bold { get; set; }
    }

    3. далее обрабатываем ячейки в виде цикла:
    using (Worksheet ws = worksheets[1]) {
    //try to assign some values to some cells
    using (Range cells = ws.Cells) {
    for (int i = 1; i < 10; ++i) {
    using (Range r = cells[1, i]) {
    r.Value = i;
    r.Interior.Color = Color.Red;
    r.Font.Bold = true;
    }
    }
    }
    }

    Но это чертовски не удобно, по сравнению с привычным методом в VBA. Пробовал описать в таком варианте:
    Range this[Object Cell1, Object Cell2] { get; }
    увы не получилось =(

    ОтветитьУдалить
  22. Здравствуйте Виктор. Скажите, подход, который Вы описываете в данной статье имеет сейчас актуальную ценность? Или сейчас есть более современный подход для работы с Excel из .NET? Просто у нас в компании имеется проект в котором также используем позднее связывание для работы с Excel, где потом следим за выгрузкой не используемых ресурсов, но как практика показала не удается все правильно выгружать и COM объекты накапливаются в памяти, что приводит к ошибкам и в дальнейшем к падению виндовой службы. Вот хочу у Вас спросить совета, что использовать для работы с Excel сейчас без привязки к оперделенной версии Excel и без слежения за неиспользуемыми объектами COM и дальнейшей очистки?

    ОтветитьУдалить
    Ответы
    1. Добрый день,
      Насколько я могу судить - подход устарел. Сейчас есть гораздо более удобные варианты работы.

      Раньше, приложения, разрабатываемые для работы с офисом, вынуждены были ссылаться на interop-сборки (например "Microsoft.Office.Interop.Excel") и эти сборки распространялись вместе с приложением. Поскольку сборки для разных версий Excel разные, возникали проблемы.

      В Net 4.0 появились "встраивание метаданных взаимодействия" (embed interop types) и "эквивалентность типов" (type equivalence).

      "Embed interop type" позволяет включать в приложение метаданные из интероп-сборки, причем только те, что реально используются приложением. Таскать интероп-сборку вместе с приложением больше не требуется.

      Если опция включена, то вместо

      var range = (Excel.Range) excel.Cells [1, 1];
      range.Font.FontStyle = "Bold";

      можно сразу писать такой код

      var range2 = excel.Cells [1, 1];
      range.Font.FontStyle = "Bold";

      Важный момент: range2 при этом имеет тип dynamic, а не object. Т.е. фактический тип range2 определяется во время выполнения. IntelliSense для range2 работать не будет, но это легко исправить -
      сразу (динамически) привести к нужному типу

      Excel.Range range2 = excel.Cells [1, 1];
      range.Font.FontStyle = "Bold";

      (пример взят из отличной книжки Albahari J., Albahari B. C# 5.0 in a Nutshell, в ней есть небольшой раздел на эту тему)

      Эквивалентность типов означает, что в период выполнения два разных типа могут считаться эквивалентными и взаимозаменяемыми. В частности, при подключени interop-сборок компилятор считает COM-типы эквивалентными при условии, что у них один и тот же GUID. Соответственно, приложение сможет корректно работать с той версией интероп-сборки, которая есть на машине пользователя - при условии, что в ней присутствуют реализации всех необходимых приложению COM-типов.

      Как это все использовать на практике подробно написано здесь
      http://www.claudiobernasconi.ch/2014/02/13/painless-office-interop-using-visual-c-sharp/

      Сам я на практике предложенный подход пока не использовал (не было подходящего проекта). Так что не могу судить, есть ли в нем аналогичные проблемы с выгрузкой COM-объектов или нет. Будет проект с Excel - попробую обязательно, т.к. работа через "позднее связывание" - то еще счастье..

      Удалить
  23. Спасибо Вам за подробное описание и направление на новый подход. Буду пробовать и исследовать на практике, т.к., мы думали использовать среду DLR, но про подход эквивалентности типов и встраивание метеданных взаимодействия я не слышал. Спасибо за полезную информацию и ссылки. По поводу того как буду пробовать данный подход Вам отпишусь про выгрузку COM-объектов. Уж очень надо решить проблему с полной выгрузкой всех объектов, чтобы предотвратить накопление и соответственно падение службы.

    Просто этот проект у нас в компании старенький висит на .NET Framework 2.0 еще и использует позднее связывание с описанием интерфейсов для MS Excel без привязки к определенной версии Microsoft.Interop.Excel. Есть виндовая служба которая создет пул из трех процессов экземпляра Excel, каждый процесс подымается в своем домене. В дальнейшем данные процессы Excel используеются для генерации отчетов с сайта и через виндовое пиложение. Служба следит за тем корректно ли создаются отчеты и если произошел какой-то сбой, а он происходит обычно с Excel процессами, то служба выгружает данный домен с опеределенным процессом и подымает новый, но все равно где копится видно COM-объекты и не все подчищается. Но может конечно еще дело и в том что в Excel используется надстройка с помощью которой идет связь с базой данных PI через PI SDK откуда вытягиваются данные в шаблоны Excel. Может еще в этом проблема. Так как у нас в службе настроено логирование и проблему все равно не удается опеределить, т.к в логах ошибок не возникает, служба работает работает и вдруг бах остановилась. В виндовых логах кроме как вот этих ошибок ничего конкретного нет:

    Ошибка вязанная с PI от которого поступают данные

    The SDK Yield thread did not stop when signaled during the last Release of the SDKSessionMgr and was terminated.

    и вторая непосредственно нашей службы:
    Имя сбойного приложения: PIRSServerHost.exe, версия: 3.0.21.36502, отметка времени: 0x55965170
    Имя сбойного модуля: ntdll.dll, версия: 6.1.7601.17725, отметка времени 0x4ec49b8f
    Код исключения: 0xc0000005
    Смещение ошибки: 0x00038dc9
    Идентификатор сбойного процесса: 0x66428
    Время запуска сбойного приложения: 0x01d0de0ac41e843f
    Путь сбойного приложения: C:\Program Files (x86)\Indusoft\Сервер отчетов\PIRSServerHost.exe
    Путь сбойного модуля: C:\Windows\SysWOW64\ntdll.dll
    Код отчета: 444c80e6-4a0b-11e5-9ec9-005056ba014f

    Но ничего конкретного определить из данных ошибок не удается.
    Так что буду пробовать Ваш совет.

    ОтветитьУдалить
  24. И еще такой вопрос основываясь на статье приведенной Вами вот по этой ссылке http://www.claudiobernasconi.ch/2014/02/13/painless-office-interop-using-visual-c-sharp/ . Скажите, там описан подход embed interop types и type equivalence, так вот есть необходимость при таком подходе использовать для работы ключевое слово dynamic?

    ОтветитьУдалить
    Ответы
    1. Александр, насколько я могу судить, явно использовать dynamic нет необходимости. Можно написать так

      dynamic range = excel.Cells [1, 1];

      но гораздо удобнее сразу

      Excel.Range range = excel.Cells [1, 1];

      При этом "внутри" dynamic используется неявно, как-то так:
      dynamic j = excel.Cells [1, 1];
      Excel.Range range = (Excel.Range)j;

      Удалить
  25. И поправьте меня, если я не так понял, что то из ссылки на статью. Правильно я полагаю, что если мое приложение имеет необходимость поддержки Office 2003 - Office 2013 у клиента, то сборке приложения я должен хранить ссылку на офис 2003, чтобы поддерживать все остальные более высокие версии, так? И если у клиента вдруг будет на машине будет стоять Office 97, то мое приложение будет работать с ошибками или вообще не будет работать, так.

    ОтветитьУдалить
    Ответы
    1. Имхо вся эта кухня работает следующим образом. Рассмотрим пример. Пусть у нас есть три офиса: 2003, 2010, 2013. В них реализованы фичи:

      2003 - F1
      2010 - F1, F2
      2013 - F1, F2, F3

      Вы собираете приложение на основе интероп сборки для офиса 2010. Вопрос: будет ли работать приложение с 2003 и 2013?

      В приложение вкомпилены метаданные для интеропсборки от десятого офиса (за счет "Embed interop type = true"). Поэтому приложение с любым установленным офисом работает как с десятым. Таким образом фича F3 приложению автоматически недоступна - для нее нет метаданных.

      Фича F1 будет работать в любом офисе начиная с 2003.
      Фича F2 будет работать только в том случае, если на машине пользователя установлен десятый офис или выше. Если же там 2003 офис, то при попытке использовать фичу F2 в рантайме вылетит ошибка.

      Так что если требуется нужна поддержка 2003-офиса, можно использовать интеропсборку от любого офиса - но при этом работать только с теми функциями, которые гарантированно есть в 2003 офисе.

      Я написал коротенькое тестовое приложение, чтобы проверить подход. Вот оно
      https://www.dropbox.com/s/aosprjypqk46v2o/TestExcel.7z?dl=0

      Приложение тривиальное. При нажатии на кнопку загружает из указанного файла все строки из UsedRange и показывает их в TextBox.

      Я собирал его на машине, на которой установлен старый Office 2003. В references добавил ссылку на Microsoft.Office.Interop.Excel версии 14 (т.е. от офиса 2010).

      С офисом 2003 работает без проблем. С десяткой тоже. На машине без офиса запускается - но при нажатии на кнопку загрузки данных - падает. Все как и должно быть.

      Проверил жрет ли память. Проблема с памятью никуда не делась. Чтобы эксели не зависали в списке процессов после того, как работа с ним закончена, следует: 1) не допускать неявного использования COM-объектов 2) явно релизить com-объекты после того, как они стали не нужны.

      Т.е. нельзя написать

      app.Workbooks.Open(workbookFullName)

      вместо этого следует написать так

      Workbooks workbooks = app.Workbooks;
      Workbook book = workbooks.Open(workbookFullName);

      и затем явно зарелизить объекты

      Marshal.FinalReleaseComObject(book);
      Marshal.FinalReleaseComObject(workbooks);

      а в конце и объект приложения
      app.Quit();
      Marshal.FinalReleaseComObject(app);

      Рекомендую посмотреть описание Marshal.FinalReleaseComObject в msdn, там подробнее написано.

      Удалить
  26. Здраствуйте! Подскажите пожалуйста, а как можно поступить в такой вот интересной ситуации:
    Если на ПК установлен Microsoft Office то данный подход будет работать... Но! Есть такой нюанс: пробовал сохранить в Ексель-файл на ПК, где Microsoft Office не установлен, либо стоит альтернативный офис типа Либры или OpenOffice... И такая конструкция уже не срабатывает!(( Выдает ошибки, мол COM не зарегистрирован в системе и всё такое...
    Отсюда у меня вопрос: как можно реализовать сохранение/открытие в/из файлов Ексель на форму программы, без вызова СОМ? Ну или с вызовом, если без этого никак, но как тогда для случаев, когда конкретно майкрософтовский Excel не установлен на ПК, или вообще никакой не установлен, а из моей программы нужно просто выполнить сохранение данных в екселевский файл, как в таком случае реализовать сохранение/чтение в/из вайла эксель-форматов?

    ОтветитьУдалить
    Ответы
    1. Можно попробовать задействовать одну из библиотек, способных считывать/создавать Excel-файлы без установленного Excel.

      Например, NPOI и EPPlus. Первая поддерживает форматы xls и xlsx, вторая - только xlsx.

      Если требуется только считывать данные, то есть еще интересная библиотека ExcelDataReader.

      Удалить
    2. Нужно и считывать и сохранять из грда в файл...
      А если нужно еще помимо xls сохранять и читать в/из формат .csv, то как быть? Могут ли эти библиотеки работать с таким форматом?

      Удалить