Sunday 17 July 2016

Отношения мастер-деталь в SharePoint 2013 между списками

Как построить связи между списками SharePoint в рамках отношений Один ко Многим, Много ко Многим и организовать между ними нечто подобие ссылочной целостности?
Наверняка этот вопрос задавали себе многие начинающие программисты SharePoin, так как стандартные средства этой великолепной системы управления сайтами не позволяют организовать полноценную ссылочную целостность между списками.

На самом деле построить каскадные отношения между списками несложно. Давайте рассмотрим небольшой пример на эту тему:

1. Создадим список сотрудников:
<?xml version="1.0" encoding="utf-8"?>
<List xmlns:ows="Microsoft SharePoint" Title="MFCEmployees" Name="MFCEmployees"
 Direction="$Resources:Direction;" Url="Lists/MFCEmployees" BaseType="0" 
xmlns="http://schemas.microsoft.com/sharepoint/" EnableContentTypes="TRUE">
  <MetaData>
       <ContentTypes>         
    <ContentType JSLink="~site/_layouts/15/webservice_001/
MyContent001/JS/MFCEmployees/MFCEmployees.js" 
ID="0x01005174e8c58336491db85ea917c7f65031" Name="Полное представление">
       <FieldRefs>                               
       <FieldRef ID="{fa564e0f-0c70-4ab9-b863-0177e6ddd247}" Name="Title" />
       <FieldRef ID="{ca828b4b-d9cf-4653-b4a5-2d44d3e9b9df}" Name="EmployeeName" />
       <FieldRef ID="{66cf8cfe-39b9-4123-bdaf-c36b7774dcd9}" Name="Birthdate" />
       <FieldRef ID="{254aabea-f690-4a88-b258-4fe87c61c612}" Name="Date_MFC_Employment" />
       <FieldRef ID="{163c8a49-88be-46e4-837c-57f3e0c1d96c}" Name="EmployeeStatus" />
       <FieldRef ID="{6d47a98d-c144-44ba-9912-339119c25681}" Name="Situation" />
       <FieldRef ID="{0655a999-cdc3-4c35-8b53-3bbd49e8192d}" Name="StatusEmployee" />
       <FieldRef ID="{6345b705-0bce-4e42-8b3e-0b3e3d37fe94}" Name="EndDateWork" />
       <FieldRef ID="{b5544a5a-5950-485e-8b3f-4286adbecb78}" Name="CompensatoryLeave" />
       <FieldRef ID="{234309e1-1823-4ad4-aba6-3c6390a378c6}" Name="Otdel" />
    </FieldRefs>
    </ContentType><ContentTypeRef ID="0x0120" /></ContentTypes>

    ФАЙЛ Schema.xml СОКРАЩЕН

  </MetaData>
</List>
Теперь создадим подчиненный список статусов сотрудников:
<?xml version="1.0" encoding="utf-8"?>
<List Id="{ca222b4b-d9cf-4653-b4a5-2d44d3e9b9df}" xmlns:ows="Microsoft SharePoint" 
Title="MFCEmployeesStatus" FolderCreation="FALSE" Direction="$Resources:Direction;"
 Url="Lists/MFCEmployeesStatus" BaseType="0" 
xmlns="http://schemas.microsoft.com/sharepoint/" EnableContentTypes="TRUE">
  <MetaData>
    <ContentTypes>        
      <ContentType ID="0x01005fec9402a88147e6937d2c0a3fb6303b" 
Name="Даты текущего состояния">
        <FieldRefs>                   
        <FieldRef ID="{4f243fe7-3f34-4e48-9c29-49f538bd4885}" Name="StatusEmployee" />
<FieldRef ID="{41790c3c-762d-4f2d-b570-939ab8281679}" Name="StartDateWork" />
<FieldRef ID="{e5da87f0-b02b-47f1-88d9-ce2cfc511ff1}" Name="EndDateWork" />
<FieldRef ID="{a50cd5f7-67ab-40d6-b9be-3ffcc6ff54e7}" Name="MFCEmployeesId" />
</FieldRefs>
      </ContentType><ContentTypeRef ID="0x01" /><ContentTypeRef ID="0x0120" />
</ContentTypes>

ФАЙЛ Schema.xml СОКРАЩЕН

  </MetaData>
</List>

Обратите внимание - у списка MFCEmployeesStatus объявлен столбец MFCEmployeesId. Это вторичный ключ списка MFCEmployees. По нему мы и будем строить отношения между списками по принципу Один ко Многим, где мастер - MFCEmployees, а деталь - MFCEmployeesStatus.

