Я – студент старших курсов одного из столичных технических вузов, который по долгу службы тесно связан с настройкой и разработкой корпоративных порталов на платформе SharePoint 2013. В этой статье я хочу рассказать и показать, как можно с помощью Excel файла наполнить и очистить список SharePoint, а также выгрузить необходимое содержимое.

А зачем это, собственного говоря, надо? Давайте представим ситуацию, что у нас есть крупная организация «Бендер и Ко», которая включает в себя некоторое число независимых филиалов, для простоты эксперимента ограничимся двумя – «Рога» и «Копыта». Организации быстро растут, приходят новые сотрудники, нужно вносить о них информацию, содержащую порядка 10 атрибутов. Времени на занесение 2-3 человек в список SharePoint вручную много не потребуется, а если таких 20 или 50? Да и Excel пользователям пока ближе, чем SharePoint. Таким образом, с целью экономии времени, я предлагаю экспортировать данные из Excel.

Импорт, экспорт и удаление данных будем осуществлять с помощью собственной веб-части SharePoint.

Итак, поехали.

1. Создаем список «Сотрудники» в SharePoint и подготавливаем шаблон Excel документа для загрузки пользователей


Мой список, выглядит вот так:



Прошу обратить внимание, что данные 3 элемента являются папками, внутрь которых мы будем добавлять сотрудников.

А шаблон Excel — вот так:



Стоит обратить внимание на то, что тип данных столбца в списке SharePoint должен совпадать с типом данных соответствующего атрибута в документе Excel. Я все поля сделал текстовыми.

2. Создаем проект SharePoint 2013


New Project -> SharePoint Solutions -> SharePoint 2013 — Empty Project -> ОК.



Далее указываем узел, выбираем Deploy as a farm solution и нажимаем Finish.



Следующим шагом необходимо добавить Visual Web Part. Клик правой кнопкой мыши на решение -> Add -> New Item -> Visual Web Part -> Add.



3. Разрабатываем внешний вид веб-части


За внешний вид отвечает файл .ascx, который открывается перед нами сразу после нажатия кнопки Add из предыдущего пункта. В своем примере я ограничусь 2 лейблами для вывода информации в ходе работы веб-части; элементом FileUpload, который позволит выбрать файл для загрузки; DropDownList — выпадающий список, содержащий главную организацию и филиалы; 3 кнопки для добавления, удаления и экспорта элементов списка. После добавления следующего кода:

<asp:Label ID="Label1" runat="server" Text="" EnableViewState="false"></asp:Label>
<asp:Label ID="Label2" runat="server" Text=""></asp:Label>
<div id="dvSource" runat="server">
   	<fieldset style="width:51%; max-width:600px; min-width:600px;">
   	<legend></legend>
   	<table>
       	<tr>
           	<td>Выберите Excel файл :  </td>
           	<td>
               	<asp:FileUpload ID="FileUpload1" runat="server" Width="453px" />
           	</td>
        	</tr>
        	<tr>
           	<td>Выберите филиал : </td>
           	<td>
               	<asp:DropDownList ID="DropDownList1" runat="server" Width="238px">
               	<asp:ListItem Enabled="true" Text="" Value="-1"></asp:ListItem>
                   	<asp:ListItem Text="Бендер и Ко" Value="1"></asp:ListItem>
                   	<asp:ListItem Text="Рога" Value="2"></asp:ListItem>
                   	<asp:ListItem Text="Копыта" Value="3"></asp:ListItem>
               	</asp:DropDownList>
           	</td>
       	</tr>   
     	</table>
<div style="float:left; margin-top:20px; width:80%; margin-left:122px">
   <asp:Button ID="Button1" runat="server" OnClick="btnUpload_Click" Text="Загрузить данные"  />
   <asp:Button ID="Button2" runat="server" OnClick="btnRemove_Click" Text="Загрузить данные на удаление" />
   <asp:Button ID="Button3" runat="server" OnClick="btnUnload_Click" Text="Выгрузить данные в Excel"  />
           	</div>
	</fieldset>
</div>

Мы получим вот такую веб-часть:



4. В файле .webpart, если необходимо, меняем Title и Description веб-части


