понедельник, 28 января 2013 г.

Создание отношений между списками


Довольно популярная задача, возникающая в рамках практически любого решения на SharePoint  – создание отношений между родительским и дочерним списками. К примеру “Проект – Задачи”, “Район – Улицы”, “Начальник – Подчиненные” и т.д.
Самый очевидный способ связать элементы разных списков – использовать поле “Подстановка” (lookup). На примере отношения “Проект – Задачи”:
Вариант 1: находясь на странице создания задачи (tasks/newform.aspx), выбираем обычным способом родительский проект из списка, это не требует никаких вмешательств в штатный функционал. Способ очень неудобный тем, что при большом количестве проектов приходится искать их в выпадающем списке.
Вариант 2: на странице просмотра проекта (projects/dispform.aspx) размещаем ссылку/кнопку, по клику на которую открывается форма создания задачи (tasks/newform.aspx) с уже заполненным полем “Проект”
Код ссылки/кнопки, который нужно вставить на странице projects/dispform.aspx:

<a href="/../Lists/Tasks/NewForm.aspx?ActID={$Param1}&amp;Source=https%3A%2F%2F..Lists%2Fprojects%2FDispForm%2Easpx&#063;ID={$Param1}">Создать задачу</a>
При этом должен быть заведен параметр в веб-части, в которой мы размещаем ссылку. Заведён так, как показано на рисунке.

Из переменной в адресной строке браузера этот параметр попадёт в tasks/newform.aspx , а атрибут “Source=…” нужен для корректного возврата после сохранения элемента в нужный projects/dispform.aspx
Для того, чтобы значение отобразилось в поле “подстановка” формы tasks/newform.aspx, необходимо вставить на эту страницу код на языке javascript – см. пример в конце статьи.

Отображение связанных записей в DispForm родительского списка

Для того, чтобы связанные записи были видны в форме, делаем следующее.
1. Создаём веб-часть представления дочернего списка в DispForm родительского списка.

 
Поигравшись с вкладкой “конструктор” на Ribbon-панели, можно придать веб-части удобоваримый вид. Например, если известно, что в рамках одного проекта обычно генерируется-назначается 5-6 задач, то очень хорошо подходит вот такой вид:

2. Необходимо добавить фильтр на представление, без него мы будем видеть все элементы списка Задачи. а нам нужны только связанные с конкретным проектом. ID проекта содержится в адресной строке браузера. Как создать параметр, получающий значение из адресной строки – было показано на первом рисунке.
Однако, если мы зададим условие фильтрации “Проект = Param1”, условие не сработает. Дело в том, что фактически мы сравниваем “1#;<название проекта>” и “1” и такое условие не может быть true. Для того, чтобы у нас было корректное поле для сравнения с параметром, нужно в свойствах списка, в параметрах поля-подстановки активировать дополнительное поле Проект:ИД

и уже это поле Проект:ИД использовать в условии фильтрации

Как скрыть поля связи (“lookup”, оно же “подстановка”) из форм

Если стоит задача скрыть поле из всех форм: создания, просмотра и редактирования – самый простой способ сделать это через SPD:

или непосредственно в браузере, последовательно – активировать управление типами содержимого в параметрах списка, зайти в тип содержимого, перевести столбец в “скрытый”. убрать управление типами содержимого.
Если нужно скрыть поле только из определённых форм, можно использовать утилиту SharePoint Manager
Она позволяет менять свойства SPField - ShowInEditForm, ShowInDisplayForm, ShowInNewForm. Лежит тут http://spm.codeplex.com/
Запускается на сервере с Sharepoint.

Можно отредактировать форму по умолчанию в дизайнере 2010 :
http://office.microsoft.com/ru-ru/sharepoint-designer-help/HA010378258.aspx
И, наконец, можно скрыть ненужные поля (или даже отобразить дополнительные) с помощью jquery, уже на стороне клиента, в браузере.


Пример установки поля “подстановка” в значение, полученное из адресной строки браузера и последующего скрытия строки с полем “подстановка”:

<script language="javascript" src="../frms/jquery-1.8.2.js" type="text/javascript"></script>
<script language="javascript" src="../frms/jquery.SPServices-0.7.2.js" type="text/javascript"></script>
<script type="text/javascript">
    // This javascript sets the default value of a lookup field identified
    // by <<FIELD DISPLAY NAME>> to the value stored in the querysting variable
    // identified by <<QUERYSTRING VARIABLE NAME>>
    // Customize this javascript by replacing <<FIELD DISPLAY NAME>> and
    // <<QUERYSTRING VARIABLE NAME>> with appropriate values.
    // Then just paste it into NewForm.aspx inside PlaceHolderMain
  
    _spBodyOnLoadFunctionNames.push("fillDefaultValues");
    function fillDefaultValues() {
      var qs = location.search.substring(1, location.search.length);
      var args = qs.split("&");
      var vals = new Object();
      for (var i=0; i < args.length; i++) {
        var nameVal = args[i].split("=");
        var temp = unescape(nameVal[1]).split('+');
        nameVal[1] = temp.join(' ');
        vals[nameVal[0]] = nameVal[1];
      }
      setLookupFromFieldName("Проект", vals["ID"]);
      $('#ctl00_m_g_e74ceaaa_2dcf_4cef_af99_40f05b6563c0_ff20_1_ctl00_ctl01').closest('tr').hide();   //   JQuery
    }
   
    function setLookupFromFieldName(fieldName, value) {
      if (value == undefined) return;
      var theSelect = getTagFromIdentifierAndTitle("select","Lookup",fieldName);
    // if theSelect is null, it means that the target list has more than
    // 20 items, and the Lookup is being rendered with an input element
      if (theSelect == null) {
        var theInput = getTagFromIdentifierAndTitle("input","",fieldName);
        ShowDropdown(theInput.id); //this function is provided by SharePoint
        var opt=document.getElementById(theInput.opt);
        setSelectedOption(opt, value);
        OptLoseFocus(opt); //this function is provided by SharePoint
      } else {
        setSelectedOption(theSelect, value);
      }
    }

    function setSelectedOption(select, value) {
      var opts = select.options;
      var l = opts.length;
      if (select == null) return;
      for (var i=0; i < l; i++) {
        if (opts[i].value == value) {
          select.selectedIndex = i;
          return true;
         }
       }
       return false;
     }
     
    function getTagFromIdentifierAndTitle(tagName, identifier, title) {
        var len = identifier.length;
        var tags = document.getElementsByTagName(tagName);
        for (var i=0; i < tags.length; i++) {
          var tempString = tags[i].id;
          if (tags[i].title == title && (identifier == "" || tempString.indexOf(identifier) == tempString.length - len)) {
            return tags[i];
          }
        }
        return null;
    }
    </script>