Теперь. когда у нас готовы списки нам необходимо создать механизм связывания двух списков по вторичному ключу. Для этого мы воспользуемся API SharePoint REST.
Без долгих объяснений прилагаю код соответствующих функций:
1. MFCEmployeesId.js:
SP.SOD.executeFunc("clienttemplates.js""SPClientTemplates"function () {
    SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
        Templates: {
            Fields: {
                //имя поля
                'MFCEmployeesId':
                {
                    NewForm: function (ctx) {
                        var qur = GetUrlKeyValue('MFCEmployeesId');
                        var val = 0;
                        if (typeof qur !== "undefined" && qur.length > 0)
                            val = qur;
                        else
                            val = 0;
                        ctx.CurrentFieldValue = val;
                        return SPFieldNumber_Edit(ctx);
                    }
                }
            }
        },
        OnPostRender: function (ctx) {
            //после загрузки
            var f = ctx.ListSchema.Field[0];
            if (f.Name == "MFCEmployeesId") {
                InputHiden(f, "Field",true);
            }
        },
        ListTemplateType: 10006
    });
});
Функция GetUrlKeyValue читает параметр запроса по имени MFCEmployeesId. 
Если находит значение, то подставляет его в поле вторичного ключа. В свою очередь функция 
InputHiden в событии OnPostRender скрывает поле от глаз пользователей.
Как видите, все очень просто. Самое главное:
1. Передать в строке запроса значение ключа сотрудника.
2. Перед сохранение статуса у сотрудника прочитать соответствующий параметр, где храниться
ключ сотрудника и сохранить его в соответствующем input-е.

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

Фильтр авторизации ASP.NET MVC >= 5

Начиная с ASP.NET MVC 4 стало возможным применять обновленный фильтр авторизации класс AuthorizationFilter c проверкой подлинности ролей.

Этот фильтр реализует тип IAuthorizationFilter и определяет необходимость выполнения метода действия (например, выполнение проверки подлинности или проверки свойств запроса) с точки зрения безопасности.
Классы AuthorizeAttribute и RequireHttpsAttribute являются примерами фильтров проверки подлинности. Фильтры проверки подлинности выполняются перед выполнением любых других фильтров.
Вот, немножко сплагиатил с официальной документации о фильтрах. Теперь по сути:

Лично я использовал в своих проектах указанный фильтр, в том числе и для проверки подлинности на основании определенных ролей. Это, впрочем, самое главное новшество фильтра авторизации, которое появилось с 4-ой версией MVC.

Я был удовлетворен работой класса и, особенно не вникал в суть его работы :) до тех пор, пока не воспользовался новой системой авторизации и профилей Microsoft ASP.NET Identity 2.0. Тогда выяснилось, что класс AuthorizeAttribute полноценно использовать нельзя. Например, для проверка ролей класс взаимодействует с интерфейсом IPrincipal, который реализует собственный, отличающийся от Identity 2.0, интерфейс. Вывод - сочинить собственный фильтр авторизации. Далее речь пойдет об этом :)

Итак, с начало код фильтра:

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using System.Web;
using System.Web.Mvc;
 
namespace avk_commerce.web.Filters
{
    public class RoleAuthorizationAttribute : ActionFilterAttributeIActionFilter
    {
        public string Roles { getset; }
        #region вызывается перед обращением к методу действия
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {                                 
            bool Match = false;
            var user = filterContext.HttpContext.User.Identity;
            //роли, переданные через параметр
            string[] _Roles = Roles.Split(',');
            if (user.IsAuthenticated)
            {
                var UserManager = 
     filterContext.HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
                int UserId = user.GetUserId<int>();
                foreach (var Role in _Roles)
                {
                    Match = UserManager.IsInRole(UserId, Role);                    
                }
                if (!Match)
                {
                    filterContext.Result = new ViewResult { ViewName = "AccessDenied" };
                }
            }
            else {
                filterContext.Result = new ViewResult { ViewName= "LoginNow" };
            }            
            base.OnActionExecuting(filterContext);
        }
        #endregion
    }
}
Как видим, класс переопределяет метод OnActionExecuting. Класс содержит свойство
Roles. Это список ролей через запятую. Мы в первую очередь проверяем прошел ли текущий 
пользователь авторизацию на сайте. Потом проверяем - соответствуют ли задекларированные
в методе контролера разрешенные роли, тем ролям, которые присвоены текущему пользователю.
Выводы сохраняем в локальной переменной Match. Далее в зависимости от значения в переменной 
Match принимаем решения. Как видите, все очень просто :)

Пример практического применение:

