Excel-DNA

Excel-DNA - библиотека для .NET, с помощью которой можно написать и собрать полностью самодостаточный файл надстройки для Excel с расширением .xll
Этот файл достаточно положить в пользовательскую папку, без прав администратора, и просто включить в настройках Excel.

Такая надстройка будет иметь доступ к COM-модели Excel, C API Excel - позволяет взаимодействовать с интерфейсом программы и рабочими книгами. Сможет реализовать пользовательские формулы и добавить свое меню на интерфейсную ленту Ribbon UI.

В статье я расскажу как написать свою формулу для Excel на C#. Собрать и установить свою первую надстройку.

Подготовка

Создадим новый проект библиотеки классов для версии .NET 6

dotnet new classlib --framework net6.0 -o ExcelAddIn

В файле .csproj нужно изменить значение версии фреймворка на

    <TargetFramework>net6.0-windows</TargetFramework>

Теперь установим базовый пакет ExcelDna.AddIn

dotnet add package ExcelDna.AddIn

Писать можно и в Visual Studio, и в VSCode.

Чтобы дебажить код в VSCode нужно создать файл launch.json и в нем изменить две строки, "program" и "args":

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": ".NET Core Launch (console)",
            "type": "coreclr",
            "request": "launch",
            "preLaunchTask": "build",
            "program": "C:\\Program Files\\Microsoft Office\\root\\Office16\\EXCEL.EXE",
            "args": [
                "${workspaceFolder}\\bin\\Debug\\net6.0-windows\\ExcelAddIn-AddIn64.xll"
            ],
            "cwd": "${workspaceFolder}",
            "console": "internalConsole",
            "stopAtEntry": false
        },
        {
            "name": ".NET Core Attach",
            "type": "coreclr",
            "request": "attach"
        }
    ]
}

Пути к файлу программы Excel и собранной надстройки нужно поставить свои.

Первая формула

Напишем простую формулу, которая складывает два числа:

global using ExcelDna.Integration; 

namespace ExcelAddIn;  

public static class ExcelFunctions
{
    [ExcelFunction]
    public static double DNASUM(double a, double b)
    {
        return a + b;
    }
}

Все статические методы помеченные атрибутом [ExcelFunction] теперь принимаются Excel как пользовательские формулы.

Нажмем F5, откроется Excel, который уже откроет файл нашей надстройки. Поскольку у нас нет цифровой подписи появится предупреждение.

Извещение системы безопасности
Извещение системы безопасности

Нажимаем "Включить" и проверяем работу нашей формулы

Первая формула работает!
Первая формула работает!

Интерфейс IExcelAddIn

Для работы нашей надстройки может понадобится, чтобы она при открытии и закрытии выполняла какие-либо полезные вещи. Например читала настройки из реестра, подключала базы данных.

Cоздадим класс MyAddIn, реализующий интерфейс IExcelAddIn

public class MyAddIn : IExcelAddIn
{
    public void AutoClose()
    {
        throw new NotImplementedException();
    }  

    public void AutoOpen()
    {
        throw new NotImplementedException();
    }
}

При открытии надстройки будет создан экземпляр класса MyAddIn и выполнен метод AutoOpen()

Делаем формулу запроса курса валют с сайта ЦБ

Напишем теперь формулу, которая делает что-то полезное, например запрашивает курс валют с сайта ЦБ.

Интерфейс для клиента:

namespace ExcelAddIn.Services; 

public interface ICurrencyClient
{
    public Task<decimal?> GetExchangeRate(DateTime date);
}

Класс клиента API сайта ЦБ. Парсим XML с курсами на нужную дату:

using System.Diagnostics;
using System.Text;
using System.Xml.Linq;  

namespace ExcelAddIn.Services;  

public class CurrencyClient : ICurrencyClient
{
    private readonly HttpClient _httpClient;
    private const string _requestAddress = @"https://www.cbr.ru/scripts/XML_daily.asp?date_req=";  

    public CurrencyClient(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }  

    public async Task<decimal?> GetExchangeRate(DateTime date)
    {
        string request = $"{_requestAddress}{date.Date:dd/MM/yyyy}";
        HttpResponseMessage response = await _httpClient.GetAsync(request);
        try
        {
            response.EnsureSuccessStatusCode();
            var bytes = await response.Content.ReadAsByteArrayAsync();
            var xml = Encoding.GetEncoding(1251).GetString(bytes);
            XElement valCourses = XElement.Parse(xml);  

            decimal? exchangeRate = decimal.Parse(valCourses.Elements("Valute")
                .Where(e => e.Element("Name").Value == "Евро")
                .FirstOrDefault()
                .Element("Value").Value);
            return exchangeRate;
        }

        catch (Exception ex)
        {
            Debug.WriteLine(ex.Message);
            return null;
        }
    }
}

Добавляем нужные сервисы и создаем провайдер сервисов

using Microsoft.Extensions.DependencyInjection;
using ExcelAddIn.Services;
using System.Text;

public class MyAddIn : IExcelAddIn
{
    public static ServiceProvider ServiceProvider { get; private set; }
    public void AutoOpen()
    {        
        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
        IServiceCollection Services = new ServiceCollection();
        Services.AddHttpClient()
            .AddSingleton<ICurrencyClient, CurrencyClient>();
        ServiceProvider = Services.BuildServiceProvider();
    }

    public void AutoClose()
    {
    }
}

И, наконец, определяем метод для формулы:

