29 авг. 2012 г.

Обработка изменения размеров окна

Когда мы меняем размер окна браузера, размеры ленты и рабочей области на сайте SharePoint пересчитываются заново и устанавливаются с помощью скриптов. Корректировка размеров ленты и рабочей области происходит в глобальной функции FixRibbonAndWorkspaceDimensions, которая определена в файле init.js.
Существует возможность добавить свои обработчики изменения размеров окна. Для этого достаточно добавить функцию-обработчик в глобальный массив g_workspaceResizedHandlers.
Например, выполнив в консоли браузера такой код (опустив лишние проверки), вы сможете видеть ширину и высоту рабочей области после изменения размеров окна:
function logWorkspaceSize() {
    var ws = document.getElementById('s4-workspace');
    console.log(ws.style.width + ' x ' + ws.style.height);
};
 
g_workspaceResizedHandlers.push(logWorkspaceSize);

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

SP.UI.Workspace.add_resized(logWorkspaceSize);

Есть в SharePoint один неприятный момент, связанный с изменением размеров элементов страницы: на странице со списком, если в представлении много колонок и они не убираются на экран, появляется прокрутка, при этом вкладка ленты "Обзор" и уведомления (которые SP.UI.Status) подгоняются под размер окна, а не представления.



Используя свой обработчик, можно это исправить:

  ExecuteOrDelayUntilScriptLoaded(function () {
    SP.UI.Workspace.add_resized(function () {
      var title = document.getElementById('s4-titlerow'),
      status = document.getElementById('s4-statusbarcontainer'),
      contentDivTable = document
        .getElementById('ctl00_MSO_ContentDiv')
        .getElementsByTagName('table')[0],
      contentTable = document.getElementById('MSO_ContentTable'),
      delta = contentDivTable.offsetWidth - contentTable.offsetWidth,
      newWidth = 0;
 
      if (delta > 0) {
        newWidth = title.offsetWidth + delta + 'px';
        title.style.width = newWidth;
        status.style.width = newWidth;
      }
    });
  }, 'init.js');
Функция FixRibbonAndWorkspaceDimensions не вызывается, если страница открыта в модальном окне, т.е. если в строке запроса присутствует параметр IsDlg=1. В этом случае надо самому устанавливать обработчик на изменение размеров окна.

28 авг. 2012 г.

Как получить экземпляр рабочего процесса

Перегружая методы класса SPWorkflowEventReceiver, мы можем обрабатывать события, связанные с рабочими процессами, например, запуск и остановку РП. Единственный источник данных о РП, для которого произошло событие, это объект типа SPWorkflowEventProperties.
Кроме InstanceId и WebUrl никаких других данных, идентифицирующих РП, не предоставляется. Нормальные люди для получения экземпляра РП используют конструктор SPWorkflow(SPWeb, Guid).

Не заметив сначала этот конструктор, я решил лезть в БД содержимого для извлечения необходимой информации. Всё же забывать о БД не стоит, потому как она может пригодиться для получения, например, всех РП, запущенных на элементах узла: выбираем записи по ИД узла, группируем по ИД списка и элемента. Можно ещё и по статусу отфильтровать. Будет работать быстрее, чем обход всех списков и элементов.

using (var connection = new SqlConnection(web.Site.ContentDatabase.DatabaseConnectionString))
{
    string commandText = 
        "select ListId, ItemId, Id from Workflow where WebId='" + web.ID + "'";

    using (var command = new SqlCommand(commandText, connection))
    {
        connection.Open();

        using (var reader = command.ExecuteReader())
        {
            if (reader.Read())
            {
                Guid listId = (Guid)reader["ListId"];
                int itemId = (int)reader["ItemId"];
                Guid workflowId = (Guid)reader["Id"];

                // группируем, как надо, и возвращаем результат
            }
        }
    }
}

30 июл. 2012 г.

Как привязать к списку свои ASPX-формы

Как быстро и просто программным способом назначить для списка свои формы, лежащие в _layouts? Для этого нужно взять у списка нужный тип содержимого и подменить часть XML в его определении. Для примера возьмём первый попавшийся тип содержимого и назначим ему формы просмотра и редактирования:

var ct = list.ContentTypes[0];

string ns = "http://schemas.microsoft.com/sharepoint/v3/contenttype/forms/url";
var xml = new System.Xml.XmlDocument();
xml.LoadXml(
"<FormUrls xmlns=\"" + ns + "\">" +
    "<Display>_layouts/MyDisplayForm.aspx</Display>" +
    "<Edit>_layouts/MyEditForm.aspx</Edit>" +
"</FormUrls>");