5. Подключаем библиотеку EPPlus



Для того, чтобы работать с документами Excel скачаем и добавим к проекту .net библиотеку EPPlus. Клик правой кнопкой мыши на решение -> Add Reference -> Browse -> Выбираем EPPlus.dll -> OK.



Также необходимо добавить EPPlus.dll в wsp пакет. Левой кнопкой мыши двойной клик на папку Package -> Advanced -> Add -> Add Existing Assembly -> Выбираем EPPlus.dll -> OK.



Последний шаг состоит в подключении
using OfficeOpenXml;
using Microsoft.SharePoint;
using System.IO;
using System.Collections.Generic;
в файл .ascx.cs.

6. Пишем функциональный код


6.1. Создаем связь между полями документа Excel и столбцами списка SharePoint с помощью Dictionary


private Dictionary<int, string> GetColumnMapping()
   {
	       	Dictionary<int, string> map = new Dictionary<int, string>();
	       	map.Add(0, "ИД");
	       	map.Add(1, "ФИО");
	       	map.Add(2, "Должность");
	       	map.Add(3, "E-mail");
	       	map.Add(4, "Номер факса");
	       	map.Add(5, "Внутренний номер");
	       	map.Add(6, "Служебный телефон");
	       	map.Add(7, "Адрес");
	       	map.Add(8, "Номер сотрудника");
	       	map.Add(9, "День рождения"); 
	       	return map;
   }

Первым параметром при вызове метода Add является индекс столбца в таблице Excel, вторым — Display Name столбца в списке SharePoint.

Теперь мы полностью готовы к написанию основного кода.

6.2. Обработка события при нажатии на кнопку «Загрузить данные»

protected void btnUpload_Click(object sender, EventArgs e)
        {
            if (!FileUpload1.HasFile)
            {
                Label1.Text = "Вы не выбрали Excel файл!";
                return;
            }
            try
            {
                string Url = SPContext.Current.Web.Url;
                using (SPSite spSite = new SPSite(Url))
                {
                    using (SPWeb spWeb = spSite.OpenWeb())
                    {
                        spWeb.AllowUnsafeUpdates = true;
                        SPList list = spWeb.Lists["Сотрудники"];
                        var folderDZO = list.RootFolder.SubFolders[DropDownList1.SelectedItem.Text].ToString();
                        SPFolder folder = spWeb.GetFolder(folderDZO);            
                        string LoginName = SPContext.Current.Web.CurrentUser.LoginName;

                        if (CheckPermission(LoginName, folder.Item, spWeb) == false)
                        {
                            Label2.Text += "У Вас нет доступа на редактирование элементов данной папки!";
                            return;
                        }
                        else
                        {
                            try
                            {
                                byte[] fileData = FileUpload1.FileBytes;
                                using (MemoryStream memStream = new MemoryStream(fileData))
                                {
                                    memStream.Flush();
                                    using (ExcelPackage pck = new ExcelPackage(memStream))
                                    {
                                        if (pck != null)
                                        {
                                            CreateListItem(pck, list, spWeb);
                                        }
                                    }
                                }

                                Label1.Text = "Данные успешно загружены...";
                            }
                            catch (Exception Ex1)
                            {
                                Label1.Text = "Возникла ошибка 
" + Ex1.Message;
                            }
                        }
                    }
                }
            }
            catch (Exception Ex)
            {
                Label1.Text = "Возникла ошибка 
" + Ex.Message;
            }
        }

Как я уже говорил в самом начале, организация включает в себя главную организацию и 2 филиала. Естественно предположить, что администраторы, добавляющие сотрудников в свои филиалы, будут разными, следовательно, у папок будут уникальные права доступа. Таким образом, прежде чем выполнить операцию создания элемента, неплохо бы убедиться в том, что у нас есть права на изменение элементов конкретной папки. Проверку осуществляет простенькая функция CheckPermission.

public static bool CheckPermission(string userName, SPListItem folders, SPWeb spWeb)
        {
            bool perm = false;
            SPUser oUser = spWeb.CurrentUser;
            perm = (bool)folders.DoesUserHavePermissions(oUser, SPBasePermissions.EditListItems);
            return perm;
        }

