Pull to refresh

Улучшаем экспорт в Excel для SharePoint

Reading time6 min
Views11K
Стандартная возможность экспорта в Excel в SharePoint работает довольно необычным образом. При нажатии на кнопку экспорта SharePoint отдает файл запроса в специальном формате, который открывается Excel, и уже сам Excel затягивает данные.

Преимущество такого подхода в том, что данные в Excel можно обновить, так как есть соединение. Но недостатков больше:

  • Нужен установленный Excel на компьютер, чтобы получить данные.
  • В полученном документе для названий колонок используется InternalName полей.
  • В полученном документе используется “сырой” формат данных, что далеко не всегда подходит.

С помощью небольшого объема кода можно подменить стандартную функцию экспорта на свою так, что пользователи ничего не заметят.


Подмена кнопок экспорта


Чтобы заменить существующий элемент в Ribbon надо добавить новый CommandUIDefinition с параметром Location равным Id существующего элемента. Все стандартные элементы находятся в файле C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\TEMPLATE\GLOBAL\XML\CMDUI.XML для SharePoint 2010 (или 15 для SharePoint 2013).

Для того, чтобы подменить кнопки экспорта в Excel надо найти кнопки с Id равными Ribbon.List.Actions.ExportToSpreadsheet и Ribbon.Library.Actions.ExportToSpreadsheet и полностью скопировать в свой проект. В качестве Location для новых элементов надо указать эти Id.

Много копипасты
<CommandUIDefinitions>
  <CommandUIDefinition
    Location="Ribbon.Library.Actions.ExportToSpreadsheet">
    <Button Id="Ribbon.Library.Actions.ExportToSpreadsheet-Replacement"
      Sequence="40"
      Command="ExportToSpreadsheet-Replacement"      
      Image16by16="/_layouts/$Resources:core,Language;/images/formatmap16x16.png" Image16by16Top="-152" Image16by16Left="-32"
      Image32by32="/_layouts/$Resources:core,Language;/images/formatmap32x32.png" Image32by32Top="-352" Image32by32Left="0"
      LabelText="$Resources:core,cui_ButExportToSpreadsheet;"
      ToolTipTitle="$Resources:core,cui_ButExportToSpreadsheet;"
      ToolTipDescription="$Resources:core,cui_STT_ButExportListToSpreadsheet;"
      TemplateAlias="o2"
        />
  </CommandUIDefinition>
  <CommandUIDefinition
    Location="Ribbon.List.Actions.ExportToSpreadsheet">
    <Button Id="Ribbon.List.Actions.ExportToSpreadsheet-Replacement"
      Sequence="40"
      Command="ExportToSpreadsheet-Replacement"
      Image16by16="/_layouts/$Resources:core,Language;/images/formatmap16x16.png" Image16by16Top="-152" Image16by16Left="-32"
      Image32by32="/_layouts/$Resources:core,Language;/images/formatmap32x32.png" Image32by32Top="-352" Image32by32Left="0"
      LabelText="$Resources:core,cui_ButExportToSpreadsheet;"
      ToolTipTitle="$Resources:core,cui_ButExportToSpreadsheet;"
      ToolTipDescription="$Resources:core,cui_STT_ButExportListToSpreadsheet;"
      TemplateAlias="o1"
        />
  </CommandUIDefinition>
</CommandUIDefinitions>


Есть еще отдельная кнопка для календаря с Id="Ribbon.Calendar.Calendar.Actions.ExportToSpreadsheet", с ней можно сделать тоже самое.

Создание контрола


Кнопка должна вызывать некоторый серверный код. Чтобы поведение было аналогично стандартной кнопке, лучше всего сделать DelegateControl, который будет размещаться на каждой странице. Для этого есть контейнер с Id="AdditionalPageHead":
<Control 
  Id="AdditionalPageHead"
  Sequence="1000"
  ControlAssembly="$SharePoint.Project.AssemblyFullName$" 
  ControlClass="$SharePoint.Type.7fd7c6f0-4eda-48ce-ac8f-aa9f9d2666ac.FullName$"/>

Кастомная кнопка будет вызывать PostBack, обрабатываемый нашим контролом. Кнопка может быть нажата на странице представления списка, тогда не надо передавать дополнительных параметров. Также представления списков могут быть добавлены на обычные страницы как веб-части, тогда нужно передать Id списка и представления на сервер.

Увы, получение Id представления, в котором была нажата кнопка – нетривиальная задача. Поэтому я ограничусь только передачей Id списка.