Sharepoint–разное накопившееся


  • Отсутствует в меню Действия узла -Изменить страницу : ?toolpaneview=2
  • Активности WorkFlow:
  1. Если возникла проблема, что в дизайнере активности есть, но не добавляются...

    Решение проблемы:
    --- ЦИТАТА----
    I found the problem. Step 2 of the install guide states to update the web.config file (usually c:\inetpub\wwwroot\wss\VirtualDirectories\80). I modified this file, but it was not the correct web.config file. I found a blog (http://blogs.msdn.com/sharepointdesigner/archive/2007/03/15/adding-activities-to-spd.aspx) that gives more detailed instructions on how to update the correct web.config file. I recommend updating the install guide with the following instructions for updating the web.config file.
    1. Open the IIS console.
    2. Find the web site that is your SharePoint web under the "Web sites" node and select it.
    3. Right click and choose "Open".
    4. In the folder window, open the web.config file.
    5. In web.config, browse to the bottom section and look for <System.Workflow.ComponentModel.WorkflowCompiler>. Inside that tag there should be an <authorizedTypes> section and an entry for each set of types. Add the following line of XML:
    <authorizedType Assembly="DP.Sharepoint.Workflow, Version=1.0.0.0, Culture=neutral, PublicKeyToken=0298457208daed83" Namespace="DP.Sharepoint.Workflow" TypeName="*" Authorized="True" />
    --- ЦИТАТА----
возможен другой токен сборки, для проверки выполняем: <путь>\gacutil /l DP.Sharepoint.Workflow
  1. Кроме того, бывает что активности даже не появляются в дизайнере.
В этом случае необходимо сделать следующее: If you have a non-english installation of sharepoint, you need to copy <file>.ACTIONS from [SP HiVE]\TEMPLATE\1033\Workflow to [SP HiVE]\TEMPLATE\1049\Workflow
  1. Кроме того, иногда РП просто отказывается открываться:
Помогал запуск Дизайнера из-под системной учетки
  • Включить подробный вывод ошибок:
в web.config нужного сайта изменить <customErrors mode="On" /> на <customErrors mode="Off" />
<SafeMode MaxControls="200" CallStack="false"... на <SafeMode MaxControls="200" CallStack="true"...

для подробного вывода ошибок рабочего процесса: http://blogs.technet.com/b/victorbutuza/archive/2009/02/26/how-to-enable-workflow-tracing-debug.aspx
вкратце:
в web.config нужного сайта включить:
после </System.Workflow.ComponentModel.WorkflowCompiler>
   <system.diagnostics>
      <switches>
          <add name="System.Workflow LogToFile" value="1" />
          <add name="System.Workflow.Runtime" value="All" />
          <add name="System.Workflow.Runtime.Hosting" value="All" />
          <add name="System.Workflow.Runtime.Tracking" value="All" />
          <add name="System.Workflow.Activities" value="All" />
          <add name="System.Workflow.Activities.Rules" value="All" />     
      </switches>
  </system.diagnostics>

и искать результат в c:\windows\system32\inetsrv, workflowtrace.log

Повышение общего уровня логирования
stsadm.exe -o setlogginglevel -category "General" -tracelevel Verbose
stsadm.exe -o setlogginglevel -category "Workflow Infrastructure" -tracelevel Verbose
stsadm.exe -o setlogginglevel -category "Workflow Features" -tracelevel Verbose
Понижение уровня (чтобы не загадить логами весь диск)
stsadm.exe -o setlogginglevel -category "General" -tracelevel Monitorable
stsadm.exe -o setlogginglevel -category "Workflow Infrastructure" -tracelevel Monitorable
stsadm.exe -o setlogginglevel -category "Workflow Features" -tracelevel Monitorable
Чтобы изменения вступили в силу, нужно перезапустить службу "Windows SharePoint Services Tracing service"

Посмотреть куда пишутся логи
You can check your current log folder path at the
Central Administration\Operations\(below the Logging and Reporting section)Diagnostic logging\in the Trace log section in the Path editbox.)
  • В 2010 нет ссылки на "настройку групп для узла". Но её можно открыть по ссылке "узел/_layouts/permsetup.aspx"
  • Удалить лишнюю информацию из lookup полей перенесённых в Excel:
    нужно создать макрос с именем Auto_Open, это позволит ему выполняться при открытии документа.
    Код макроса следующий:
        Cells.Replace What:="*;#", Replacement:="", LookAt:=xlPart, SearchOrder _
            :=xlByRows, MatchCase:=False, SearchFormat:=False, ReplaceFormat:=False
           Application.OnTime Now + TimeValue("00:00:06"), "Auto_Open"
  • Как раскрасить календарь: http://www.planetwilson.co.uk/sharepoint-2013-colour-calendar/ 
  • Как раскрасить список:
    <script> 
    var colors = new Array(
                      new Array( "принято с замечаниями", "#FF0000", "#000000" ),
                      new Array( "принято"              , "#00FF00", "#0000FF" ),
                      new Array( "не принято"           , "#0000FF", "#000000" )
                                  );
    function getObjInnerText (obj) 

          return (obj.innerText) ? obj.innerText : (obj.textContent) ? obj.textContent : ""; 
    }  
    function runOnLoad() {
          nodes = document.getElementsByTagName("td"); 
            for( var i = 0; i < nodes.length; i++ ) 
            { 
                var itext = getObjInnerText( nodes[i] )
                for( i1 in colors ) {
                      if( colors[i1][0] == itext ) {
                            nodes[i].style.background = colors[i1][1]
                            nodes[i].style.color      = colors[i1][2]
                      }
                }
             }
    }
  • function ExpGroupRenderData( htmlToRender, groupName, isLoaded ) {      var tbody = document.getElementById( "tbod" + groupName + "_" )      var wrapDiv = document.createElement( "DIV" )
          wrapDiv.innerHTML = "<TABLE><TBODY id=\"tbod" + groupName + "_\" isLoaded=\"" + isLoaded + "\">" + htmlToRender + "</TBODY></TABLE>";     
          tbody.parentNode.replaceChild( wrapDiv.firstChild.firstChild, tbody );
          runOnLoad();
    }
    //_spBodyOnLoadFunctionNames.push("runOnLoad");
    </script>
  • РП в дизайнере при назначении задачи пользователю не дает подставить в качестве адресата поле с типом "Пользователь", если там разрешен множественный выбор. Но достаточно убрать его, потом поставить его в РП, а потом вернуть как было и множественный выбор работает ) Задачи уходят нескольким людям...Активность называется "Назначить форму группе", например. В этом случае РП ожидает закрытия всех задач обычно, он это если указывать адресатов по одному, а не подсовывая в качестве списка адресатов поле со множственным выбором. Хтя скорее всего корректно отработает. Если имеем дело с активностью "получить данные от пользователя", то РП может пойти дальше и после закрытия первой задачи.
    •Если рабочие процессы ведут себя странно, а именно: при создании/изменения элемента висят в состоянии "Запуск", отрабатывая нормально только в случае запуска вручную - предположительно помогает очистка корзины семейства веб-узлов. Кроме очистки корзины было сделано:
  • iisreset
    Attempting stop...
    Internet services successfully stopped
    Attempting start...
    Internet services successfully restarted
    stsadm -o getproperty -pn job-workflow -url http://…
    <Property Exist="No" />
    stsadm -o setproperty -pn job-workflow -pv "Every 1 minutes between 0 and 59" -url http://…
    Операция успешно завершена.
    stsadm -o getproperty -pn workflow-eventdelivery-throttle
    <Property Exist="Yes" Value="15" />
    stsadm -o setproperty -pn workflow-eventdelivery-throttle -pv "20"
    Операция успешно завершена.
    •Вот тут: /_layouts/wrkmng.aspx можно поглядеть выполняющиеся РП.
    •Рабочие процессы на элементы, добавляющиеся по email:
    Вкратце, проблема связана с тем что эти записи добавляются из-под Системной Учетной Записи - http://support.microsoft.com/kb/953289/ru
    фикс лечит проблему
    •Привязка РП к опросу не прокатит. Тут об этом написано Привязка РП к типу "Опрос"
  • В 2010ом всякий рабочий процесс ведёт свой журнал в "Журнале рабочего процесса". Там всегда можно посмотреть хронологию выполнения экземпляров РП.
  • На список Task (Задачи) можно повесить РП, но нельзя его просмотреть/запустить руками из менюшек. Для просмотра можно создать представление, в которое включить поля - рабочие процессы. Для запуска руками - воспользоваться ссылкой /_layouts/IniWrkflIP.aspx?List={ae2420f3-b6e8-49b6-a458-c071847121b7}&ID=814&TemplateID={25b34c92-7c30-433d-8e76-d215c8142a89} айдишники списка достать из того же "Журнал рабочего процесса", в нем TemplateID это "Идентификатор сопоставления рабочего процесса"
  • По написанию рабочих процессов. http://office.microsoft.com/ru-ru/sharepoint-designer-help/CH010373544.aspx