using ExcelAddIn.Services;
using Microsoft.Extensions.DependencyInjection;  

namespace ExcelAddIn;  

public static class ExcelFunctions
{  	
    [ExcelFunction]
    public static object ExchangeRate(double dateField)
    {
        ICurrencyClient currencyClient = MyAddIn.ServiceProvider.GetService<ICurrencyClient>();
        DateTime date = dateField == 0 ? DateTime.Today : DateTime.FromOADate(dateField);  

        if (ExcelAsyncUtil.Run(nameof(ExchangeRate), dateField, delegate
        {
            return currencyClient.GetExchangeRate(date)
                .GetAwaiter()
                .GetResult() ?? -1m;
        }) is not decimal requestResult)
        {
            return "Загрузка...";
        }  

        else if (requestResult < 0)
        {
            return ExcelError.ExcelErrorNA;
        }

        else
        {
            return Math.Round(requestResult, 2);
        }
    }
}

Теперь формула =ExchangeRate( ) возвращает сегодняшний курс евро, а если сослаться на ячейку с датой в формате Excel, на нужную дату.

Сборка и установка

Чтобы собрать нашу надстройку в один файл со всеми зависимостями, нужно добавить в файл .csproj строки

  <Target Name="PackedReferences" AfterTargets="AfterBuild" BeforeTargets="ExcelDnaBuild">
    <ItemGroup>
      <References Include="$(OutDir)*.dll" Exclude="$(OutDir)$(TargetFileName)" />
    </ItemGroup>
    <PropertyGroup>
      <ExcelAddInInclude>@(References)</ExcelAddInInclude>
    </PropertyGroup>
  </Target>

Теперь делаем

dotnet build

и в папке \bin\Debug\net6.0-windows забираем два собранных файла для 32 и 64-разрядных версий Excel

Эти файлы нужно поместить в папку %AppData%\Microsoft\AddIns и в настройках Excel поставить галочку напротив нашего файла. Готово!

Заключение

В этой статье описывается как написать и собрать простую надстройку для Excel средствами .NET.

Код можно посмотреть по ссылке https://gitea.cebotari.ru/chebser/ExcelAddIn/

Комментарии (13)


  1. Sazonov
    10.06.2023 19:31

    А под мак оно будет работать?


    1. schebotar Автор
      10.06.2023 19:31

      Windows-only, насколько мне известно.


  1. AlexeyK77
    10.06.2023 19:31
    +1

    Вирусописатели говорят большое спасибо корпорации микрософт еще за один сопособ незметного протаскивания и запуска всяких зловредов в обход систем безопасности.


    1. vitesse
      10.06.2023 19:31
      +5

      В статье написано "Поскольку у нас нет цифровой подписи появится предупреждение" и если вы скачали расширение без подписи и установили себе, а потом подтвердили запуск с "возможным нарушением безопасности", то "вирусописатели говорят большое спасибо" вам.
      Microsoft не может заблокировать полностью запуск без подписи - тестировать же нужно как-то расширения, да и для личных нужд можно без подписи (мы когда делали код для автоматического заполнения таблиц по XML-datasource, сделали расширение чтоб выводить все биндинги в виде tooltip для помощи себе в отладке).


      1. Skykharkov
        10.06.2023 19:31
        +1

        Вот именно. Если пользователь видит окно где явно написано - "Нет подписи\сертификата" или там "Издатель неизвестен" то он сам себе злобный буратино. А если админы такое разрешают запускать в корпоративной середе, то гнать таких админов ссаными тряпками на мороз.


        1. AlexeyK77
          10.06.2023 19:31

          Немножко соц.инженерии и пользователь нажмет ОК хоть сто раз. Основная проблема, что исполняемый код можно поместить в пользователский каталог, а потом офис его сам подгрузит в свое пространство. Это же классика жанра.


    1. schebotar Автор
      10.06.2023 19:31

      Один из побочных эффектов на самом деле, это то что Windows Defender очень не любит такие надстройки и может видеть в них вирус даже если ты их пишешь сам и знаешь что там зловреда нет.


  1. BasiC2k
    10.06.2023 19:31
    +1

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


    1. schebotar Автор
      10.06.2023 19:31

      С VSTO не работал.

      Среди преимуществ ExcelDNA называют большую производительность, и то что это Open Source решение.


      1. BasiC2k
        10.06.2023 19:31
        +1

        На будущее - большим плюсом к статье будет обзор и сравнение похожих технлогий.


    1. aborouhin
      10.06.2023 19:31
      +1

      Сам c Excel-DNA не работал (меня больше Word интересует), но, я так понимаю, из плюсов у неё остался более низкий порог входа для VBA-макрописателей и поддержка .NET Core, а не только Framework. Раньше, вроде, VSTO ещё требовало платной Visual Studio и не умело в UDF, но сие в прошлом.

      Но вообще и то, и то, увы, deprecated. Для поддержки веб- и мак-версий Office (по моим впечатлениям, первыми пользуются не так часто, а вот маков расплодилось множество) остаются только Office Add-Ins на JS (причём интерфейс add-in'а можно на Blazor Webassembly написать, а вот взаимодействие с Office API - только JS без вариантов, печаль).


  1. Bagir123
    10.06.2023 19:31

    А что на net 6.0 уже работает? Надо бы проверить


    1. schebotar Автор
      10.06.2023 19:31

      Да, с последним обновлением обеспечили поддержку.

      Единственный нюанс, на целевой машине должен быть установлен соответствующий рантайм.