Все возможные значения параметра SPBasePermissions вы можете просмотреть тут.

Далее используется функция CreateListItem, которая непосредственно производит добавление пользователя.

private void CreateListItem(ExcelPackage pck, SPList list, SPWeb spWeb)
        {
            Dictionary<int, string> column = GetColumnMapping();
            ExcelWorksheet ws = pck.Workbook.Worksheets[1];
            int rowCount = ws.Dimension.End.Row + 1;
            int colCount = ws.Dimension.End.Column + 1;
            var folderDZO = list.RootFolder.SubFolders[DropDownList1.SelectedItem.Text];
            SPListItemCollection itemColl = list.Items;

            for (int i = 2; i < rowCount && ws.Cells[i, 1].Value != null; i++)
            {
                if (IsPeople(ws.Cells[i, 1].Value.ToString(), list) == false)
                {                 
                    SPListItem item = list.AddItem(folderDZO.ServerRelativeUrl, SPFileSystemObjectType.File, null);
                    for (int j = 1; j < colCount; j++)
                    {
                        if (column.ContainsKey(j))
                            item[column[j]] = ws.Cells[i, j].Value.ToString();
                    }
                    item.Update();
                }
                else Label2.Text += " Пользователь " + ws.Cells[i, 1].Value.ToString() + " не был добавлен, поскольку он уже существует!";
            }
        }

Внутри этого метода мы обращаемся к функции IsPeople, которая проверяет не был ли данный пользователь добавлен ранее.

public static bool IsPeople(string fio, SPList list)
        {
          
            foreach (SPListItem item in list.Items)
            {
                if (item["ФИО"].ToString() == fio)
                    return true;
            }

            return false;
        }

Поскольку я знаю, что людей с одинаковой фамилией, именем и отчеством в моем примере нет, то в качестве атрибута для проверки я выбрал столбец ФИО. В реальной же ситуации необходимо выбрать столбец, который однозначно подчеркнет уникальность записи, например, E-mail.

6.3. Обработка события при нажатии на кнопку «Загрузить данные на удаление»

Непосредственно обработка точно такая же как и в предыдущем пункте. Однако функция CreateListItem немного модифицирована.

private void RemoveListItem(ExcelPackage pck, SPList list, SPWeb spWeb)
        {
            Dictionary<int, string> column = GetColumnMapping();
            ExcelWorksheet ws = pck.Workbook.Worksheets[1];
            int rowCount = ws.Dimension.End.Row + 1;
            int colCount = ws.Dimension.End.Column + 1;
            var folderDZO = list.RootFolder.SubFolders[DropDownList1.SelectedItem.Text];
            SPListItemCollection itemColl = list.Items;

            for (int i = 2; i < rowCount && ws.Cells[i, 1].Value != null; i++)
            {
                if (IsPeople(ws.Cells[i, 1].Value.ToString(), list) == true)
                {
                   
                    foreach (SPListItem item in list.Items)
                    {
                        if (ws.Cells[i, 1].Value.ToString() == item["ФИО"].ToString())
                        {
                            item.Delete();
                            break;
                        }
                    }
                    list.Update();
                }
                else Label2.Text += " Пользователь " + ws.Cells[i, 1].Value.ToString() + " не был удален, поскольку его не существует!";
            }
        }