using System.Web.Mvc;
using avk_commerce.web.Filters;
using avk_commerce.web.Models;
 
namespace avk_commerce.web.Controllers.Admin
{
    [RoleAuthorization(Roles = CustomRoles.Admin)]
    [Culture]    
    public class AdminController : BaseController
    {
        #region Index
        public ActionResult Index()
        {
            var item = this.GetHomeData(null);
            return View(item);
        }
        #endregion
               
        #region Dispose
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                //base.db.Dispose();
            }
            base.Dispose(disposing);
        }
        #endregion
    }
}
 

Saturday 14 May 2016

Рассылка почтовых сообщений с помощью JavaScript REST API в SharePoint

Выполнить передачу почтового сообщения с помощью метода SendEmail на стороне клиента легко. В сети имеются масса примеров, о том как это сделать.
О проблемах исполнения этого метода тоже немало написано. Например частой ошибкой является - недействительный адрес электронной почты пользователя SharePoint. Даже, если достоверно известно, что пользователь имеет подтвержденный электронный адрес и у него достаточно высокий уровень доступа к материалам сайта, при отправке почты ему могут возникнуть проблемы. 
Лично я обнаружив, что нельзя довериться способу передачи почты с помощью методов sharepoint rest api, решил подстраховаться и написать в рамках собственного web service метод передачи почты пользователю SharePoint.
Конечно, если в вашей ферме SharePoint не работает собственное решение методов web service, то писать и развертывать его в ферме  только лишь для указанных целей, не стоит :). У меня же уже имелся в ферме собственный web service, в рамках которого выполнялись несколько десятков методов, поэтому добавить туда еще один метод - не проблема.

Итак, что я сделал:

sendEmailфункция javascript. она выполняет простую работу - передает почтовое сообщение пользователю (или группе пользователям). Если возникают проблемы, то функция обращается к моему методу на сервере (обратите внимание на функцию GetReturnAjax). Там, за "кулисами" работает метод SetItemSPSendEmailItem. Как правило с ним проблем не бывает :).

function sendEmail(from, to, body, subject, containerId, gridId, urlPanel, mparam) {        
    var urlTemplate = _spPageContextInfo.webAbsoluteUrl + 
"/_api/SP.Utilities.Utility.SendEmail";
    var container = $('#' + containerId);
    $.ajax({
        contentType: 'application/json',
        url: urlTemplate,
        type: "POST",
        data: JSON.stringify({
            'properties': {
                '__metadata': { 'type''SP.Utilities.EmailProperties' },
                'From': from,
                'To': { 'results': [to] },
                'Body': body,
                'Subject': subject
            }
        }),
        headers: {
            "Accept""application/json;odata=verbose",
            "content-type""application/json;odata=verbose",
            "X-RequestDigest": $("#__REQUESTDIGEST").val()
        },
        beforeSend: function () {
            if (container.length) {
                container.prepend($('<p />').addClass('loading-aj-dialog'))
            }
        },
        success: function (data) {
            container.find('p.loading-aj-dialog').remove();
            GetReturnAjax(urlPanel, JSON.stringify(mparam), false,
                function (data) {
                    container.find('p.loading-aj-dialog').remove();
                    alert(subject + " передано успешно.");
            }, containerId);
        },
        error: function (err) {
            container.find('p.loading-aj-dialog').remove();
            console.log(JSON.stringify(err));
            var mparam2 = { "subject": subject,"body": body, "to": to };
            //если ошибка, то пытаемся передать сообщение через собственную службу
            GetReturnAjax(urlPanel, JSON.stringify(mparam2), false,
                function (data) {
                    container.find('p.loading-aj-dialog').remove();
                    alert(subject + " передано успешно.");
                }, containerId);
        }
    });
}
Метод на сервере на тот случай, если на клиенте будут проблемы.        
        [WebMethod]
        [System.Web.Script.Services.ScriptMethod ( UseHttpGet = false, ResponseFormat = ResponseFormat.Json )]
        public m_message SetItemSPSendEmailItem ( string subject,
            string body, string to )
        {
            m_message retur = new m_message { title = "", type = 2 };
            string error = "";
            using ( SPSite site = new SPSite ( this.GetUrlSite ) )
            {
                using ( SPWeb web = site.OpenWeb ( ) )
                {
                    try
                    {                                                
                        if ( !string.IsNullOrEmpty ( subject ) && 
                             !string.IsNullOrEmpty ( body ) && 
                             !string.IsNullOrEmpty ( to ) )
                        {
                            SPUtility.SendEmail ( web, truefalse, to, subject, body );
                        }
                    }
                    catch ( Exception ex )
                    {
                        error = ex.ToString ( );
                        retur.title = error;
                    }
                    finally
                    {
                        if ( web != null )
                            web.Dispose ( );
                    }
                }
            }
            return retur;
        }