воскресенье, 27 января 2013 г.

Завершение экземпляров WorkFlow

Скрипт завершает все выполняющиеся экземпляры РП и при необходимости выполняет перезапуск.
Иногда он выполняет рестарт даже если не указывать -restart, во избежание этого нужно закомментарить выделенный красным фрагмент.
 
Может также удалять старые выполняющиеся экземпляры РП. Отслеживать выполнение можно тут: _layouts/wrksetng.aspx?List=%7B8E4A6033%2DFD1B%2D4BBA%2DAE60%2D6D2869D3A28B%7D (ID списка ваш)
 
Пример использования:
Видим картинку, хотим удалить старые экземпляры процесса "Оповещения (предыдущая версия: 23.09.2011 14:01:26)"
 
wf

 
Запускаем ./Cancel-SPWorkflow -site "https://skr.mts.ru/it/d/dpp/its" -List "Задачи" -Workflow "Оповещения (предыдущая версия: 23.09.2011 14:01:26)"
 
Дожидаемся окончания выполнения скрипта и видим:
 
wf2

 
 
##################################################################################
#
#
#  Script name: Cancel-SPWorkflow.ps1
#
#  Author:       Mattias.Karlsson@zipper.se
#  Homepage:     www.mysharepointofview.com
#
#
##################################################################################