ct.XmlDocuments.Delete(ns);
ct.XmlDocuments.Add(xml);
ct.Update();

19 июл. 2012 г.

Рабочий процесс не открывается в дизайнере

Некоторое время не мог понять, почему один рабочий процесс перестал открываться в дизайнере: при открытии отображались несколько сообщений об ошибках типа
The service 'System.Workflow.ComponentModel.Compiler.ITypeProvider' must be installed for this operation to succeed. Ensure that this service is available.
The type 'System.Workflow.Activities.SequentialWorkflowActivity' has no property named 'CanModifyActivities'.
The service 'System.Workflow.ComponentModel.Design.IIdentifierCreationService' must be installed for this operation to succeed. Ensure that this service is available.
Оказалось, такая ошибка появляется, если в проекте есть CS-файл, который называется так же, как файл с классом РП.

5 июл. 2012 г.

Обновление рабочих процессов

Фактически никакой версионности в рабочих процессах (далее - РП) Visual Studio, в отличие от декларативных РП, не существует. То, что в различных статьях называется обновлением рабочего процесса, по сути представляет из себя копирование и развёртывание ещё одного РП. Так называемая новая версия связана со старой только посредством идентификатора, а названия и имена классов могут различаться. Никаких атрибутов для хранения номера версии в определении РП (workflow template) нет, и пользователь никогда не узнает, какой версией какого процесса он пользуется, если, конечно, разработчик не упомянет версию в названии или описании РП.

Необходимость в организации обновления РП возникает, если разработчик изменил класс РП, добавив туда новые активности, методы, поля и т.д. Если меняется только код методов, например, исправляются ошибки, то достаточно просто обновить решение, чтобы новый код попал в GAC, после этого РП нормально продолжат свою работу.
Если же разработчик изменил структуру РП, то простого обновления сборки в GAC недостаточно, ибо после такого обновления SharePoint не сможет восстановить состояние РП: сохранялась одна структура РП, а при восстановлении она оказывается другой.

Итак, три необходимых условия для осуществления обновления РП:
  1. Разный код для разных версий РП.
  2. Определения разных версий РП должны иметь одинаковый идентификатор.
  3. Определения разных версий РП должны находиться решениях с разными идентификаторами.
Первый принцип можно реализовать по-разному: создать разноименные классы в одной сборке или одноименные классы в разных сборках.
Остальные параметры решения: идентификаторы и названия возможностей, название и описание определения РП и пр. - можно менять или оставить прежними, на обновление это не влияет.

Я решил записать алгоритм одной из возможных реализаций изменения проекта для корректного обновления РП.

Шаг 1. Изменить идентификатор решения. 
В Visual Studio меняем свойства Solution Id у нашего WSP.

Шаг 2. Изменить версию сборки. 
Если версия сборки меняется при каждом билде, то это хорошо. Если же не меняется, то придётся поменять версию сборки вручную, чтобы новая версия работала вместе со старой.

Шаг 3. Добавить старую сборку в решение. 
Кто не знает, как это сделать, может прочитать инструкцию http://msdn.microsoft.com/ru-ru/library/ee231595.
При добавления сборки нужно в поле Location к имени файла добавить название подпапки (получится, например, "V1\MyWorkflow.dll"), потому что Visual Studio не добавит в решение два одноименных файла: старую сборку и текущую.

Шаг 4. Изменить определение РП. 
Этот шаг не обязателен. В атрибуте CodeBesideAssembly обычно указывается переменная $assemblyname$, так что прописывать новую версию сборки не потребуется. Можно изменить атрибут Name, добавив в название РП номер версии, исключительно для информирования пользователей.
Важно, что нельзя менять идентификатор определения - атрибут Id. Определения РП с одинаковым идентификатором, но находящиеся в разных решениях, SharePoint рассматривает как разные версии одного определения и подменяет старую версию новой. Если же определения находятся в одной возможности или в одном решении, то установится только первое.