<CommandUIHandlers>
  <CommandUIHandler
    Command="ExportToSpreadsheet-Replacement"
    CommandAction="javascript:(function(){var x=SP.ListOperation.Selection.getSelectedList(); if (x) {__doPostBack('ExportToSpreadsheet-Replacement', x);}})();" />
</CommandUIHandlers>


Далее на сервере нужно проверить нажата ли кнопка на странице представления списка или на обычной странице:

protected override void OnLoad(EventArgs e) { 
    if (this.Page.Request["__EVENTTARGET"] == "ExportToSpreadsheet-Replacement") 
    {
        var spContext  = SPContext.Current;
        SPList list;
        SPView view;

        if (spContext.ViewContext.View != null)
        {
            list = spContext.List;
            view = spContext.ViewContext.View;
        }
        else
        {
            var listId = new Guid(this.Page.Request["__EVENTARGUMENT"]);
            var web = spContext.Web;
            list = web.Lists[listId];
            view = list.DefaultView;
        }

        ExportData(list.Title + " - " + view.Title, GetDataTable(list, view));
    } 
}


Получение таблицы данных по представлению очень просто:

private static System.Data.DataTable GetDataTable(SPList list, SPView view)
{
    var query = new SPQuery(view);
    SPListItemCollectionPosition position;
    var flags = SPListGetDataTableOptions.UseBooleanDataType | SPListGetDataTableOptions.UseCalculatedDataType;
    var result = list.GetDataTable(query, flags, out position);
    while (position != null)
    {
        list.AppendDataTable(query, flags, result, out position);
    }
    return result;
}


Генерация Excel файла



Последним шагом требуется сформировать Excel файл и отдать его клиенту. Один из самых простых способов сгенерировать Excel – использовать библиотеку ClosedXml (http://closedxml.codeplex.com/).

private void ExportData(string title, System.Data.DataTable table)
{
    var wb = new XLWorkbook();
    var ws = wb.Worksheets.Add(title);
    ws.Cell(1, 1).InsertTable(table);

    var response = this.Page.Response;
    response.Clear();

    response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
    var filename = title+".xlsx";
    response.AddHeader("content-disposition", GetContentDisposition(filename));

    // Flush the workbook to the Response.OutputStream
    using (var memoryStream = new MemoryStream())
    {
        wb.SaveAs(memoryStream);
        memoryStream.WriteTo(response.OutputStream);
    }
    response.End();
}


Первые три строки метода собственно формируют Excel документ (спасибо ClosedXml), а все остальное – код для отдачи файла клиенту.

Заголовок ответа content-disposition очень по-разному воспринимается разными браузерами, поэтому отдать файл с корректным русским именем требует некоторых танцев с бубном.

Код для формирования корректного заголовка content-disposition я нашел на StackOverflow stackoverflow.com/questions/93551/how-to-encode-the-filename-parameter-of-content-disposition-header-in-http

private string GetContentDisposition(string filename)
{
    var request = this.Page.Request;
    string contentDisposition;

    if (request.Browser.Browser == "IE" && (request.Browser.Version == "7.0" || request.Browser.Version == "8.0"))
        contentDisposition = "attachment; filename=" + Uri.EscapeDataString(filename);
    else if (request.UserAgent != null && request.UserAgent.ToLowerInvariant().Contains("android")) // android built-in download manager (all browsers on android)
        contentDisposition = "attachment; filename=\"" + MakeAndroidSafeFileName(filename) + "\"";
    else
        contentDisposition = "attachment; filename=\"" + filename + "\"; filename*=UTF-8''" + Uri.EscapeDataString(filename);
    return contentDisposition;
}

private static readonly Dictionary<char, char> AndroidAllowedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._-+,@£$€!½§~'=()[]{}0123456789".ToDictionary(c => c);
private string MakeAndroidSafeFileName(string fileName)
{
    char[] newFileName = fileName.ToCharArray();
    for (int i = 0; i < newFileName.Length; i++)
    {
        if (!AndroidAllowedChars.ContainsKey(newFileName[i]))
            newFileName[i] = '_';
    }
    return new string(newFileName);
}


Заключение


Чтобы все заработало на Production надо добавить файлы ClosedXml.dll и DocumentFormat.OpenXml.dll в ваш пакет. Кнопки Ribbon и контрол поместить в одну фичу уровня Site или Web.

Весь проект можно посмотреть по ссылке — spsamples.codeplex.com/SourceControl/latest#ExportToExcel

Готовый WSP файл тут — spsamples.codeplex.com/releases/view/117220

Большая часть кода универсальна, и не зависит от SharePoint. Вы можете использовать аналогичный подход в любом проекте на ASP.NET.
Tags:
Hubs:
Total votes 15: ↑13 and ↓2+11
Comments1

Articles