Ну, вот, на сегодня, пожалуй все.

Tuesday 26 April 2016

Подробней о технической стороне решения "Работа с проектами документов". Часть 1

Прошлый мой материал имеет краткое описание форм решения "Работа с проектами документов" для SharePoint 2013. Здесь же, как я и обещал ранее, будет описание технической стороны этого проекта.
Следует сразу же оговориться, что данный проект целесообразно было бы сделать в рамках приложения для SharePoint 2013. Здесь же следует описание проекта, который состоит из нескольких изолированный решений Sharepoint 2013 и решений для фермы Sharepoint 2013.

Изолированные решения:

  1. Библиотека документов "Работа с проектами документов"
  2. Настраиваемый список "Участники согласования документов"
  3. Настраиваемый список "mListSP" (Таблица связи)
  4. Список "Обсуждение проектов документов"
  5. Пользовательское действие (CustomAction) 

Решения для фермы

  1. Библиотека. Методы для выполнения на стороне сервера Веб сервисов (WCF and SOAP). Методы этого решения выполняют задачи, которые было сложно сделать в рамках существующих в sharepoint 2013 rest api. Именно сложно, а невозможно. Лично я рекомендую использовать в своих работах методы sharepoint 2013 rest api. Но, к сожалению, сам не всегда следую собственным рекомендациям :). 
  2. Библиотека. Методы для контроля обработчиков событий. Расширение класса SPItemEventReceiver. Методы решения контролируют события - создания, редактирования и удаления строк в библиотеках и списках.
  3. Библиотека. Методы для выполнения заданий по исследованию содержимого проекта. Расширение класса SPJobDefinition
Это первая часть материалов. Далее планируется раскрывать каждую техническую часть подробней. Там же будут выложены ссылки на исходный материал.

Организация процесса согласования проектов документов. Решение для SharePoint 2013.

Поставлена задача написать для Sharepoint 2013 решение которое будет организовывать процесс согласования проектов документов.
При этом, главным условием было наличие в решении двух видов согласования: 
  1. Параллельный
  2. Последовательный
Кроме этого решение должно позволять:
  1. Регулировать уровень доступа к проекту документа индивидуально для каждого документа.
  2. Назначать каждому проекту документа (далее по тексту - Проект) тип согласования индивидуально.
  3. Создавать для каждого Проекта свой индивидуальный список согласующих лиц и предусмотреть возможность для автора Проекта редактировать этот список на любом этапе процесса согласования. Доступ к редакторам списка должен иметь только инициатор процесса согласования. Все остальные пользователи сайта SharePoint, включая участников согласования и администраторов сайта могут видеть только не редактируемый список согласующих лиц.
  4. Предусмотреть систему оповещения авторов Проектов, а так же участников процесса согласования о любых значимых событиях процесса согласования по почте.
  5. Предусмотреть возможность обсуждения Проектов участниками согласования в отдельном решении SharePoint (далее по тексту - Решение по обсуждению Проектов). При этом Решение по обсуждению Проектов должно быть интегрировано в единую систему оповещения о событиях процесса согласования.
  6. Исключить возможность редактирования документов после согласования.  
В итоге получаем единое решение SharePoint 2013, состоящее из
  1. Модернизированная библиотека документов "Проекты документов". Библиотека будет содержать документы.
  2. Настраиваемый список "Список согласующих лиц". Список будет хранить участников согласования.
  3. Настраиваемый список "Связи участников согласования с проектами документов". Список будет хранить информацию о связях библиотеки документов со списком участников согласования. Между библиотекой документов и списком мы организуем отношения "Много ко Многим". Например у конкретного документа будет множество участников согласования, которые принимали по нему решение и наоборот - у одного участника согласования будет множество документов, по которым он принимал решение. 
  4. CustomAction. Для меню проекта.

Некоторые формы решения:

1. Редактор библиотеки документов:


2. Представление библиотеки документов


3. Редактор списка Участников согласования документов 


4. Представление участников согласования

5. Представление списка связей участников согласования документов и документов

6. Редактор связи библиотеки документов и списка участников согласования

7. Форма обсуждения проектов документов


8. Форма на финише согласования документов. Возможность распечатать список согласовавших проект документа

В следующих постах будут раскрыты детали проекта поэтапно. Следите за обновление материалов :)