Повторяясь, напомню, что этот алгоритм не единственно возможный. Хоть во всех статьях об обновлении РП и пишут о необходимости создания новой сборки, наличие этой новой сборки для развёртывания новой версии РП, строго говоря, не обязательно. Конечно, такой вариант грамотней и предпочтительней, но ведь можно пойти другим путём - создать копию класса РП в текущей сборке! Тогда выполнять шаги 2 и 3 не потребуется, а на шаге 4 вместо атрибута CodeBesideAssembly нужно будет поменять атрибут CodeBesideClass. Способ с копированием класса даже проще и быстрее и лучше подходит для ситуации, когда обновилась не только сборка с РП, но ещё десяток-другой связанных сборок, которые придётся копировать вместе со сборкой РП. Или, например, в сборке с РП находятся другие элементы: веб-части, приёмники событий и т.д. - для которых смена версии сборки влечёт необходимость дополнительных действий при обновлении.


Действия при обновлении РП следующие:
  1. Отозвать старое решение.
  2. Развернуть новое.
  3. Активировать возможность.
  4. Обновить существующие ассоциации с РП, включив их, потому что при деактивации старой возможности ассоциации отключаются.

27 февр. 2012 г.

Сообщения об ошибках

Сообщения об ошибках в SharePoint иногда напоминают вердикты дельфийской пифии: то есть хрен поймёшь, что имелось в виду. Вот почему, например, такой вызов
SPList.GetItemById(-1)
бросает исключение с таким сообщением:
One or more field types are not installed properly. Go to the list settings page to delete these fields.

Где, казалось бы, связь? Но, как и в случае со словами оракула, смысл этого сообщения становится ясен не сразу, в данном случае - после изучения метода GetItemById в декомпиляторе. Оказывается, если переданный идентификатор элемента меньше нуля, то в формируемый CAML-запрос вставляется не поле ID, а поле BdcIdentity, которого в списке нет.

24 февр. 2012 г.

Функция GoToPage

В скриптах SharePoint, в файле init.js, есть глобальная функция GoToPage, принимающая один параметр - адрес страницы. Алгоритм работы этой функции прост: происходит перенаправление на указанный в параметре адрес с добавленным параметром Source. В этот параметр Source записывается текущий адрес, т.е. адрес страницы, с которой происходит перенаправление.
Использовать функцию GoToPage целесообразно для открытия страниц, на которых имеются кнопки для закрытия формы, отмены операции или возврата на предыдущую страницу, ведь в адрес для возврата не нужно вычислять, он уже передан в параметре Source.
Пример:
<a href="javascript:GoToPage('/someFolder/somePage.aspx')">Some page</a>
Для проверки выполните в консоли браузера такой код:
GoToPage(window.location.pathname);

15 февр. 2012 г.

CopyUtil.aspx

В SharePoint существует страница приложения /_layouts/CopyUtil.aspx. Совсем не просто определить её назначение по названию. На самом деле она ничего никуда не копирует, а просто перенаправляет пользователя на форму определённого элемента списка.
Использование её целесообразно при формировании списка ссылок для элементов с разных узлов, например, для отображения результата запроса, сделанного через SPSiteDataQuery.

Рассмотрим, какие параметры следует добавить в адрес страницы CopyUtil.
Если нужный элемент находится на том же узле, на котором будет размещена ссылка, то достаточно добавить параметр InThisWeb=1. В противном случае мы должны как-то указать узел элемента: либо через идентификаторы, либо через адрес. Мы имеем два варианта указания нахождения элемента: по идентификаторам коллекции узлов, узла, списка и самого элемента, или же просто по адресу элемента. В первом случае нужно обязательно добавить параметры Use=id, SiteId, WebId (эти два последних игнорируются, если есть InThisWeb=1), ListId и ItemId. Во втором случае - добавить параметр ItemUrl.
Для того, чтобы открылась форма редактирования элемента, необходимо добавить параметр Action=editform. Чтобы направить пользователя на форму просмотра, добавляем Action=dispform или же вовсе пропускаем параметр Action.
Если добавить параметр Source, то он без изменения будет добавлен в адрес элемента.
Получив необходимые данные, страница CopyUtil откроет, если надо, узел, возьмёт нужный список и адреса его форм, сформирует адрес элемента и выполнит перенаправление с использованием метода SPUtility.Redirect.

Использование этой страницы вместе с публикацией имеет свои особенности и ограничения, про которые можно прочитать здесь: http://blog.mastykarz.nl/copyutilaspx-and-publishing-sites/

Download.aspx

Надо скачать какой-то файл с портала? Авторизовавшись, запрашиваем страницу /_layouts/download.aspx с параметром SourceUrl, в котором указываем адрес нужного файла. В ответ SharePoint вернёт содержимое файла.
Например, я хочу посмотреть, как сделана страница default.aspx, которая была добавлена в корневую папку сайта:
http://site/_layouts/download.aspx?SourceUrl=default.aspx