param([string]$site, [string]$List, [string]$Workflowname, [switch]$Restart, [switch]$IncludeRunning, [switch]$help)
[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint")
function GetHelp() {
$HelpText = @"
DESCRIPTION:
NAME: Cancel-SPWorkflow
Cancel SharePoint Workflows and restart them if wanted
PARAMETERS:
-Site    Url to the SharePoint Site
-List    Name of the list
-Workflowname  Name of the workflow
-Restart  Optional if you want to restart the workflows
-IncludeRunning  Optional if you want to include running workflows
-help     Displays the help topic
SYNTAX:
Cancel-SPWorkflow -help
Displays the help topic for the script
Cancel-SPWorkflow -site http://yourSPSite -list Listname -Workflowname YourWorkflow
Terminates all workflows with the specific workflow name
Cancel-SPWorkflow -site http://yourSPSite -list Listname -Workflowname YourWorkflow -Includerunning
Terminates all workflows with the specific workflow name and include running workflows
Cancel-SPWorkflow -site http://yourSPSite -list Listname -Workflowname YourWorkflow -restart
Terminates all workflows with the specific workflow name and then restart the workflow for all items
"@
$HelpText
}
function Get-SPSite([string]$site) {
New-Object Microsoft.SharePoint.SPSite($site)
}
function Get-SPWeb([string]$site) {
$SPSite = Get-SPSite $site
$SPSite.OpenWeb()
}
function Get-SPList([string]$site, [string]$List){
$OpenWeb = Get-SPWeb $site
$OpenWeb.Lists[$List]
}
function CancelWF($site, $List, $Workflowname, [switch]$Restart, [switch]$IncludeRunning) {
$SPList = Get-SPList $site $List
    Write-host $SPList.WorkflowAssociations
        if(($SPList.WorkflowAssociations | where {$_.Name -eq $Workflowname}) -gt $null){
        TerminateWF $site $List $Workflowname $Restart $IncludeRunning
    }
    else{
            Write-host "The Specifed Workflow does not exist in the list"
    }
}       
function StartWF($Site, $List, $Workflowname){
    $SPList = Get-SPList $site $List
    $WFAssociation = $SPList.WorkflowAssociations | where {$_.name -eq $Workflowname}
    foreach($item in $SPList.items) {
        $StartWorkflow = $SPList.Parentweb.site.workflowmanager.startworkflow($item, $WFAssociation, $WFAssociation.AssociationData)
    }
}
function TerminateWF($Site, $List, $Workflowname, $Restart, $IncludeRunning) {
    $SPList = Get-SPList $site $List
    foreach($item in $SPList.items) {
  if($item.Workflows.count -gt 0){
   foreach($Workflow in $item.workflows){
                if(($SPList.WorkflowAssociations | where {$_.id -eq $Workflow.AssociationId}).name -eq $Workflowname){
                    if($IncludeRunning){
         [Microsoft.SharePoint.Workflow.SPWorkflowManager]::CancelWorkflow($Workflow)
        }
                    else{
                        if(($Workflow.Internalstate -ne "Running") -and ($Workflow.Internalstate -ne "Completed") -and ($Workflow.Internalstate -ne "Cancelled")) {
[Microsoft.SharePoint.Workflow.SPWorkflowManager]::CancelWorkflow($Workflow)
                            } 
                      }
      }
   }
  }
}
    if($Restart){
        StartWF $Site $List $Workflowname
   }
   Write-host "Operation completed successfully" $TerminatedWFs
}
if($help) { GetHelp; Continue }
if($Site -AND $List -AND $Workflowname) { CancelWF -Site $Site -List $List -Workflowname $Workflowname -Restart $Restart -IncludeRunning $IncludeRunning}
else{ GetHelp; Continue}

“Отдел Х” захотел себе “сайт”. Создать для них веб приложение или семейство узлов?

Однозначного ответа нет, но вот такая табличка возможно поможет склониться к правильному варианту:

 

Веб-приложение

Семейство сайтов

Сайт внутри семейства

Возможность использовать для хранения контента отдельную БД

+

+

-

Возможность иметь различные СУС-КПЭ (доступность, восстановление, частота резервного копирования) в зависимости от контента

+

+

-

Возможность использовать отдельный пул приложений IIS

+

-

-

Возможность использовать уникальное доменное имя

+

-

-

Наличие собственных настроек/политик (группы доступа, корзина, др.)

+

+

-

Возможность настройки уникальных параметров веб-приложения (например тип аутентификации)

+

-

-

Возможность создания логического разделения контента сайтов

-

+

+

Возможность совместного использования шаблонов и навигации

-

+

+

Независимое развертывание решений

+

-

-

 

 

 

 

Так же стоит отметить, что в случае "разделения" узлов на приложения или семейства улучшается управляемость вследствие сокращения количества групп SharePoint и увеличения количества администраторов (у приложения или семейства узлов один администратор, соответственно при увеличении числа семейств в каждом - свой).

Sharepoint – частный случай решения “проблемы больших списков”

Пользователи Sharepoint очень любят наполнять списки данными. Часто это приводит вот к такой картине:

pic1

Пороговое значение списка 20 тыс. установлено в Администраторской консоли, обычно оно ещё меньше – всего 5 тыс. элементов.

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

В целом такое ограничение вполне оправдано. Список – это не таблица в БД, из него не выбрать данные select’ом, и необходимо осознанно подходить к наполнению списка данными, настройке представлений на списке. При бесконтрольном добавлении данных список становится неуправляемым, пользователям становится сложно искать в нём необходимые данные.

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

Первое: необходимо отключить для отдельно взятого списка ограничение по порогу. Для всех остальных списков это ограничение продолжит работать, и администратору не придётся волноваться, что вышедшие из-под контроля пользователя за неделю заполнят всё имеющееся дисковое пространство на SQL-сервере Sharepoint.
Отключить ограничение можно установкой свойства isThrottled в “false”. Для этого нужно под учёткой Администратора фермы зайти на сервер, запустить командную консоль SharePoint и выполнить:
$list.EnableThrottling = $false
$list.Update()

После этого для списка будут доступны все возможности фильтрации, как если бы превышения порога не было. Впрочем, предупреждение о превышении порога в меню “Параметры списка” останется.

Второе: нужно настроить представления для комфортной работы пользователей. Если нам известно, что в основном определённые группы пользователей работают с определёнными группами записей, нужно создать для них фильтрованные представления. Например, если в нашем списке есть поле
“Регион”, можно создать представления под названием “Все записи – Магадан”, “Все записи – Колыма” и т.д., отфильтрованные по полю “Регион”.

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

pic2

Третье и самое главное: штатный поиск Sharepoint не умеет искать элементы по части слова или маске. Так, например, если у нас есть элементы с названиями “первый”, “второй”, “третий”, то поиск “перв” или “перв*” ничего не даст. Однако, как показывает практика, это самый необходимый тип поиска с точки зрения пользователя, “затерявшегося” в большом списке. Такой поиск по одному полю достаточно просто реализуется с использованием пары веб-частей и Sharepoint Designer.

Создадим представление. Например, на основе существующего “Все записи – Магадан”. Назовём его “Магадан – фильтр записей”, и разместим в верхней части страницы веб-часть под названием “Фильтр текста”

Далее выделяем веб-часть с данными списка и добавляем параметр для веб-части с произвольным именем:

pic0

Затем кликаем на “Добавить подключение”, и соединяем веб-части, как показано на рисунках:

pic3

pic4pic5

И добавляем фильтр

pic6

После этого любой текст, введённый в фильтр, после нажатия “Ввод” ограничит количество записей.

вторник, 16 октября 2012 г.

Автоматизируем бизнес-процесс на SharePoint подручными средствами

 

Введение

Здравствуйте!
Хочу поделиться опытом автоматизации отдельно взятого бизнес-процесса на платформе Sharepoint 2010.
Думаю, эта статья будет интересна в первую очередь тем пользователям, кто по какой-то причине не хочет писать код в Visual Studio. Она о том, как можно обойти ограничения штатного функционала SharePoint, используя различные подручные решения.

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

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

Механизм у всех подобных систем примерно одинаков: в общем случае необходим список для хранения основных сущностей: акция, идея, заказ, проблема и т.д.  Далее - необходим список задач для формирования поручений. Кроме того, наверняка понадобится некоторое количество списков-справочников, например статусы, роли, регионы, филиалы и т.д.

У платформы есть все необходимое для этого: возможность быстро строить списки для основных данных, назначаемых задач и справочников, уже синхронизированные с АД пользователи, готовый механизм раздачи доступов и многое другое. Однако для того чтобы полностью автоматизировать процесс и обойтись без координатора, чьей задачей будет назначать задачи-поручения, обрабатывать ответы и переводить нашу сущность  из статуса в статус - для этого как минимум необходимо задействовать рабочие процессы - workflow. С них и начнём.

Постановка задачи

Мы рассмотрим частный случай бизнес-процесса - назовём его "Заказы на регламентные процедуры"
Вкратце: есть список готовых общепринятых регламентных процедур - Регламентов. К прямому созданию, изменению или удалению элементов этого списка процедур никто не имеет доступа, однако любой пользователь может предложить создание, изменение или удаление процедуры.
Для того, чтобы это произошло, он создает т.н Заказ. Заказ проходит определенную последовательность шагов-статусов, на каждом из которых определенным ролям назначаются поручения. От того, с каким решением будет закрыто поручение, зависит в какой статус перейдет Заказ. Кроме этого, последовательность шагов индивидуальна для заказов на создание, изменение и удаление, а также зависит от дополнительного признака экспресс, который позволяет пройти Заказу по упрощенной схеме. Таким образом, на каждом шаге то, куда пойдет Заказ, зависит от трех условий: от его типа, от наличия признака экспресс и от решения пользователя.
Когда Заказ пройдёт цепочку согласований и дополнений, автоматически создастся/изменится/удалится соответствующий Регламент.

Общая архитектура решения

Основные списки, созданные для решения задачи:

  • Регламенты
  • Заказы
  • Задачи
  • …+ Различные справочники

Схема процесса:

Capture

Автоматизация процесса

На скриншоте ниже похожая схема, выполненная с помощью Visio

Capture1

Её можно экспортировать в MS SharePoint Designer – автоматически будет создан рабочий процесс с условиями “если… то…” и задачками, но даже эта упрощённая схема выглядит весьма громоздкой. Особенно если учесть то, что практически на каждом шаге заказа надо выполнить дополнительные активности – например, отправить письмо, в некоторых шагах требуется внести изменения в другие списки. Это делает линейный рабочий процесс (а SP Designer позволяет делать только линейные) сложноприменимым к нашей ситуации.

Для решения этой задачи можно использовать вот такую комбинацию из двух рабочих процессов, назовём её “кольцо”:

Capture2

Рабочий процесс “кольцо” с успехом заменяет линейный workflow.
Идея незамысловатая - у нас уже есть 2 списка, список заказов и список задач. На список заказов мы навешиваем несложный рабочий процесс, который смотрит на текущий статус Заказа и в зависимости от этого создаёт ту или иную задачу. На список задач мы навешиваем ещё более несложный рабочий процесс, который при завершении задачи обновляет статус в связанном Заказе, тем самым инициируя повторное выполнение рабочего процесса на Заказе.

Для того, чтобы не описывать все шаги и переходы непосредственно в теле workflow, создадим ещё несколько списков-справочников:

  • tasklist – список всех возможных задач, назначаемых пользователям при переходе на очередной шаг
  • logicoftransitions – логика переходов. Список, состоящий из полей
    • taskfrom – задача, из которой надо выполнить переход
    • condition1 – условие 1 (в нашем случае, это решение Да или Нет, принятое пользователем по предыдущей задаче)
    • condition2 – условие 2 (в нашем случае, наличие или отсутствие признака “экспресс”)
    • condition3 – условие 3 (в нашем случае, тип заказа – создание РП, изменение РП, удаление РП)
    • taskto – задача, в которую надо перевести процесс с учётом предыдущих условий
    • sendemail - поле  Да/Нет, посылать ли письмо при выполнении условия
    • emailsubj – тема письма
    • emailtext – текст письма
    • emailsendto – адресаты письма

Этих списков достаточно для автоматизации процесса. Разумеется, если мы оставим поле taskfrom пустым – это будет означать, что задача первая и не имеет предшествующих задач. Пустое поле taskto означает отсутствие следующего шага – задачи, и если нужно сделать что-то помимо отправки письма, эту ситуацию нужно дополнительно обработать в теле workflow.

Итоговый workflow на списке Заказы получается достаточно коротким – судите сами (картинка разбита на 2 половины):

Capture3_1Capture3_2

workflow для списка Задачи и вовсе содержит только одну активность

Capture4

Есть ещё один нюанс. В рабочем процессе нельзя просто так взять и выбрать элемент из списка по совпадению нескольких полей. Одно совпадающее поле - это пожалуйста, но не больше. Однако проблема решается элегантным способом. Нам нужно навесить небольшой рабочий процесс или обработчик или даже просто вычисляемое поле на список logicoftransitions - его задачей будет просто заполнить скрытое текстовое поле, которое мы и станем использовать для сравнения. Так, например, если шаг зависит от состояния 'имя задачи',решение'да/нет','признак экспресс' и 'тип заказа' - то соответственно строка будет выглядеть например так 'согласование руководителя-да-да-создание заказа'. В основном рабочем процессе есть переменная conditionstring, ее и станем сравнивать с нашим полем.

Плюсы такого решения:
+ достаточно один раз сделать узел-болванку, и превращение его в рабочий узел для автоматизации конкретного бизнес-процесса в последующем займёт совсем немного времени
+ очень легко менять траекторию движения заказа - не нужно менять сам рабочий процесс, достаточно изменить справочные значения в tasklist и logicoftransitions.
+ с условием небольшой доработки можно реализовать возможность для  пользователя перескакивать в произвольный шаг, или даже назначать отдельным заказам персональную траекторию.

Минусы:
- нельзя поглядеть на красивой диаграмке, где в данный момент находится наш заказ, как это можно сделать, если использовать штатный линейный workflow

Для того, чтобы хотя бы частично нивелировать этот недостаток, можно отображать в DispForm - форме просмотра Заказа все связанные задачи. Это даст наглядную картину того, на каком шаге в данный момент находится заказ и от какого пользователя(ей) ожидаются данные. Делается это просто: мы хотим, чтобы задачи были привязаны к заказу - достаточно присвоить полю-подстановке "Заказ” значение ИД текущего элемента. Несложно также отобразить их в форме просмотра Заказа, нужно просто на DispForm для списка “Заказы” добавить вебчасть - форму просмотра с фильтром, который отображает элементы списка задач со значением поля-подстановки:ИД равным параметру взятому из строки запроса браузера.

Создание Заказа, привязанного к Регламенту. Автозаполнение и скрытие полей.

Как следует из ТЗ, необходимо иметь возможность создавать Заказы на изменение и удаление Регламента. Для этого, очевидно, Заказ должен быть привязан к Регламенту.
Пожеланием заказчика системы было автозаполнение полей в этих вновь создаваемых заказах – поля из Регламента должны были переноситься в Заказ.
Одним из испробованных способов было создание готового Заказа рабочим процессом, и отправка пользователю ссылки на получившийся Заказ. Однако эта практика показала свою неэффективность – пользователю не хочется отвлекаться на почту, ему хочется видеть создаваемый Заказ на экране с возможностью внесения изменений ещё до его создания.
Было придумано вот такое решение. Используем всё тот же Sharepoint Designer. Создаётся новая форма для просмотра элемента списка Регламенты, причём создавать её желательно клонированием существующего файла DispForm.aspx
Затем под формой, отображающей все поля, создаём форму создания нового элемента списка Заказы. Таким образом, мы имеем одну форму, в верхней части которой находятся поля в режиме просмотра, в нижней - в режиме заполнения.
Для заполнения используем старый добрый джаваскрипт. Код скрипта можно разместить на нашей *.aspx - странице после строки:
 
<asp:Content ContentPlaceHolderId="PlaceHolderMain" runat="server">

или в новой веб-части Редактор контента, размещенной под всеми остальными веб-частями.

Шаг номер один: взять значение из полей верхней веб-части. Нюанс в том, что у этих полей совпадающие id. Так, например, если у нас несколько полей с типом “Однострочный текст” - у всех у них будет id “SPFieldText” Это конечно же затрудняет поиск нужного значения, и вот один из способов решения этой задачи:
   //Получаем содержимое всех тэгов TD
   var inputTags = document.getElementsByTagName('TD');
  
   //Создаём массивы для хранения содержимого различных типов полей
   al = new Array();
   nm = new Array();
   lp = new Array();
   bl = new Array();
   ch = new Array();
  
   //Заполняем массивы значениями
   for(var i=0;i<inputTags.length;i++)
   if(inputTags[i].id == 'SPFieldNumber')
       al.push(inputTags[i]);
   else if(inputTags[i].id == 'SPFieldText')
       nm.push(inputTags[i]);
   else if(inputTags[i].id == 'SPFieldLookup')
       lp.push(inputTags[i]);
   else if(inputTags[i].id == 'SPFieldBoolean')
       bl.push(inputTags[i]);
   else if(inputTags[i].id == 'SPFieldChoice')
       ch.push(inputTags[i]);

Теперь у нас в элементе массива al[0] - содержимое первого тэга с id “SPFieldNumber”, в al[1] - второго и т.д...

Шаг номер два: записать полученные значения в соответствующие поля. В форме редактирования, в отличие от формы просмотра, все поля имеют уникальный id, (кстати, его можно посмотреть в IE кликнув F12, затем выбрав нужное поле курсором - поиском):

Capture5

Для каждого типа поля требуется свой индивидуальный подход, например для полей с типом “Однострочный текст” и “Выбор” содержимое тега выглядит вот так:

<td valign="top" class="ms-formbody" width="450px" id="SPFieldText">
        <!-- FieldName="Название"
             FieldInternalName="Title"
             FieldType="SPFieldText"
          -->
            Новая РП
        </td>
и значения из содержимого тега можно взять следующим образом:

//запишем в переменную содержимое тега
nm0=nm[0].innerHTML;

//зададим переменную для определения закрывающего символа комментария, после которого и находится требуемый нами текст
end_comment = '-->';

//возьмем только нужные символы
end_pos = nm0.indexOf( end_comment );
nm0=nm0.substr( end_pos + end_comment.length );
nm0 = nm0.replace("&nbsp;","");

//запишем текст в соответствующее поле
document.getElementById("ctl00_m_g_41362970_e9f4_429e_b60f_f62d1e52e332_ff11_new_ctl00_ctl00_TextField").value = nm0

Для поля с типом “Подстановка”, содержимое тега выглядит так

<td valign="top" class="ms-formbody" width="450px" id="SPFieldLookup">
        <!-- FieldName="МР"
             FieldInternalName="_x0418__x043d__x0441__x0442__x04"
             FieldType="SPFieldLookup"
          -->
            <a href="/it/d/dpp/nmc/Lists/InstanceForis/DispForm.aspx?ID=6&RootFolder=*">МР Сибирь​</a>
        </td>
нам необходимо только значение ID, получить его можно, используя регулярные выражения, вот так:

lp0 = lp[0].innerHTML;
   var r = /ID=(\d+)/ig
   if ( r.test( lp0 ) )
    {
        res = lp0.match( r )
        lp0 = RegExp.$1
    }

Теперь нужно установить поле “Подстановка” в нужное значение - тут довольно объемный код, используется несколько функций, взятых вот отсюда: http://blogs.msdn.com/b/sharepointdesigner/archive/2007/06/13/using-javascript-to-manipulate-a-list-form-field.aspx
Эти функции копируются на нашу страницу как есть, без изменений. После этого для того чтобы установить поле “Подстановка” в значение достаточно написать, например:
setLookupFromFieldName("Команда заказавшая РП", lp0); // подставляет значение переменной lp0
или
setLookupFromFieldName("Связанная РП", vals["ID"]); // подставляет значение, полученное из адресной строки браузера
Таким образом, наш Заказ становится привязанным к Регламенту.

Иногда некоторые поля требуется установить в нужные значения явно. Например, для поля “Да/Нет”:
document.getElementById("ctl00_m_g_41362970_e9f4_429e_b60f_f62d1e52e332_ff3_new_ctl00_ctl00_BooleanField").checked=false;


Далее, некоторые поля нужно скрыть от пользователя, дабы избежать изменения пользователем. В моём случае это поля “Связанный регламент” и “Тип заказа”.
Для скрытия полей нам полей существует отличное решение в библиотеке jQuery. Достаточно подключить эти библиотеки, указав путь к ним следующим образом:
<script language="javascript" type="text/javascript" src="/jQuery%20Libraries/jquery-1.6.1.min.js"></script>

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

$('#ctl00_m_g_41362970_e9f4_429e_b60f_f62d1e52e332_ff24_new_ctl00_Lookup').closest('tr').hide();

Было бы неправильно не упомянуть здесь про SPServices - библиотеку на базе jQuery, написанную специально для Sharepoint. В этом проекте она не пригодилась, но тем не менее там очень много полезных функций. Например $().SPServices.SPCascadeDropdowns - позволяет фильтровать выпадающие значения поля “Подстановка” в зависимости от выбора в предыдущих полях и ещё множество других полезностей. Адрес проекта: http://spservices.codeplex.com

Проверка введённых полей

В SharePoint есть признак обязательности/необязательности поля и возможность задать формулу для проверки вводимых значений.
Иногда штатного механизма проверки полей оказываетя недостаточно. Например, если нужно сделать вводимое значение зависимым от других значений.
Для того, чтобы реализовать проверку полей, можно использовать возможности решения SharePoint Power EventReceiver 2010 взятого отсюда http://ilovesharepoint.codeplex.com/releases/view/55733
Установленное решение добавляет в параметры списка 2 дополнительных пункта меню:

Capture6

и позволяет написать код на powershell для любого обработчика событий. В случае, когда нам нужно проверить корректность вводимых значений, нам нужно определить функции ItemAdding и ItemUpdating
Для этого просто раскомментариваем их и пишем код вроде этого:

function ItemUpdating{
   if( $properties.AfterProperties["_x0413__x0440__x0443__x043f__x04"] -eq 0 ) { $atmsg="Команда заказавшая РП; "
       $warcnt=$warcnt+1
}
  if(($properties.AfterProperties["xpress"] -eq "true") -and ($warcnt))
{
$properties.ErrorMessage = "<b>Вам необходимо заполнить обязательные поля:</b><br>$atmsg<br>Нажмите Назад в браузере для возврата на форму заполнения)"
$properties.Cancel = $true
}
}

Будьте внимательны  с коварными Before- и AfterProperties, подробно про них можно почитать тут: http://gandjustas.blogspot.com/2011/05/blog-post.html

Ограничение доступа на закрытие задачи, оповещения о просроченной задаче.

Изначально в свежесозданном списке Задачи можно назначать задачу только одному человеку – однако это легко изменить, просто поменяв свойство “Может содерать несколько значений” для поля “Кому назначено”

Сложнее решается другой момент – как позволять изменять задачу только тем пользователям, которым она непосредственно назначена? В SharePoint Designer есть активность “Дать разрешения на элемент списка” – она доступна в так называемом “Шаге олицетворения”, в котором все активности выполняются от имени создателя рабочего процесса. Однако эта активность не работает с полем, в котором у нас несколько пользователей – работает только для одного.

Здесь можно воспользоваться ещё одним решением от создателя Power EventReceiver 2010 – оно называется Advanced Workflow Actions for SharePoint Designer 2010, находится по ссылке http://ilovesharepoint.codeplex.com/wikipage?title=Workflow%20Actions%20for%20SharePoint%20Designer%202010&referringTitle=Documentation и позволяет выполнять дополнительные активности в нашем рабочем процессе. Нас интересует активность Execute PowerShell Script – воспользуемся ей, чтобы разделить содержимое  поля “Кому назначено” на составляющие – на отдельных пользователей.

Вот как выглядит активность в workflow (запускаем на создании новой задачи):

Capture7

А вот её код, достаточно простой благодаря powershell:

Capture8

Понятно, ограничение этого решения - максимум 5 пользователей. Обычно этого более чем достаточно.

Наконец, как организовать оповещение пользователя об истечении срока по задаче (или о приближении этого срока, или о том что задача истекла 2 дня назад – или обо всем вместе) – необходим ещё один рабочий процесс с активностью “Сделать паузу до…”, он будет ждать наступления нужного срока и отправлять оповещение.

Редирект на нужную страницу

Время от времени требуется перенаправить пользователя на какую-либо страницу по нажатию кнопки “Сохранить”.

Если это всегда одна и та же ссылка, то достаточно удалить стандартную кнопку и создать свою с кодом наподобие этого:

<input type="button" value="Сохранить" name="btnFormAction" onclick="javascript: {ddwrt:GenFireServerEvent('__commit;__redirect={../orderz/myorders.aspx}')}" />

Если же направление редиректа зависит от выбранных полей, то поможет всё тот же Power EventReceiver 2010. Код примерно такой:

function ItemAdding{

   if ($properties.AfterProperties["reqtype"] -eq "Создание РП")
    {
$properties.Status = [Microsoft.SharePoint.SPEventReceiverStatus]::CancelWithRedirectUrl
$properties.RedirectUrl = "../orderz/NewForm.aspx"
}
   else
{
$properties.Status = [Microsoft.SharePoint.SPEventReceiverStatus]::CancelWithRedirectUrl
$properties.RedirectUrl = "../regproc"
}  
}

Ограничение доступа на создание/изменение без ограничения разрешений

Пользователю запрещено создавать, изменять или удалять элементы в списке Регламенты, однако если лишить его разрешений на выполнение этих действий, то и рабочему процессу не удастся проделать эти операции (если конечно не выполнять эти действия в Шаге олицетворения – но в этом случае автором будет создатель, что не всегда удобно)

Одно из решений – можно попросту сделать новые формы для списка – вместо традиционных NewForm, EditForm создать новые, поставить на них галочку “по умолчанию” и удалить оттуда все поля для редактирования, заменив их на какой-нибудь текст, объясняющий пользователю почему создавать/удалять Регламенты напрямую нельзя и что нужно сделать вместо этого – со ссылками и инструкциями.

Заключение

Конечно, для окончательной сдачи проекта заказчику требуются ещё некоторые “полировочные” действия – скрытие некоторых полей, создание пользовательских рабочих мест с настройкой представлений и меню в зависимости от роли в процессе. Однако основные “хитрости” в статье я перечислил, очень надеюсь на то что они кому-нибудь окажутся полезны.