6.4. Обработка события при нажатии на кнопку «Выгрузить данные в Excel»

 protected void btnUnload_Click(object sender, EventArgs e)
        {
            string Url = SPContext.Current.Web.Url;
            using (SPSite spSite = new SPSite(Url))
            {
                using (SPWeb spWeb = spSite.OpenWeb())
                {
                    spWeb.AllowUnsafeUpdates = true;
                    SPList list = spWeb.Lists["Сотрудники"];
                    SPFolder folder = list.RootFolder.SubFolders[DropDownList1.SelectedItem.Text.ToString()];

                    SPQuery query = new SPQuery();
                    query.Folder = folder;
                    SPListItemCollection listitem = list.GetItems(query);

                    ExcelPackage pck = new ExcelPackage();
                    var ws = pck.Workbook.Worksheets.Add("Сотрудники");

                    int rowIndex = 1;
                    ws.Cells[rowIndex, 1].Value = "ИД";
                    ws.Cells[rowIndex, 2].Value = "ФИО";
                    ws.Cells[rowIndex, 3].Value = "Должность";
                    ws.Cells[rowIndex, 4].Value = "E-mail";
                    ws.Cells[rowIndex, 5].Value = "Номер факса";
                    ws.Cells[rowIndex, 6].Value = "Внутренний номер";
                    ws.Cells[rowIndex, 7].Value = "Служебный телефон";
                    ws.Cells[rowIndex, 8].Value = "Адрес";
                    ws.Cells[rowIndex, 9].Value = "Номер сотрудника";
                    ws.Cells[rowIndex, 10].Value = "Дата рождения";
                    rowIndex++;
           
                    for (int i = 0; i < listitem.Count; i++)
                    {
                        SPListItem item = listitem[i];
                        ws.Cells[rowIndex, 1].Value = item["ИД"];
                        ws.Cells[rowIndex, 2].Value = item["ФИО"];
                        ws.Cells[rowIndex, 3].Value = item["Должность"];
                        ws.Cells[rowIndex, 4].Value = item["E-mail"];
                        ws.Cells[rowIndex, 5].Value = item["Номер факса"];
                        ws.Cells[rowIndex, 6].Value = item["Внутренний номер"];
                        ws.Cells[rowIndex, 7].Value = item["Служебный телефон"];
                        ws.Cells[rowIndex, 8].Value = item["Адрес"];
                        ws.Cells[rowIndex, 9].Value = item["Номер сотрудника"];
                        ws.Cells[rowIndex, 10].Value = item["День рождения"];
                        rowIndex++;
                    }

                    pck.SaveAs(this.Page.Response.OutputStream);
                    this.Page.Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
                    this.Page.Response.AddHeader("content-disposition", "attachment;  filename=List_of_Employees_as_on_" + DateTime.Now.ToString("dd_MMM_yyyy_HH_mm_ss") + ".xlsx");
                    this.Page.Response.Flush();
                    this.Page.Response.End();
                
              }
          }
      }

На этом кодить закончили.

7. Развернем решение и добавим веб-часть на страницу


Сначала убедимся, что наш элемент Visual Web Part добавлен в фичу. В окне Solution Explorer переходим в папку Feature и двойной клик по Feature1. Если элемент уже добавлен в фичу — Items in the Feature, то можно идти дальше, если нет – нужно переместить элемент из левого окошка в правое.



Далее правой кнопкой мыши кликаем на решение ->Deploy. Дожидаемся сообщения Deploy succeeded. Теперь решение развёрнуто на портале. Веб-часть располагается в папке Custom.



8. Проверим работу веб-части


8.1. Добавим сотрудников





8.2. Протестировать удаление можно шаблоном, который мы использовали в предыдущем пункте


8.3. Выгрузим сотрудников





Заключение. Потратив некоторое время на разработку веб-части, мы значительно сэкономили время администраторов филиалов, которым пришлось бы вручную заносить сотрудников в список. Таким образом, кнопочная форма и Excel сделали работу SharePoint еще проще и быстрее.

Решение можно скачать тут.

Спасибо за внимание.

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


  1. BOBS13
    09.07.2015 18:54

    Хорошо описан процесс создания Веб-части для Sharepoint. Пара замечаний по поводу кода.
    Прямые обращения к полю по его заголовку- это плохо можно перименовать поле, лучше создать некий класс с InternalName полей или с их ID.

     string Url = SPContext.Current.Web.Url;
                    using (SPSite spSite = new SPSite(Url))
                    {
                        using (SPWeb spWeb = spSite.OpenWeb())
                        {
    

    Лишний раз поднимаете тот же самый Web.
    item["ФИО"].ToString()
    

    Поле может вернуть null и вылетит Exception.
    В общем хороший обзор как создать не сложную веб часть для SharePoint, спасибо.


  1. Pavel7
    09.07.2015 22:30

    Разрабатываем внешний вид веб-части


    Почему бы не использовать библиотеку + обработчик загруженных файлов? Получаем атрибуты, drag and drop и историю файлов из коробки.

    SPList list = spWeb.Lists[«Сотрудники»];


    Списки надо получать по URL-у, не по Title

    var folderDZO = list.RootFolder.SubFolders[DropDownList1.SelectedItem.Text].ToString();
    SPFolder folder = spWeb.GetFolder(folderDZO);


    Зачем такие сложности, если list.RootFolder.SubFolders[DropDownList1.SelectedItem.Text] и есть нужная папка?

    SPListItemCollection itemColl = list.Items;

    if (ws.Cells[i, 1].Value.ToString() == item[«ФИО»].ToString())

    foreach (SPListItem item in list.Items)


    Не надо так сурово насиловать сиквел, используйте хотя бы CAML, хотя правильный вариант будет с SPWeb.ProcessBatchData.

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


    1. IvTikhonov Автор
      10.07.2015 11:04

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

      Получаем атрибуты
      Атрибуты чего? Файла? — у меня их нет. Атрибуты внутри файла? — я их и так получаю. Вы предлагаете тоже самое, но используя другой механизм. Однако, мне кажется, что веб-часть удобнее и прозрачнее для работы конечного пользователя.
      list.RootFolder.SubFolders[DropDownList1.SelectedItem.Text]
      Да, немножко перемудрил.
      Это не говоря уже о более глобальных вопросах
      А у меня и не было целью их затрагивать, стояла конкретная задача — загрузить массово элементы из Excel в список на портал. В общем случае, это же может быть, например, какая-либо продукция, о которой необходимо хранить информацию на портале и UPS нам не поможет, а сотрудники — частный случай.

      А так, спасибо за комментарий, замечания учту на будущее.


  1. gandjustas
    10.07.2015 15:35

    Ну и жесть.

    1) Вы не в курсе что в режиме быстрого редактирования списка можно копипастить данные из Excel?
    2) Вы не в курсе что к списку можно подключиться из Access и редактировать массово записи с фоновой синхронизацией?


    1. IvTikhonov Автор
      10.07.2015 18:59

      Статья про создание веб-части для массовой загрузки из Excel. От пользователя требуется выбрать Excel и нажать 1 из 3-х кнопок. Выбирает список и редактирует за него веб-часть. Так, что быстрое редактирование и Accsess — мимо кассы.


      1. gandjustas
        10.07.2015 20:02
        -1

        Если же пользователь берет access, то он сразу в access вбивает данные, а они попадают на сайт. Ему даже не нужно формировать файл Excel.

        Вы решаете проблему которой нет, способом который может принести больше проблем.

        Ведь мало сделать веб-часть, надо её на какую-то страницу положить (на какую?), потом надо как-то апгрейдить.
        Вы представляете себе даунтайм фермы от апгрейда вашей сборки с веб-частью? А если все начнут такие веб-части писать, то проблемы множатся.

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


  1. gandjustas
    10.07.2015 15:55

    По коду:

    protected void btnUpload_Click(object sender, EventArgs e)
    


    Логика в батонклике — это пять.

     string Url = SPContext.Current.Web.Url;
                    using (SPSite spSite = new SPSite(Url))
                    {
                        using (SPWeb spWeb = spSite.OpenWeb())
                        {
                            spWeb.AllowUnsafeUpdates = true;
    

    Чем SPContext.Current.Web не угодил?

    folders.DoesUserHavePermissions(oUser, SPBasePermissions.EditListItems);
    

    У вас элементы добавляются и удаляются, для этого есть отдельные разрешения. А вот как раз редактирование вы не используете.

    foreach (SPListItem item in list.Items)
    

    Не пишите так никогда, ВООБЩЕ НИКОГДА.

    item["ФИО"].ToString()
    

    Это упадет если ФИО пустое, надо Convert использовать.

      for (int i = 2; i < rowCount && ws.Cells[i, 1].Value != null; i++) //N строк
                {
                    if (IsPeople(ws.Cells[i, 1].Value.ToString(), list) == true) //Тут вытягиваем все элементы списка (M)
                    {
                       
                        foreach (SPListItem item in list.Items) // Тут еще раз вытягиваем все элементы списка (M)
                        {
                            if (ws.Cells[i, 1].Value.ToString() == item["ФИО"].ToString())
                            {
                                item.Delete();
                                break;
                            }
                        }
                        list.Update();  //Зачем???
                    }
    

    Итого у вас 2N вытягиваний элементов списка по M элементов + N апдейтов списков.
    Если в списке 100 элементов и в загруженном excel тоже 100, то вам надо будет считать 20 000, если даже очень повезет, то будет по 5 000 в секунду, то это уже 4 секунды. Не считая удалений и обработки list.Update().

    Возникает резонный вопрос — код вообще тестировался на реальных объемах? Или «works on my machine»?
    И что будете делать если в списке внезапно станет более 5000 элементов?

    Вы вообще зачем такой сырой код выкладывали?
    Разве так сложно поставить R# и reSP (http://www.subpointsolutions.com/resp/) и посмотреть ошибки перед тем, как выкладывать?


    1. IvTikhonov Автор
      10.07.2015 19:30

      У вас элементы добавляются и удаляются, для этого есть отдельные разрешения
      А разве разрешение Изменить не включает в себя Создание и Удаление? — Включает. Так, что все работает как и надо.
      Это упадет если ФИО пустое
      Кто же будет делать ключевое поле пустым?

      Про то, что мое решение супер оптимизировано я не писал. Но судя по вашим вычислениям, оно все же сэкономит время, 4 секунды, даже 1 минуту на 100 пользователей — неплохой результат, руками заполнять потребовалось бы значительно больше времени.
      Разве так сложно поставить R# и reSP
      Обязательно поставлю


      1. gandjustas
        10.07.2015 20:07

        А разве разрешение Изменить не включает в себя Создание и Удаление? — Включает. Так, что все работает как и надо.

        Откуда такая уверенность? Смотри МСДН, а там черным по английскому написано, что есть SPBasePermissions.AddListItems и SPBasePermissions.DeleteListItems.

        Кто же будет делать ключевое поле пустым?

        У вас нет никакой гарантии, что в Excel который попадает на вход есть данные в поле ФИО. Это означает что ФИО обязательно будет пустым когданибудь (относительно скоро).

        руками заполнять потребовалось бы значительно больше времени

        Скопировать из Excel в Quick View еще быстрее. Зачем веб-часть то?


        1. IvTikhonov Автор
          10.07.2015 22:05

          1) Уверенность из определения разрешения Изменить SharePoint, где написано, что оно действует на создание, изменение и удаление записей в списке. Плюс проверенно опытным путем. Если бы я не читал MSDN, я бы не ссылался на него. Я вас прекрасно понял, вы бы использовали то, а я это.

          2) Не надо путаться в собственных терминах. Вы писали про

          item[«ФИО»].ToString()
          я ответил, что в списке оно будет обязательное, а вы теперь уже пишите про атрибут в строке таблицы Excel. Проверка на пустое поле ФИО в Excel у меня имеется
          ws.Cells[i, 1].Value != null;

          3) Потому, что задача такая: пользователь мало знаком с SharePoint. Списки, поля, разрешения, редактирование — для него космос. Он хочет форму и пару кнопок — всё. Я выбрал веб-часть. Про свой метод вы можете рассказать отдельно. Я думаю много кому будет интересно)


          1. gandjustas
            10.07.2015 22:52

            Ты путаешь роли и разрешения.

            Даже если ты всегда ты всегда пишешь значение в поле, то никто не мешает пользователю этого не сделать. Вообще в шарике крайне сложно гарантировать что индексатор SPListItem не вернет null. Ты когда пару лет поработаешь с шариком плотно — все поймешь.

            Если пользователь с шариком мало знаком, то на кой Вообще данные в шарик грузить??? Что потом будет с этими данными?


  1. gandjustas
    10.07.2015 15:56

    Резюмируя скажу вот что:
    Вы сделали то, что не нужно, так еще и сделали некачественно.


  1. Mr_well
    16.07.2015 10:30

    Во многом соглашусь с критиками, особенно про for each.
    С другой стороны если это гайд для начинающих, то пойдет.
    От себя добавлю что пора завязывать с FTC, Микрософт не раз дал понять, что практически все можно сделать через CSOM/REST.